从redux-thunk到redux-saga实践

文章同步于Github Pines-Cheng/blog

简介

本质都是为了解决异步action的问题

Redux Saga能够理解为一个和系统交互的常驻进程,其中,Saga可简单定义以下:javascript

Saga = Worker + Watcher

saga特色:html

  • saga的应用场景是复杂异步,如长时事务LLT(long live transcation)等业务场景。
  • 方便测试,可使用takeEvery打印logger。
  • 提供takeLatest/takeEvery/throttle方法,能够便利的实现对事件的仅关注最近事件、关注每一次、事件限频
  • 提供cancel/delay方法,能够便利的取消、延迟异步请求
  • 提供race(effects),[…effects]方法来支持竞态和并行场景
  • 提供channel机制支持外部事件

Redux Saga适用于对事件操做有细粒度需求的场景,同时他们也提供了更好的可测试性。java

thunk VS saga

这里有一个简单的需求,登陆页面,使用redux-thunkasync / await。组件可能看起来像这样,像日常同样分派操做。react

组件部分两者应该是大同小异:ios

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

使用redux-thunk

登陆的action文件git

// auth.js

import request from 'axios';
import { loadUserData } from './user';

// define constants
// define initial state
// export default reducer

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

// more actions...

更新用户数据的页面:github

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

// more actions...

使用redux-saga

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST) //等待 Store 上指定的 action LOGIN_REQUEST
    try {
      let { data } = yield call(request.post, '/login', { user, pass }); //阻塞,请求后台数据
      yield fork(loadUserData, data.uid); //非阻塞执行loadUserData
      yield put({ type: LOGIN_SUCCESS, data }); //发起一个action,相似于dispatch
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

咱们使用形式yield call(func,… args)调用api函数。调用不会执行效果,它只是建立一个简单的对象,如{type:’CALL’,func,args}。执行被委托给redux-saga中间件,该中间件负责执行函数而且用其结果恢复generatorr。ajax

优势

相比Redux Thunk,使用Redux Saga有几处明显的变化:redux

  • 在组件中,再也不dispatch(action creator),而是dispatch(pure action)
  • 组件中再也不关注由谁来处理当前action,action经由root saga分发
  • 具体业务处理方法中,经过提供的call/put等帮助方法,声明式的进行方法调用
  • 使用ES6 Generator语法,简化异步代码语法

除开上述这些不一样点,Redux Saga真正的威力,在于其提供了一系列帮助方法,使得对于各种事件能够进行更细粒度的控制,从而完成更加复杂的操做。axios

方便测试

const iterator = loginSaga()

assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

注意,咱们经过简单地将模拟数据注入迭代器的下一个方法来检查api调用结果。模拟数据比模拟函数更简单。

监听过滤action

经过yield take(ACTION)能够方便自由的对action进行拦截和过滤。Thunks由每一个新动做的动做建立者调用(例如LOGIN_REQUEST)。即动做被不断地推送到thunk,而且thunk不能控制什么时候中止处理那些动做。

复杂应用场景

假设例如咱们要添加如下要求:

  • 处理LOGOUT用户操做
  • 在第一次成功登陆时,服务器返回token,该token在expires_in字段中存储的一些后到期。咱们必须在每隔expires_in毫秒时间后的从新向后台刷新受权
  • 考虑到在等待api调用的结果(初始登陆或刷新)时,用户能够在其间注销

你将如何实现这一点与thunk?同时还为整个流程提供全面的测试覆盖?

但是若是你使用redux-saga:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}

function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires_in)
    token = yield call(authorize, {token})
  }
}

function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN_REQUEST)

      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

在上面的例子中,咱们使用race表示了并发要求。

  • 若是take(LOGOUT)赢得比赛(即用户点击注销按钮)。比赛将自动取消authAndRefreshTokenOnExpiry后台任务。
  • 若是authAndRefreshTokenOnExpiry在调用(受权,{token})调用的中间被阻止,它也将被取消。取消自动向下传播。

其余特殊场景

同时执行多个任务

有时候咱们须要在几个ajax请求执行完以后,再执行对应的操做。redux-thunk须要借助第三方的库,而redux-saga是直接实现的。

import { call } from 'redux-saga/effects'

// 正确写法, effects 将会同步执行
const [users, repos] = yield [
  call(fetch, '/users'),
  call(fetch, '/repos')
]

当咱们须要 yield 一个包含 effects 的数组, generator 会被阻塞直到全部的 effects 都执行完毕,或者当一个 effect 被拒绝 (就像 Promise.all 的行为)。

监听action

在redux-saga中,咱们可使用了辅助函数 takeEvery 在每一个 action 来到时派生一个新的任务。 这多少有些模仿 redux-thunk 的行为:举个例子,每次一个组件调用 fetchProducts Action 建立器(Action Creator),Action 建立器就会发起一个 thunk 来执行控制流。

在现实状况中,takeEvery 只是一个在强大的低阶 API 之上构建的辅助函数。 在这一节中咱们将看到一个新的 Effect,即 take。take 让咱们经过全面控制 action 观察进程来构建复杂的控制流成为可能。

让咱们开始一个简单的 Saga 例子,这个 Saga 将监听全部发起到 store 的 action,而后将它们记录到控制台。

使用 takeEvery('')( 表明通配符模式),咱们就能捕获发起的全部类型的 action。

import { takeEvery } from 'redux-saga'

function* watchAndLog(getState) {
  yield* takeEvery('*', function* logger(action) {
    console.log('action', action)
    console.log('state after', getState())
  })
}

如今咱们知道如何使用 take Effect 来实现和上面相同的功能:

import { take } from 'redux-saga/effects'

function* watchAndLog(getState) {
  while(true) {
    const action = yield take('*')
    console.log('action', action)
    console.log('state after', getState())
  })
}

take 就像咱们更早以前看到的 call 和 put。它建立另外一个命令对象,告诉 middleware 等待一个特定的 action。 正如在 call Effect 的状况中,middleware 会暂停 Generator,直到返回的 Promise 被 resolve。 在 take 的状况中,它将会暂停 Generator 直到一个匹配的 action 被发起了。 在以上的例子中,watchAndLog 处于暂停状态,直到任意的一个 action 被发起。

注意,咱们运行了一个无限循环的 while(true)。记住这是一个 Generator 函数,它不具有 从运行至完成 的行为(run-to-completion behavior)。 Generator 将在每次迭代上阻塞以等待 action 发起。

一个简单的例子,假设在咱们的 Todo 应用中,咱们但愿监听用户的操做,并在用户初次建立完三条 Todo 信息时显示祝贺信息。

import { take, put } from 'redux-saga/effects'

function* watchFirstThreeTodosCreation() {
  for(let i = 0; i < 3; i++) {
    const action = yield take('TODO_CREATED')
  }
  yield put({type: 'SHOW_CONGRATULATION'})
}

与 while(true) 不一样,咱们运行一个只迭代三次的 for 循环。在 take 初次的 3 个 TODO_CREATED action 以后, watchFirstThreeTodosCreation Saga 将会使应用显示一条祝贺信息而后停止。这意味着 Generator 会被回收而且相应的监听不会再发生。

任务取消

一旦任务被 fork,可使用 yield cancel(task) 来停止任务执行。取消正在运行的任务,将抛出 SagaCancellationException 错误。

防抖动

为了对 action 队列进行防抖动,能够在被 fork 的任务里放置一个 delay。

const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))

function* handleInput(input) {
  // 500ms 防抖动
  yield call(delay, 500)
  ...
}

function* watchInput() {
  let task
  while(true) {
    const { input } = yield take('INPUT_CHANGED')
    if(task)
      yield cancel(task)
    task = yield fork(handleInput, input)
  }
}

在上面的示例中,handleInput 在执行以前等待了 500ms。若是用户在此期间输入了更多文字,咱们将收到更多的 INPUT_CHANGED action。 而且因为 handleInput 仍然会被 delay 阻塞,因此在执行本身的逻辑以前它会被 watchInput 取消。

经常使用

Effect

一个 effect 就是一个纯文本 JavaScript 对象,包含一些将被 saga middleware 执行的指令。

使用 redux-saga 提供的工厂函数来建立 effect。 举个例子,你可使用 call(myfunc, 'arg1', 'arg2') 指示 middleware 调用 myfunc('arg1', 'arg2') 并将结果返回给 yield 了 effect 的那个 Generator。

从 Saga 内触发异步操做(Side Effect)老是由 yield 一些声明式的 Effect 来完成的 (你也能够直接 yield Promise,可是这会让测试变得困难。使用 Effect 诸如 call 和 put,与高阶 API 如 takeEvery 相结合,让咱们实现与 redux-thunk 一样的东西, 但又有额外的易于测试的好处。

task

一个 task 就像是一个在后台运行的进程。在基于 redux-saga 的应用程序中,能够同时运行多个 task。经过 fork 函数来建立 task:

function* saga() {
  ...
  const task = yield fork(otherSaga, ...args)
  ...
}

Watcher/Worker

指的是一种使用两个单独的 Saga 来组织控制流的方式。

Watcher: 监听发起的 action 并在每次接收到 action 时 fork 一个 worker。

Worker: 处理 action 并结束它。

示例:

function* watcher() {
  while(true) {
    const action = yield take(ACTION)
    yield fork(worker, action.payload)
  }
}

function* worker(payload) {
  // ... do some stuff
}

take(pattern)

建立一条 Effect 描述信息,指示 middleware 等待 Store 上指定的 action。 Generator 会暂停,直到一个与 pattern 匹配的 action 被发起。

用如下规则来解释 pattern:

  • 若是调用 take 时参数为空,或者传入 '*',那将会匹配全部发起的 action(例如,take() 会匹配全部的 action)。
  • 若是是一个函数,action 会在 pattern(action) 返回为 true 时被匹配(例如,take(action => action.entities) 会匹配那些 entities 字段为真的 action)。
  • 若是是一个字符串,action 会在 action.type === pattern 时被匹配(例如,take(INCREMENT_ASYNC))。
  • 若是参数是一个数组,会针对数组全部项,匹配与 action.type 相等的 action(例如,take([INCREMENT, DECREMENT]) 会匹配 INCREMENT 或 DECREMENT 类型的 action)。

put(action)

用于触发 action,功能上相似于dispatch。

建立一条dispatch Effect 描述信息,指示 middleware 发起一个 action 到 Store。

直接使用dispatch:

//...

function* fetchProducts(dispatch)
  const products = yield call(Api.fetch, '/products')
  dispatch({ type: 'PRODUCTS_RECEIVED', products })
}

该解决方案与咱们在上一节中看到的从 Generator 内部直接调用函数,有着相同的缺点。若是咱们想要测试 fetchProducts 接收到 AJAX 响应以后执行 dispatch, 咱们还须要模拟 dispatch 函数。

相反,咱们须要一样的声明式的解决方案。只需建立一个对象来指示 middleware 咱们须要发起一些 action,而后让 middleware 执行真实的 dispatch。 这种方式咱们就能够一样的方式测试 Generator 的 dispatch:只需检查 yield 后的 Effect,并确保它包含正确的指令。

redux-saga 为此提供了另一个函数 put,这个函数用于建立 dispatch Effect

import { call, put } from 'redux-saga/effects'
//...

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  // 建立并 yield 一个 dispatch Effect
  yield put({ type: 'PRODUCTS_RECEIVED', products })
}

如今,咱们能够像上一节那样轻易地测试 Generator:

import { call, put } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// 指望一个 call 指令
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

// 建立一个假的响应对象
const products = {}

// 指望一个 dispatch 指令
assert.deepEqual(
  iterator.next(products).value,
  put({ type: 'PRODUCTS_RECEIVED', products }),
  "fetchProducts should yield an Effect put({ type: 'PRODUCTS_RECEIVED', products })"
)

call(fn, ...args)

用于调用异步逻辑,支持 promise 。

建立一条 Effect 描述信息,指示 middleware 调用 fn 函数并以 args 为参数。fn 既能够是一个普通函数,也能够是一个 Generator 函数。

middleware 调用这个函数并检查它的结果。

若是结果是一个 Generator 对象,middleware 会执行它,就像在启动 Generator (startup Generators,启动时被传给 middleware)时作的。 若是有子级 Generator,那么在子级 Generator 正常结束前,父级 Generator 会暂停,这种状况下,父级 Generator 将会在子级 Generator 返回后继续执行,或者直到子级 Generator 被某些错误停止, 若是是这种状况,将在父级 Generator 中抛出一个错误。

若是结果是一个 Promise,middleware 会暂停直到这个 Promise 被 resolve,resolve 后 Generator 会继续执行。 或者直到 Promise 被 reject 了,若是是这种状况,将在 Generator 中抛出一个错误。
当 Generator 中抛出了一个错误,若是有一个 try/catch 包裹当前的 yield 指令,控制权将被转交给 catch。 不然,Generator 会被错误停止,而且若是这个 Generator 被其余 Generator 调用了,错误将会传到调用的 Generator。

yield fork(fn ...args) 的结果是一个 Task 对象 —— 一个具有某些有用的方法和属性的对象

fork(fn, ...args)

建立一条 Effect 描述信息,指示 middleware 以 无阻塞调用 方式执行 fn。

fork 相似于 call,能够用来调用普通函数和 Generator 函数。但 fork 的调用是无阻塞的,在等待 fn 返回结果时,middleware 不会暂停 Generator。 相反,一旦 fn 被调用,Generator 当即恢复执行。
fork 与 race 相似,是一个中心化的 Effect,管理 Sagas 间的并发。

race(effects)

建立一条 Effect 描述信息,指示 middleware 在多个 Effect 之间执行一个 race(相似 Promise.race([...]) 的行为)。

api

redux-saga的其余详细API列举以下,API详解能够查看API 参考

  • Middleware API

    • createSagaMiddleware(...sagas)
    • middleware.run(saga, ...args)
  • Saga Helpers

    • takeEvery(pattern, saga, ...args)
    • takeLatest(pattern, saga, ..args)
  • Effect creators

    • take(pattern)
    • put(action)
    • call(fn, ...args)
    • call([context, fn], ...args)
    • apply(context, fn, args)
    • cps(fn, ...args)
    • cps([context, fn], ...args)
    • fork(fn, ...args)
    • fork([context, fn], ...args)
    • join(task)
    • cancel(task)
    • select(selector, ...args)
  • Effect combinators

    • race(effects)
    • [...effects] (aka parallel effects)
  • Interfaces

    • Task
  • External API

    • runSaga(iterator, {subscribe, dispatch, getState}, [monitor])

参考

相关文章
相关标签/搜索