前言
笔者最近在做一些后台项目, 使用的是 Ant Design Pro https://github.com/ant-design/ant-design-pro , 其使用了 https://github.com/redux-saga/redux-saga 处理异步数据流, 本文将对 redux-saga 的原理做一个简单的解读, 并将实现一个简易版的 redux-saga.
Generator 函数的自动流程控制
在 redux-saga 中, saga 是指一些长时操作, 用 generator 函数表示. generator 函数的强大之处在于其可以手动的暂停, 恢复执行, 且可以与函数体外进行数据交互, 看如下例子:
- function *gen() {
- const a = yield 'hello';
- console.log(a);
- }
- cont g = gen();
- g.next(); // { value: 'hello', done: false }
- setTimeout(() => g.next('hi'), 1000) // 此时 a => 'hi' 一秒后打印'hi'
可以看出来 genrator 函数何时进行下一步操作完全取决于外部的调度时机, 且其内部执行状态也由外部的输入决定, 这使得 generator 函数可以很方便的做异步流程控制. 举个例子, 我们首先读取一个文件的内容作为查询参数, 然后请求一个查询接口并把返回的内容打印出来:
- function getParams(file) {
- return new Promise(resolve => {
- fs.readFile(file, (err, data) => {
- resolve(data)
- })
- })
- }
- function getContent(params) {
- // request 返回 promise
- return request(params)
- }
- function *gen() {
- const params = yield getParams('config.json');
- const content = yield getContent(params);
- console.log(content);
- }
我们可以手动控制 gen 函数的执行:
- const g = gen();
- g.next().value.then(params => {
- g.next(params).value.then(content => {
- g.next(content);
- })
- })
以上可以达到我们的目的, 但是过于繁琐, 我们想要的是 generator 函数可以自动的执行, 可以写一个简易的自动执行函数如下:
- function genRun(gen) {
- const g = gen();
- next();
- function next(err, pre) {
- let temp;
- (err === null) && (temp = g.next(pre));
- (err !== null) && (temp = g.throw(pre));
- if(!temp.done) {
- nextWithYieldType(temp.value, next);
- }
- }
- }
- function nextWithYieldType(value, next) {
- if(isPromise(value)) {
- value
- .then(success => next(null, success))
- .catch(error => next(error))
- }
- }
- genRun(gen);
此时 generator 函数便可以自动执行, 事实上我们可以发现, generator 的内部状态完全是由 nextWithYieldType 决定的, 我们可以根据 yield 的类型执行不同的处理逻辑.
Effect
事实上 sagaMiddleware.run(saga) 可以类似看做 genRun(saga), 而 saga 是由一个个的 effect 组成的, 那么 effect 是什么? redux-saga 官网的解释: 一个 effect 就是一个 Plain Object JavaScript 对象, 包含一些将被 saga middleware 执行的指令. redux-saga 提供了很多 Effect 创建器, 如 call,put,take 等, 已 call 为例:
- function saga*() {
- const result = yield call(genPromise);
- console.log(result);
- }
call(genPromise) 生成的就是一个 effect, 它可能类似如下:
- {
- isEffect: true,
- type: 'CALL',
- fn: genPromise
- }
事实上 effect 只表明了意图, 而实际的行为由类似于上文的 nextWithYieldType 完成, 例如:
- function nextWithYieldType(value, next) {
- ...
- if(isCallEffect(value)) {
- value.fn(). then(success => next(null, success)).catch(error => next(error))
- }
- }
当 genPromise 函数返回的 promise 被 resolve 后便会打印出结果.
生产者与消费者
观察下面的例子
- function *saga() {
- yield take('TEST');
- console.log('test...');
- }
- sagaMiddleware.run(test);
saga 会在 take('TEST') 处阻塞, 只有执行了 dispatch({type: 'TEST'}) 后 saga 才能继续运行 (注意: 此时的 dispatch 方法是经过 sagaMiddleware 包装过的). 这给我们的感觉似乎很像是 take 是一个生产者, 在等待 disaptch 的消费, 事实上 take 只是一个 Effect 生成器, 具体的处理逻辑依然是在 nextWithYieldType 完成的, 类似于:
- function nextWithYieldType(value, next) {
- ...
- // take('TEST') 生成的 effect 简单的认为是 {isEffect: true, type: 'TAKE', name: 'TEST'}
- if(isTakeEffect(value)) {
- channel.take({pattern: value.name, cb: params => next(null, params)})
- }
- }
channel 是一个任务生成器, 它有两个方法: take 生成任务, put 消费任务:
- function channel() {
- /*
- task = {
- pattern,
- cb
- }
- */
- let _task = null;
- function take(task) {
- _task = task;
- }
- function put(pattern, args) {
- if(!_task) return;
- if(pattern == _task.pattern) _task.cb.call(null, args);
- }
- return {
- take,
- put
- }
- }
显然任务是在执行 dispatch 的时候被消费掉的, 这个工作是在 sagaMiddleware 中做的, 类似于如下:
- const sagaMiddleware = store => {
- return next => action => {
- next(action);
- const { type, ...payload } = action;
- channel.put(type, payload);
- }
- }
看到这里我们可以发现, 需要我们做的就是不断的完善 nextWithYieldType 这个函数, 当完成了 put,fork,takeEvery 对应的逻辑后, 一个具备基本功能的 redux-saga 就诞生啦, 笔者就不在赘述这些功能的实现了. 最后, 你可以查看这里:, 这是笔者实现的一个简易版的 redux-saga, 希望对你有所帮助.
全文完.
来源: https://juejin.im/post/5bd5bd8bf265da0aca33544d