redux-saga 和 redux-thunk 是最广为人知的2种 redux 的异步流处理方案。在认识 redux-saga 之前,我们先再看一看 redux-thunk。
在 redux 里,dispatch 出的 action 是一个对象,而 redux-thunk 中间件可以返回一个函数,函数处理完之后再 dispatch 一个对象到 reducer 里。redux 的源码也相当简单:
1 | function createThunkMiddleware (extraArgument) { |
从源码里可以看到 redux-thunk 先判断到来的 action 是否为函数,如果是函数的话,就调用这个函数,否则就执行下一个 action。
redux-thunk 的缺点很明显,redux 只是执行了这个函数,不会在乎函数主体是什么。每一个异步操作都发起一个有副作用的 action,这样异步代码会分布在每一个 action里,形式不统一,也不易维护。
而 redux-saga 则是将所有的异步操作都统一放到了 saga的文件函数里,使异步操作可以被集中处理,同时也达到了形式上的统一,易于维护。
如何使用 redux-saga
redux-saga 包括三个部分
- worker saga
做所有的工作,如调用 API,进行异步请求,并且获得返回结果- watcher saga
监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务- root saga
立即启动 sagas 的唯一入口
首先在 store 的入口文件里加上对应的中间件
1 | import { createStore, applyMiddleware, compose } from 'redux' |
之后在 sagas 的文件夹里集中写 saga 的代码
1 | import axios from 'axios' |
常用 API
take,takeEvery,takeLatest
创建一条 Effect 描述信息,指示 middleware 等待 Store 上指定的 action。 Generator 会暂停,直到一个与 pattern 匹配的 action 被发起
1 | function* mySaga() { |
与 take 有类似功能的 API 还有 takeEvery 和 takeLatest
- takeEvery 允许多个 getInitList 实例同时启动,在某个特定时刻,尽管之前还有一个或多个 getInitList 尚未结束,我们还是可以启动一个新的 getInitList 任务
- 和 takeEvery 不同,在任何时刻 takeLatest 只允许一个 getInitList 任务在执行。并且这个任务是最后被启动的那个。 如果已经有一个任务在执行的时候启动另一个 getInitList ,那之前的这个任务会被自动取消。
put
用于触发 action,功能上类似于dispatch
1 | function* getInitList() { |
call(fn, …args)
call 创建了一条描述结果的信息, 用于调用异步逻辑, 指示 middleware 调用 fn 函数并以 args 为参数。fn 既可以是一个普通函数,也可以是一个 Generator 函数。
如果结果是一个 Generator 对象,middleware 会执行它,如果结果是一个 Promise,middleware 会暂停直到这个 Promise 被 resolve,resolve 后 Generator 会继续执行。
1 | function* fetchProducts() { |
假设我们想测试上面的 generator:
1 | const iterator = fetchProducts() |
而如果我们使用 call 去调用这个 ajax 方法
1 | function* fetchProducts() { |
测试的代码就可以这样写了
1 | const iterator = fetchProducts() |
fork(fn, …args)
创建一条 Effect 描述信息,指示 middleware 以 无阻塞调用 方式执行 fn。
1 | const task = yield fork(takeLatest, 'GET_PERSON_DATA', getPersonData) |
这里面通过 takeLatest 去监听 type: ‘GET_PERSON_DATA’ 的 action,当有一个这样的 action 被触发,就会 fork 出一个 task
这边讲下 fork 与 call 的区别
- fork 是非阻塞的,非阻塞就是遇到它,不需要等它执行完, 就可以直接往下运行
- call 是阻塞,阻塞的意思就是一定要等它执行完, 才可以直接往下运行
- fork是返回一个任务,这个任务是可以被取消的;而call就是它执行的正常返回结果!(非常重要)
cancel
一旦任务被 fork,可以使用 yield cancel(task) 来中止任务执行。取消正在运行的任务。
1 | while (yield take('START_BACKGROUND_SYNC')) { |
all
效果与 Promise.all 相对应,创建一个 Effect 描述信息,用来命令 middleware 并行地运行多个 Effect,并等待它们全部完成
1 | yield all([ |
当并发运行 Effect 时,middleware 将暂停 Generator,直到以下任一情况发生:
- 所有 Effect 都成功完成:返回一个包含所有 Effect 结果的数组,并恢复 Generator。
- 在所有 Effect 完成之前,有一个 Effect 被 reject:在 Generator 中抛出 reject 错误。
race
效果与 Promise.race 相对应,创建一个 Effect 描述信息,用来命令 middleware 在多个 Effect 间进行竞赛
1 | const [response, cancel] = yield race([ |
如果 fetchUsers 先 resolve,那么 response 将是 fetchUsers 的结果,并且 cancel 将是 undefined
如果在 fetchUsers 完成之前,Store 上先发起了一个 ‘CANCEL_FETCH’ 类型的 action,那么 response 将是 undefined,并且 cancel 将是被发起的 action
delay
返回一个 effect 描述信息,用于阻塞执行 ms 毫秒,并返回 val 值
1 | const [posts, timeout] = yield race({ |
上面的例子其实就是限制了 fetchUsers 必须在 1秒内作出响应,否则会作超时处理。
优点
- 查询与责任分离,保证了action的纯洁性,符合 redux 设计思想
- 实现以同步方式写异步操作,容易理解,逻辑清晰
- 通过发送指令而不是直接调用让异步操作变得容易测试
- 监听、执行自动化
- 高级的异步控制流以及并发管理,实现颗粒更小的异步控制,通过
fork
实现并发任务。 - 架构上的优势:将所有的异步流程控制都移入到了 sagas,UI 组件不用执行业务逻辑,只需 dispatch action 就行,增强组件复用性
缺点
- action 任务拆分更细,原有流程上相当于多了一个环节,对开发者的设计和抽象拆分能力更有要求
- 代码复杂性也有所增加,比较复杂,学习成本高
- 异步请求相关的问题较难调试排查
demo
写了一个 redux-saga 的一个小 demo,书写了 redux-saga 的主要架构,并实现了发送请求,取消请求和超时请求几个小功能。