背景

Redux是React的一个状态管理工具,通过单一状态树以及将状态修改操作聚拢到reducer的方式实现状态管理的 可预测 。但整个过程都是 同步的 ,Redux本身并没有内置提供异步操作数据的机制,所以需要中间件来进行这部分的任务,其中最为著名的两个插件便是 redux-thunk , redux-saga

redux-thunk

redux-thunk的基本思路是将 action 定义为一个接受 dispatchgetState 为参数的函数,以此实现在 action 当中对 dispatch 执行时机的控制。简单的应用如下:

  1. // actionCreator.js
  2. const incrementAfter = (ms) => (dispatch, getState) => {
  3. setTimeout(() => {
  4. dispatch({
  5. type: 'INCREMENT'
  6. })
  7. }, ms)
  8. }
  9. }
  10. const increment = () => {
  11. return {
  12. type: 'INCREMENT'
  13. }
  14. }
  15. const store = createStore(reducers, applyMiddleware(thunk))
  16. // 1000ms后加1
  17. store.dispatch(incrementAfter(1000))

而redux-thunk中间件的 实现原理 也非常简单,通过判断 action 的类型来确定 dispatchaction 的调用关系,如果 action 为函数,则 dispatch 作为参数传入,反之, action 作为 dispatch 的参数传入。

  1. function createThunkMiddleware(extraArgument) {
  2. return ({ dispatch, getState }) => (next) => (action) => {
  3. if (typeof action === 'function') {
  4. return action(dispatch, getState, extraArgument);
  5. }
  6. return next(action);
  7. };
  8. }

redux-thunk的优缺点都非常明显,优点是使用起来非常简单,只需要定义相关的异步函数即可,缺点也在于简单,面对复杂一点的异步流程(譬如打断的情况)缺乏内置的管理方法,导致action难以管理的情况。

redux-saga

saga可以理解为一个专门处理副作用的线程(这个线程并非真实存在),redux-saga使用了ES6 generator函数作为处理副作用的方式,,所以这个专门处理副作用的函数称为 saga函数

redux-saga的 基本原理 是通过内部提供的一系列 副作用函数 (call, put等等),生成对应的 副作用描述符(Effect descriptor) ,将此副作用描述符交给 中间件 执行。也就是说,redux-saga的 副作用的创建与执行是分离 的,这么做的一个显著优势就是使得 测试变得非常方便 ,测试副作用的正确性只需要判断对应的描述符即可。

除此之外,redux-saga本身内置了丰富多样的处理副作用的函数,能够 轻松构建复杂的异步控制流 ,这对比redux-thunk来说是一个显著的优势。

基本API

take

等待一个action或者channel,这意味着generator函数会被暂停,直至相应action被dispatch才会继续。这个函数的语义是 等待 ,不同于takeEvery的 被动地 每次触发( pushed模式 ),使用take的saga函数是 主动 获取对应的action或者channel,也能决定继续监听action还是取消监听( pull模式 )。

得益于这种 pull模式 ,我们的saga函数能够使用take实现复杂的控制流,譬如以下的登入登出的控制流,下面的控制流满足了几个需求:

  1. 获取登陆请求后进行认证
  2. 进行认证的过程 不能阻塞 登出操作
  3. 认证成功后保存 认证信息
  4. 登出时如果存在正在进行登陆认证,则需要 取消
  5. 登出或者认证失败则需要清空认证信息,然后 等待 下一轮登陆请求

在这个例子当中可以清晰地看到 redux-saga 在构建复杂的异步控制流方面的强大能力,这在 redux-thunk 中是无法想象的。

  1. function* authorize(user, password) {
  2. try {
  3. const token = yield call(Api.authorize, user, password)
  4. yield put({type: 'LOGIN_SUCCESS', token})
  5. yield call(Api.storeItem, {token})
  6. return token
  7. } catch(error) {
  8. yield put({type: 'LOGIN_ERROR', error})
  9. } finally {
  10. if (yield cancelled()) {
  11. // ... put special cancellation handling code here
  12. }
  13. }
  14. }
  15. function* loginFlow() {
  16. while (true) {
  17. const {user, password} = yield take('LOGIN_REQUEST')
  18. // fork return a Task object
  19. const task = yield fork(authorize, user, password)
  20. const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
  21. if (action.type === 'LOGOUT')
  22. yield cancel(task)
  23. yield call(Api.clearItem, 'token')
  24. }
  25. }

takeEvery

处理每个符合条件的action或channel,每次命中都会执行对应的saga函数

  1. const takeEvery = (pattern, saga, ...args) => fork(function*() {
  2. while (true) {
  3. const action = yield take(pattern)
  4. yield fork(saga, ...args.concat(action))
  5. }
  6. })

takeLatest

处理最近一个符合条件的action或channel,如果前一个saga没完成,则会将其终止

适用于处理连续重复发送的ajax请求

  1. const takeLatest = (pattern, saga, ...args) => fork(function*() {
  2. let lastTask
  3. while (true) {
  4. const action = yield take(pattern)
  5. if (lastTask) {
  6. yield cancel(lastTask) // cancel is no-op if the task has already terminated
  7. }
  8. lastTask = yield fork(saga, ...args.concat(action))
  9. }
  10. })

函数调用

call

阻塞调用 函数,直至函数返回saga函数才会继续向下执行。一般用于等待请求结果后改变请求状态的情景。

fork

非阻塞调用 函数,函数的调用并不会阻塞saga函数的执行。可使用 cancel 函数进行取消。

组合

all

类似Promise.all,多个saga函数并行调用,一个错误其他则停止,所有saga完成后再进行下一步。典型应用是并行请求。

race

类似Promise.race,一个saga结束(无论成功失败)后立即下一步,一个典型应用就是请求超时。

actionChannel

主要特性是能够 缓存 没来得及处理的action(譬如call)。

对于处理 无阻塞并行请求 的情况,一般的操作是通过 take+fork 的形式来实现:

  1. function *watchRequest() {
  2. while(true) {
  3. const {payload} = yield take('REQUEST')
  4. yield fork(handleRequest, payload)
  5. }
  6. }

这种方式的局限在于无法实现将请求 队列化 ,意思是将接收到的请求逐个处理(前一请求完毕后再进行下一请求)。

而actionChannel能将没有来得及处理的action缓存下来,以下代码为例:

  1. function *watchRequest() {
  2. const channel = yield(actionChannel('REQUEST'))
  3. while(true) {
  4. const {payload} = yield take(channel)
  5. yield call(handleRequest, payload)
  6. }
  7. }

在上面的代码当中,take接受的参数是一个actionChannel,actionChannel将所有的action以队列的形式进行缓存,当执行take时,则会返回队头action,得到参数后由call调用,当call调用完成后,则会继续从channel当中获取action进行处理。

eventChannel

eventChannel与actionChannel类似,都能作为 数据源 。eventChannel的 订阅函数 有一个emmit参数,功能是发送数据,其返回值则是一个取消订阅的函数。

  1. function countdown(secs) {
  2. return eventChannel(emitter => {
  3. const iv = setInterval(() => {
  4. secs -= 1
  5. if (secs > 0) {
  6. emitter(secs)
  7. } else {
  8. // this causes the channel to close
  9. emitter(END)
  10. }
  11. }, 1000);
  12. // The subscriber must return an unsubscribe function
  13. return () => {
  14. clearInterval(iv)
  15. }
  16. }
  17. )
  18. }
  19. export function* saga() {
  20. const chan = yield call(countdown, value)
  21. try {
  22. while (true) {
  23. // take(END) will cause the saga to terminate by jumping to the finally block
  24. let seconds = yield take(chan)
  25. console.log(`countdown: ${seconds}`)
  26. }
  27. } finally {
  28. console.log('countdown terminated')
  29. }
  30. }

在上述例子当中,countDown返回一个eventChannel,此eventChannel能作为saga函数的数据源,每一秒钟输出一个数据,当数据为 END 时,当前saga会被终止,进入到finally块当中。

总结

  • redux-thunk优点是使用简单,容易上手,缺点是面对复杂的异步流程有点力不从心,因此适用于异步流程简单的业务场景
  • redux-saga优点是拥有各种内置的异步流程控制工具,能够轻松构建出复杂的异步流程,缺点是学习成本高,上手需要一定的时间,适用于各种业务场景,不考虑上手成本应该优先使用redux-saga。

参考资料

https://www.vhudyma-blog.eu/2020-07-25-redux-thunk-vs-redux-saga-the-differences/
https://redux-saga.js.org/docs/introduction/BeginnerTutorial.html
https://stackoverflow.com/questions/34930735/pros-cons-of-using-redux-saga-with-es6-generators-vs-redux-thunk-with-es2017-asy