背景
使用umi一套新开项目,对其中的redux-saga比较有兴趣,对redux-thunk不熟悉,网络上偏向saga的更多,阅读该项目源码,了解内部原理,对优化使用有很大作用,默认读者了解了redux相关知识.
为什么需要使用redux-saga
redux是同步数据解决方案,至于前端常用异步场景的数据处理留给用户自己做这方面的脏活累活
假设用async await处理异步场景
function fetchData(userId) {
// 返回一个promise, 内容是数据
}
function fetchUser() {
// 返回一个promise, 内容是用户信息
}
class Component {
componentDidMount() {
// 操作1
const { userInfo: { userId } } = await fetchUser();
store.dispatch({type: 'UPDATE_USER_ID', payload: userId});
// 操作2
const { data } = await fetchData(userId);
store.dispatch({type: 'UPDATE_DATA', payload: data});
}
}
那问题来了,如果其他组件也需要复用这个获取数据的逻辑该怎么办呢,稍好的方案是建立一个类来实现复用,
class DataHandler {
static fetchData() {
// 操作1
const { userInfo: { userId } } = await fetchUser();
store.dispatch({type: 'UPDATE_USER_ID', payload: userId});
// 操作2
const { data } = await fetchData(userId);
store.dispatch({type: 'UPDATE_DATA', payload: data});
}
}
class ComponentA {
componentDidMount() {
DataHandler.fetchData();
}
}
class ComponentB {
componentDidMount() {
DataHandler.fetchData();
}
}
如果在操作1之后需要加各种定制化逻辑,就会变得越来越臃肿,所以我们需要一个专职处理异步逻辑的redux插件,负责统一,更细粒度的管理异步.
使用redux-saga之后
function* rootSaga() {
yield fork(genA)
yield fork(genB)
yield fork(genC)
yield fork(genD)
}
function* genA() {
yield take('handler1')
// handler1
}
function* genB() {
yield take('handler2')
// handler2
}
function* genC() {
yield take({ type: 'fetchuser-1' })
yield call(fetchUser)
yield put({ type: 'handle1'})
}
function* genD() {
yield take('fetchuser-2')
yield call(fetchUser)
yield put({ type: 'handle2'})
}
且易于测试
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 })"
)
总结
async await实现也是预先写好代码,某块异步场景怎么怎么执行代码,saga也是预先编排好各个逻辑块,它们之间的差异是,谁能实现的更优雅,更方便,更有效
优点
- 将异步与reducer区分开了,更加优雅,适合大量APi请求,而且每个请求之间存在复杂的依赖关系
- 将异步逻辑统一管理,粒度控制精细
- 易测试 缺点
- 学习曲线比较陡
- 库体积较大
基础知识,生成器函数
生成器函数格式一般为
function* gen() {
yield 'xxx'
yield 888
yield { name: 'aike' }
yield new Promise(resolve => {
resolve('1')
})
yield () => {}
}
执行
const iterator = gen()
iterator.next() // { done: false, value: 'xxx'}
iterator.next() // { done: false, value: '888'}
iterator.next() // { done: false, value: { name: 'aike' }}
iterator.next() // { done: false, value: Promise}
iterator.next() // { done: false, value: func}
iterator.next() // { done: true} done=true,表示当前迭代器执行完毕
核心原理一句话
redux-saga是redux的中间件,通过迭代器函数进入待机转态,通过dispatch的action匹配预置好的处理程序callback来做区别处理,也就是捕捉未来的action且做出响应
effect
不好翻译,类似于副作用,一个因子单位,可以是任何形式,不过在这边规定了几种格式,带有扩展功能,例如put effect一般是
{
[IO]: true, // 个人感觉只是各种effect的统一标识 其实可以通过['PUT', 'TAKE'...].includes(effect.type)来代替
combinator: false,
type: 'PUT',
payload, // {chanel, action} action={type, payload}
}
[io]标志
出现的地方有两个,定义以及生成的地方,在生成各种内置effect的时候都会设置[IO] = true
const makeEffect = (type, payload) => ({
[IO]: true,
combinator: false,
type,
payload,
})
使用的地方
if (is.promise(effect)) {
resolvePromise(effect, currCb)
} else if (is.iterator(effect)) {
proc(env, effect, task.context, effectId, meta, /* isRoot */ false, currCb)
// 判断IO 其实可以['PUT', 'TAKE', 'FORK'...].includes(effect.type) 不过这样也好 统一在makeEffect中设置
} else if (effect && effect[IO]) {
const effectRunner = effectRunnerMap[effect.type]
effectRunner(env, effect.payload, currCb, executingContext)
} else {
currCb(effect)
}
[SAGA_ACTION]标志
标记挂载位置wrapSagaDispatch
export const wrapSagaDispatch = dispatch => action => {
// 包装该dispatch, 来自env env.dispatch
return dispatch(Object.defineProperty(action, SAGA_ACTION, { value: true }))
}
标记是否有saga派发的action,比如put('xxxxx') 触发位置举例,如果属于saga,则不需要进行任务调度,asap(),如果是原生action,需要进行任务调度
function runPutEffect(env, { channel, action, resolve }, cb) {
/**
Schedule the put in case another saga is holding a lock.
The put will be executed atomically. ie nested puts will execute after
this put has terminated.
**/
asap(() => {
let result
try {
// env.dispatch 触发saga action
result = (channel ? channel.put : env.dispatch)(action)
} catch (error) {
cb(error, true)
return
}
if (resolve && is.promise(result)) {
resolvePromise(result, cb)
} else {
cb(result)
}
})
// Put effects are non cancellables
}
作用在于区分原生redux action和saga action,因为saga action还有其他匹配任务 比如响应saga的take操作
saga中间件接入redux部分
// store.js
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from '../../../packages/redux-saga/dist/redux-saga.umd.js'
const sagaMiddleware = createSagaMiddleware()
const store = createStore(reducer, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(rootSaga) // 启动saga,分解生成器函数 构建saga树,预置处理程序
redux经典中间件方法构造
修改的重点是channel.put(action)
,往主任务中的channel中推入action,尝试匹配在saga队列中注册的action,如果匹配则执行预置callback,比如take('target-action')的用法
// redux中间件经典样例
return next => action => {
if (sagaMonitor && sagaMonitor.actionDispatched) {
sagaMonitor.actionDispatched(action)
}
const result = next(action) // hit reducers
channel.put(action) // 会在redux中透传action,也会触发redux-saga中的action处理
return result
}
saga流程处理模型
put effect
经过saga包装的action分发,会触发普通action,也会触发由take预先注册好的action对应的handler
// saga
import { put } from 'redux-saga/effects'
// 根节点saga
export default function* rootSaga () {
yield put({ type: 'INCREMENT' })
}
经过sagaMiddleware改写
此时拿到的根saga rootSaga在runSaga中执行
const iterator = saga(...args) // 生成根节点生成器
// 根task生成
const task = proc(env, iterator, context, effectId, getMetaInfo(saga), /* isRoot */ true, undefined)
proc执行
// 生成主任务 每个proc都有主任务,主任务之间又有子父级关系
const mainTask = { meta, cancel: cancelMain, status: RUNNING }
const task = newTask(env, mainTask, parentContext, parentEffectId, meta, isRoot, cont)
上下文处理
// 用于新开任务作为继承关系
const executingContext = {
task,
digestEffect,
}
执行next() next()
,执行根节点iterator.next()
,
获取put effect
执行result = iterator.next(arg)
,此时获取到的effect为result.value
// effect
effect = {
@@redux-saga/IO: true,
combinator: false,
payload: {
context: null,
action: {
type: 'INCREMENT'
@@redux-saga/SAGA_ACTION: true
},
type: 'PUT'
}
}
// result
result = {
done: false,
value: effect
}
done=false,所以执行digestEffect 分解effect
定义currCb,currCb的执行顺带触发cb执行,也就是task的next
finalRunEffect = runEffect => runEffect // 这个可以当做没做啥
function currCb(res, isErr) {
if (effectSettled) {
return
}
effectSettled = true // 先打标记effect的处理为完成
cb.cancel = noop // defensive measure
if (env.sagaMonitor) {
if (isErr) {
env.sagaMonitor.effectRejected(effectId, res)
} else {
env.sagaMonitor.effectResolved(effectId, res)
}
}
if (isErr) {
sagaError.setCrashedEffect(effect)
}
cb(res, isErr) // 响应task的next
}
执行runEffect
当前put的effect不是迭代器,匹配的分解器是runPutEffect
function runPutEffect(env, { channel, action, resolve }, cb) {
asap(() => {
let result
try {
// 没有channel,调用的env.dispatch,触发redux的dispatch效果
// 所以 可以说put()和redux的dispatch效果类似
result = (channel ? channel.put : env.dispatch)(action)
} catch (error) {
cb(error, true)
return
}
if (resolve && is.promise(result)) {
resolvePromise(result, cb)
} else {
cb(result)
}
})
}
env.dispatch之后,经过saga中间件
channel收集action,尝试匹配take注册的callback,如果有 调出执行
return next => action => {
if (sagaMonitor && sagaMonitor.actionDispatched) {
sagaMonitor.actionDispatched(action)
}
const result = next(action) // hit reducers
channel.put(action) // channel收集action,
return result
}
// 尝试匹配
put(input) {
if (closed) {
return
}
if (isEnd(input)) {
close()
return
}
const takers = (currentTakers = nextTakers)
for (let i = 0, len = takers.length; i < len; i++) {
const taker = takers[i]
// 根据正则匹配到的callback接力处理
if (taker[MATCH](input)) {
taker.cancel()
taker(input)
}
}
},
用一张图来总结上述流程
take effect
注册action以及对应的handler,在saga发起put effect时,响应击中的action进行逻辑处理,也就是常说的捕捉未来的action
// saga
import { put } from 'redux-saga/effects'
// 根节点saga
export default function* rootSaga () {
yield take({ type: 'INCREMENT' })
// handler
}
function runTakeEffect(env, { channel = env.channel, pattern, maybe }, cb) {
// 设置callback
const takeCb = input => {
if (input instanceof Error) {
cb(input, true)
return
}
if (isEnd(input) && !maybe) {
cb(TERMINATE)
return
}
cb(input)
}
try {
channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null)
} catch (err) {
cb(err, true)
return
}
cb.cancel = takeCb.cancel
}
// channel队列中push进已配置好的taker 等待put触发
take(cb, matcher = matchers.wildcard) {
if (process.env.NODE_ENV !== 'production') {
checkForbiddenStates()
}
if (closed) {
cb(END)
return
}
cb[MATCH] = matcher
ensureCanMutateNextTakers()
nextTakers.push(cb)
cb.cancel = once(() => {
ensureCanMutateNextTakers()
remove(nextTakers, cb)
})
},
流程总结如下图
takeEvery effect
前面通过我们分析,发现可以通过take来注册捕捉action,但是它只是一次性消费,然后不再响应相同的action,如果我们有一类需求,需要不间断响应某个action呢,比如按钮点击,那就是takeEvery工具方法
他的本质是生产一个take effect,在消费它的同时继续生产下一个take effect,take流程我们比较熟悉
// saga
import { put } from 'redux-saga/effects'
// 根节点saga
export default function* rootSaga () {
yield takeEvery({ type: 'INCREMENT' })
// handler
}
解析
// 1.执行takeEvery,我们获取到了一个迭代器,这个迭代器有两个状态,q1,q2,进入流程proc后,
// 2. 执行迭代器next获取到初始状态q1,effect类型为take,执行注册流程,阻塞等待put触发
// 3. 假设put触发,执行主进程的procNext,内部包裹该迭代器的next,
procNext() {
iterator.next()
}
// 4. 获取到的返回值快照如下
{
done: undefined, !== true
value: {
type: 'FORK',
[io]: true,
...
}
}
// 5.继续执行fork流程,开启新的进程proc
// 6.注意看,fork开启的子进程是以原有的迭代器iterator作为参数传递继续作为迭代器使用,执行后获取到的是下一次的take effect,
// 7. 循环往复 注册 => 消费 => 注册 => ...
用一张图来描述takeEvery
saga提供的effect,以及工具方法比较多,在这里就不一一解析了
schduler 任务调度
上面有说到,take注册,put触发,那如果put执行在take之前呢,将会发生action丢失的情况,这是不允许的,任务调度就是解决它们的先后问题
function* rootSaga() {
yield fork(genA) // LINE-1
yield fork(genB) // LINE-2
}
function* genA() {
yield put({ type: 'A' })
yield take('B')
}
function* genB() {
yield take('A')
yield put({ type: 'B' })
}
如果没有任务调度程序,流程将会如下图所示
我们发现,中间丢失了一个put-A,调度程序的目的就是解决这个问题
schduler只暴露了两个方法,一个是asap(尽快执行),一个是immediately(立即执行),经过观察,我们发现,在主进程和子进程启用的时候包裹了一层immediately,也就是进程开启的时候都把锁semaphore+1,也就是semaphore > 0,后续所有用了asap包裹的任务都不会在进程结束之前执行,恰巧,所有有关put的effect都包裹了一层asap,也就是说,put类型分发action都是在主进程执行完后(和take effect阻塞意义不同,它是callback阻塞,但是proc内部阻塞,但是proc本身是会作为代码块执行完成,且take之后的put操作本身也不会丢失)才会执行,所以都会在完成各种注册后才开始分发action. 为什么要主进程proc和子进程proc都加上immediately呢,进程只能覆盖到自己线上的注册和分发,假设子进程的子进程有个take阻塞,阻塞的后续又有fork呢,触发take后又需要保证接下来的注册和分发的顺序问题,所以都加上。 用了schduler之后的处理流程如下图
channel
在上面篇幅中我们并没有提到channel,其实它就是我们effect的生产,消费的中转中心
每个chanel管理一个对应的buffer,一个对象数组,讲代码简化后得到下面的代码块
// 我们可以简单的认为 channel维护的effect如下
buffer = takers = [
{
pattern: '', // action string
cb: func, // 回调函数
}
]
channel.take // 注册effect buffer.push(target) takers.push(target)
// 消费effect 在消费之前可进行正则匹配,校验action找出match的effect继而消费 也就是channel的升级版multicastChannel在做的事情
// buffer.shift() takers.shift()
channel.put
actionChannel解决成队列的action消费
上面我们解决了多次监听且响应一个action的问题,但是不能解决action成队列消费的问题,saga提供了一个工具方法actionChannel解决这个问题。
function* rootSaga() {
// 长度为5的REQUEST的队列处理
const requestChan = yield actionChannel('REQUEST', buffers.sliding(5))
...
}
// 在消费当前action的同时,分发下一个action 移除了边界判断代码
function runChannelEffect(env, { pattern, buffer }, cb) {
const chan = channel(buffer)
const match = matcher(pattern)
const taker = action => {
if (!isEnd(action)) {
// 消费
env.channel.take(taker, match)
}
// 继续分发
chan.put(action)
}
env.channel.take(taker, match)
cb(chan)
}