一.目标定位express
redux-saga is a library that aims to make side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) in React/Redux applications easier and better.
做为一个Redux中间件,想让Redux应用中的反作用(即依赖/影响外部环境的不纯的部分)处理起来更优雅redux
二.设计理念
Saga像个独立线程同样,专门负责处理反作用,多个Saga能够串行/并行组合起来,redux-saga负责调度管理api
Saga来头不小(1W star不是浪得的),是某篇论文中提出的一种分布式事务机制,用来管理长期运行的业务进程promise
P.S.关于Saga背景的更多信息,请查看Background on the Saga concept并发
三.核心实现
利用generator,让异步流程控制易读、优雅、易测试app
In redux-saga, Sagas are implemented using Generator functions. To express the Saga logic we yield plain JavaScript Objects from the Generator.
实现上,关键点是:框架
以generator形式组织逻辑序列(function* + yield),把一系列的串行/并行操做经过yield拆分开异步
利用iterator的可“暂停/恢复”特性(iter.next())分步执行async
经过iterator影响内部状态(iter.next(result)),注入异步操做结果分布式
利用iterator的错误捕获特性(iter.throw(error)),注入异步操做异常
用generator/iterator实现是由于它很是适合流程控制的场景,体如今:
yield让描述串行/并行的异步操做变得很优雅
以同步形式获取异步操做结果,更符合顺序执行的直觉
以同步形式捕获异步错误,优雅地捕获异步错误
P.S.关于generator与iterator的关系及generator基础用法,能够参考generator(生成器)_ES6笔记2
例如:
const ts = Date.now(); function asyncFn(id) { return new Promise((resolve, reject) => { setTimeout(() => { console.log(`${id} at ${Date.now() - ts}`); resolve(id); }, 1000); }); } function* gen() { // 串行异步 let A = yield asyncFn('A'); console.log(A); let B = yield asyncFn('B'); console.log(B); // 并行异步 let C = yield Promise.all([asyncFn('C1'), asyncFn('C2')]); console.log(C); // 串行/并行组合异步 let D = yield Promise.all([ asyncFn('D1-1').then(() => { return asyncFn('D1-2'); }), asyncFn('D2') ]); console.log(D); } // test let iter = gen(); // 尾触发顺序执行iter.next let next = function(prevResult) { let {value: result, done} = iter.next(prevResult); if (result instanceof Promise) { result.then((res) => { if (!done) next(res); }, (err) => { iter.throw(err); }); } else { if (!done) next(result); } }; next();
实际结果符合预期:
A at 1002 A B at 2012 B C1 at 3015 C2 at 3015 ["C1", "C2"] D1-1 at 4019 D2 at 4020 D1-2 at 5022 ["D1-2", "D2"]
执行顺序为:A -> B -> C1,C2 -> D1-1 -> D2 -> D1-2
redux-saga的核心控制部分与上面示例相似(没错,就是这么像co),从实现上看,其异步控制的关键是尾触发顺序执行iter.next。示例没添Effect这一层描述对象,从功能上讲Effect并不重要(Effect的做用见下面术语概念部分)
Effect层要实现的东西包括2部分:
业务操做 -> Effect
以Effect creator API形式提供,提供各类语义的用来生成Effect的工具函数,例如把dispatch action包装成put、把方法调用包装成call/apply
Effect -> 业务操做
在执行时内部进行转换,例如把[Effect1, Effect2]转换为并行调用
相似于装箱(把业务操做用Effect包起来)拆箱(执行Effect里的业务操做),此外,完整的redux-saga还要实现:
做为middleware接入到Redux
提供读/写Redux state的接口(select/put)
提供监听action的接口(take/takeEvery/takeLatest)
Sagas组合、通讯
task顺序控制、取消
action并发控制
…
差很少是一个大而全的异步流程控制库了,从实现上看,至关于一个加强版的co
四.术语概念
Effect
Effect指的是描述对象,至关于redux-saga中间件可识别的操做指令,例如调用指定的业务方法(call(myFn))、dispatch指定action(put(action))
An Effect is simply an object which contains some information to be interpreted by the middleware.
Effect层存在的主要意义是为了易测试性,因此用简单的描述对象来表示操做,多这样一层指令
虽然能够直接yield Promise(好比上面核心实现里的示例),但测试case中没法比较两个promise是否等价。因此添一层描述对象来解决这个问题,测试case中能够简单比较描述对象,实际起做用的Promise由redux-saga内部生成
这样作的好处是单测中不用mock异步方法(通常单测中会把全部异步方法替换掉,只比较传入参数是否相同,而不作实际操做),能够简单比较操做指令(Effect)是否等价。从单元测试的角度来看,Effect至关于把参数提出去了,让“比较传入参数是否相同”这一步能够在外面统一进行,而不用逐个mock替换
P.S.关于易测试性的更多信息,请查看Testing Sagas
另外,mock测试不但比较麻烦,还不可靠,毕竟与真实场景/流程有差别。经过框架约束,多一层描述对象来避免mock
这样作并不十分完美,还存在2个问题:
业务代码稍显麻烦(不直接yield promise/dispatch action,而都要用框架提供的creator(call, put)包起来)
有额外的学习成本(理解各个creator的语义,适应先包一层的玩法)
例如:
// 直接 const userInfo = yield API.fetch('user/info', userId); // 包一层creator const userInfo = yield call(API.fetch, 'user/info', userId); // 并指定context,默认是null const userInfo = yield call([myContext, API.fetch], 'user/info', userId);
形式上与fn.call相似(实际上也提供了一个apply creator,形式与fn.apply相似),内部处理也是相似的:
// call返回的描述对象(Effect) { @@redux-saga/IO: true, CALL: { args: ["user/info", userId], context: myContext, fn: fetch } } // 实际执行 result = fn.apply(context, args)
写起来不那么直接,但比起易测试性带来的好处(不用mock异步函数),这不很过度
注意,不须要mock异步函数只是简化了单元测试的一个环节,即使使用这种对比描述对象的方式,仍然须要提供预期的数据,例如:
// 测试场景直接执行 const iterator = fetchProducts() // expects a call instruction assert.deepEqual( iterator.next().value, call(Api.fetch, '/products'), "fetchProducts should yield an Effect call(Api.fetch, './products')" ) // 预期接口返回数据 const products = {} // expects a dispatch instruction assert.deepEqual( iterator.next(products).value, put({ type: 'PRODUCTS_RECEIVED', products }), "fetchProducts should yield an Effect put({ type: 'PRODUCTS_RECEIVED', products })" )
P.S.这种描述对象的套路,和Flux/Redux的action一模一样:Effect至关于Action,Effect creator至关于Action Creator。区别是Flux用action描述消息(发生了什么),而redux-saga用Effect描述操做指令(要作什么)
Effect creator
redux-saga/effects提供了不少用来生成Effect的工具方法。经常使用的Effect creator以下:
阻塞型方法调用:call/apply 详见Declarative Effects
非阻塞型方法调用:fork/spawn 详见redux-saga’s fork model
并行执行task:all/race 详见Running Tasks In Parallel,Starting a race between multiple Effects
读写state:select/put 详见Pulling future actions
task控制:join/cancel/cancelled 详见Task cancellation
大多creator语义都很直白,只有一个须要额外说明下:
join用来获取非阻塞的task的返回结果
其中fork与spawn都是非阻塞型方法调用,两者的区别是:
经过spawn执行的task彻底独立,与当前saga无关
当前saga无论它执行完了没,发生cancel/error也不会影响当前saga
效果至关于让指定task独立在顶层执行,与middleware.run(rootSaga)相似
经过fork执行的task与当前saga有关
fork所在的saga会等待forked task,只有在全部forked task都执行结束后,当前saga才会结束
fork的执行机制与all彻底一致,包括cancel和error的传递方式,因此若是任一task有未捕获的error,当前saga也会结束
另外,cancel机制比较有意思:
对于执行中的task序列,全部task天然完成时,把结果向上传递到队首,做为上层某个yield的返回值。若是task序列在处理过程当中被cancel掉了,会把cancel信号向下传递,取消执行全部pending task。另外,还会把cancel信号沿着join链向上传递,取消执行全部依赖该task的task
简言之:complete信号沿调用链反向传递,而cancel信号沿task链正向传递,沿join链反向传递
注意:yield cancel(task)也是非阻塞的(与fork相似),而被cancel掉的任务在完成善后逻辑后会当即返回
P.S.经过join创建依赖关系(取task结果),例如:
function* rootSaga() { // Returns immediately with a Task object const task = yield spawn(serverHello, 'world'); // Perform an effect in the meantime yield call(console.log, "waiting on server result..."); // Block on the result of serverHello const result = yield join(task); }
Saga
术语Saga指的是一系列操做的集合,是个运行时的抽象概念
redux-saga里的Saga形式上是generator,用来描述一组操做,而generator是个具体的静态概念
P.S.redux-saga里所说的Saga大多数状况下指的都是generator形式的一组操做,而不是指redux-saga自身。简单理解的话:在redux-saga里,Saga就是generator,Sagas就是多个generator
Sagas有2种顺序组合方式:
yield* saga() call(saga)
一样,直接yield iterator运行时展开也面临不便测试的问题,因此经过call包一层Effect。另外,yield只接受一个iterator,组合起来不很方便,例如:
function* saga1() { yield 1; yield 2; } function* saga2() { yield 3; yield 4; } function* rootSaga() { yield 0; // 组合多个generator不方便 yield* (function*() { yield* saga1(); yield* saga2(); })(); yield 5; } // test for (let val of rootSaga()) { console.log(val); // 0 1 2 3 4 5 }
注意:实际上,call(saga)返回的Effect与其它类型的Effect没什么本质差别,也能够经过all/race进行组合
Saga Helpers
Saga Helper用来监听action,API形式是takeXXX,其语义至关于addActionListener:
take:语义至关于once
takeEvery:语义至关于on,容许并发action(上一个没完成也当即开始下一个)
takeLatest:限制版的on,不容许并发action(pending时又来一个就cancel掉pending的,只作最新的)
takeEvery, takeLatest是在take之上的封装,take才是底层API,灵活性最大,能手动知足各类场景
P.S.关于3者关系的更多信息,请查看Concurrency
pull action与push action
从控制方式上讲,take是pull的方式,takeEvery, takeLatest是push的方式
pull与push是指:
pull action:要求业务方主动去取action(yeild take()会返回action)
push action:由框架从外部注入action(takeEvery/takeLatest注册的Saga会被注入action参数)
pull方式的优点在于:
容许更精细的控制
好比能够手动实现takeN的效果(只关注某几回action,用完就释放掉)
以同步形式描述控制流
takeEvery, takeLatest只支持单action,若是是action序列的话要拆开,用take能保留关联逻辑块的完整性,好比登陆/注销
别人更容易理解
控制逻辑在业务代码里,而不是藏在框架内部机制里,必定程度上下降了维护成本
P.S.关于pull/push的更多信息,请查看Pulling future actions
五.场景示例
有几个印象比较深的场景,充分体现出了redux-saga的优雅
接口访问
function* fetchProducts() { try { const products = yield call(Api.fetch, '/products') yield put({ type: 'PRODUCTS_RECEIVED', products }) } catch(error) { yield put({ type: 'PRODUCTS_REQUEST_FAILED', error }) } }
除了须要知道put表示dispatch action外,几乎不须要什么注释,实际状况就是你想的那样
登陆/注销 function* loginFlow() { while (true) { yield take('LOGIN') // ... perform the login logic yield take('LOGOUT') // ... perform the logout logic } }
pull action能保持关联action的处理顺序,而不须要额外外部状态控制。这样保证了LOGOUT老是在执行过LOGIN以后的某个时刻发生的,代码看起来至关漂亮
特定操做提示 // 在建立第3条todo的时候,给出提示消息 function* watchFirstThreeTodosCreation() { for (let i = 0; i < 3; i++) { const action = yield take('TODO_CREATED') } yield put({type: 'SHOW_CONGRATULATION'}) } // 接口访问异常重试 function* updateApi(data) { for(let i = 0; i < 5; i++) { try { const apiResponse = yield call(apiRequest, { data }); return apiResponse; } catch(err) { if(i < 4) { yield call(delay, 2000); } } } // attempts failed after 5 attempts throw new Error('API request failed'); }
即takeN的示例,这样就把本应该存在于reducer中的反作用提到了外面,保证了reducer的纯度
六.优缺点
优势:
易测试,提供了各类case的测试方案,包括mock task,分支覆盖等等
大而全的异步控制库,从异步流程控制到并发控制应有尽有
完备的错误捕获机制,阻塞型错误可try-catch,非阻塞型会通知所属Saga
优雅的流程控制,可读性/精炼程度不比async&await差多少,很容易描述并行操做
缺点:
体积略大,1700行,min版24KB,实际上并发控制等功能很难用到
依赖ES6 generator特性,可能须要polyfill
P.S.redux-saga也能够接入其它环境(不与Redux绑定),详细见Connecting Sagas to external Input/Output
参考资料
JavaScript Power Tools Part II: Composition Patterns In Redux-Saga
API Reference
Reference 6: A Saga on Sagas