![e683064e-b233-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/e683064e-b233-eb11-8da9-e4434bdf6706.png)
redux-saga中文文档:https://redux-saga-in-chinese.js.org/docs/introduction/index.html
关于redux-saga一直想总结一篇全面解析的文档,网上有很多帖子讲redux-saga,不过对于初学者来说理解起来有点抽象,对于框架dva,umi也是简单的使用。希望读者在看过本文之后,能够知道dva怎样集成redux-saga,redux-saga怎样实现异步和action监听,以及umi又是如何设计的。之前总结过redux设计思想和源码分析的文档,放到了csdn上,分别从源码,函数式编程,中间件,设计模式等方面讲解了redux。本文将来说说redux一个重要的中间件redux-saga。
之所以说redux-saga重要,不是因为其用途,redux-saga的功能也可以通过其他方式实现,但是redux-saga在内部使用了generator作为核心语法,并且有两个开源框架dva,umi与之相联系,这就决定了redux-saga的地位。
近期一直研究node IOC框架,总算抽出时间,补上这篇文档,本文将从redux-saga实现原理,redux-saga的设计思想,redux-saga和dva的关系,umi和dva的关系,dva和umi框架现状五个方面切入,之后引出对前端设计模式的思考。欢迎各位大佬指正不足。
一,redux-saga实现原理
1,用法:看一个框架和库的原理,首先要找到其入口,首先redux-saga的用法:
import React from 'react';
import App from './app.js';
import { createStore, applyMiddleware, compose } from 'redux'
import reducer from './redux/reducer'
import createSagaMiddleware from 'redux-saga'
import rootSaga from './redux/saga/saga'
import { Provider } from 'react-redux'
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
compose(
applyMiddleware(sagaMiddleware),
window.devToolsExtension ? window.devToolsExtension() : compose
)
)
sagaMiddleware.run(rootSaga);
redux-saga和其他redux中间件的使用是有区别的,除了正常的引入中间件还需要两个步骤:
- const sagaMiddleware =createSagaMiddleware();
- 在通过applyMiddleware引入后,执行sagaMiddleware.run(rootSaga);
这也就找到了入口
runSaga接收的参数,一般用法:
import { fork } from 'redux-saga/effects'
...
export default function* rootSaga() {
yield fork(setStepStatus)
}
接收了一个generator函数,里面的异步是fork或者其他effects
2,分析sagaMiddleware和sagaMiddleware.run
首先对effects进行分析,常用的effects有call, put, take, takeEvery, fork等,部分源码:
export {
take,
takeMaybe,
put,
putResolve,
all,
race,
call,
apply,
cps,
fork,
spawn,
join,
cancel,
select,
actionChannel,
cancelled,
flush,
getContext,
setContext,
delay,
} from './internal/io'
export { debounce, retry, takeEvery, takeLatest, takeLeading, throttle }
from './internal/io-helpers'
挑选一个effect进行分析,比如take源码(验证已经去掉):
export function take(patternOrChannel = '*', multicastPattern) {
if (is.pattern(patternOrChannel)) {
return makeEffect(effectTypes.TAKE, { pattern: patternOrChannel })
}
if (is.multicast(patternOrChannel) && is.notUndef(multicastPattern) && is.pattern(multicastPattern)) {
return makeEffect(effectTypes.TAKE, { channel: patternOrChannel, pattern: multicastPattern })
}
if (is.channel(patternOrChannel)) {
return makeEffect(effectTypes.TAKE, { channel: patternOrChannel })
}
}
可以看到take根据patternOrChannel共有三种情况,最终调用了makeEffect()方法,该方法接收两个参数。对于take的三种情况先不讨论,先看下makeEffect.
源码:
const makeEffect = (type, payload) => ({
[IO]: true, //这里的IO是一个常量,值为@@redux-saga/IO
combinator: false, //是否进行组合
type,
payload,
})
第一个参数是一个常量,代表当前effect的类型,这里都是'TAKE'
payload是传入的pattern,channel组合而成的对象。
于是可以得出,take返回的是一个带有type的对象:
{
'@@redux-saga/IO': true, //这里的IO是一个常量,值为@@redux-saga/IO
combinator: false, //是否进行组合
type:"TAKE"
payload:{
channel: patternOrChannel,
pattern: multicastPattern
}
}
其余的effect返回的对象与take类似,只是payload的内容会有差别
返回的对象有了,那么payload到底是什么呢,继续=>
对于各个effect接收的参数做下分类:
- 第一类接收函数的effect:
call, cps, fork,spawn,apply
最终payload调用:getFnCallDescriptor(fnDescriptor, args)
源码:
function getFnCallDescriptor(fnDescriptor, args) {
let context = null
let fn
if (is.func(fnDescriptor)) {
fn = fnDescriptor
} else {
if (is.array(fnDescriptor)) {
;[context, fn] = fnDescriptor
} else {
;({ context, fn } = fnDescriptor)
}
if (context && is.string(fn) && is.func(context[fn])) {
fn = context[fn]
}
}
return { context, fn, args }
}
- 第二类接收effect的effect
all, race,这里effect是对其他effects的组合
- 第三类接收action, channel, pattern等的effect
put, take等(这里主要介绍这两种)
官方对于take的介绍是告诉middleware监听一个特定的action,那么这种监听到底是如何实现的呢。这也是redux-saga关于流程控制的核心设计思想。
使用实例:
while (true) {
let request = yield take(actionTypes.GET_MATERIALS_RESOURCE);
...
}
take接收的是一个action类型,也就是我们在redux的action中定义的type,也可以是,如果是*,代表匹配所有的actionType
至于为什么要放在while(true)里面,下面会介绍到。
take接收到的参数有三种类型,这里以最简单的类型介绍,也就是接收的是字符串:
比如传入:"get_data",此时take返回的就是我们经常写的action对象:
{
'@@redux-saga/IO': true, //这里的IO是一个常量,值为@@redux-saga/IO
combinator: false, //是否进行组合
type:"TAKE"
payload:{
pattern: "get_data"
}
}
实例:假设现在有一个saga对象,返回内容如下:
export default function* setLoadUrl() {
while (true) {
let request = yield take(actionTypes.GET_MATERIALS_RESOURCE);
}
}
接下来rootSaga引入setLoadUrl.js:
import { fork } from 'redux-saga/effects'
import setLoadUrl from './setLoadUrl'
export default function* rootSaga() {
yield fork(setLoadUrl)
}
从上面的分析可以知道,fork接收的是函数,最终生成的函数对象是:
{
'@@redux-saga/IO': true, //这里的IO是一个常量,值为@@redux-saga/IO
combinator: false, //是否进行组合
type:"FORK"
payload:{
context:null,//这里没有传入context,如果传入fork,格式是[context,fn]
fn:setLoadUrl,
args:undefined//这里没有传入参数
}
}
接下来就要回到redux-saga的入口了,(上面的过程主要分析了rootSaga到底是什么)
- const sagaMiddleware =createSagaMiddleware();
- 在通过applyMiddleware引入后,执行sagaMiddleware.run(rootSaga);
部分源码:
export default function sagaMiddlewareFactory({ context = {}, channel = stdChannel(), sagaMonitor, ...options } = {}) {
let boundRunSaga
function sagaMiddleware({ getState, dispatch }) {
boundRunSaga = runSaga.bind(null, {
...options,
context,
channel,
dispatch,
getState,
sagaMonitor,
})
return next => action => {
if (sagaMonitor && sagaMonitor.actionDispatched) {
sagaMonitor.actionDispatched(action)
}
const result = next(action) // hit reducers
channel.put(action)
return result
}
}
sagaMiddleware.run = (...args) => {
return boundRunSaga(...args)
}
...
}
createSagaMiddleware返回的sagaMiddleware函数,一个基本的redux中间件结构。
agaMiddleware.run(rootSaga)指向了boundRunSaga.
boundRunSaga是runSaga指定this指向后的函数,看下runSaga
部分源码:
export function runSaga(
{ channel = stdChannel(), dispatch, getState, context = {}, sagaMonitor, effectMiddlewares, onError = logError },
saga,
...args
) {
const iterator = saga(...args)
const effectId = nextSagaId()
if (sagaMonitor) {
// monitors are expected to have a certain interface, let's fill-in any missing ones
sagaMonitor.rootSagaStarted = sagaMonitor.rootSagaStarted || noop
sagaMonitor.effectTriggered = sagaMonitor.effectTriggered || noop
sagaMonitor.effectResolved = sagaMonitor.effectResolved || noop
sagaMonitor.effectRejected = sagaMonitor.effectRejected || noop
sagaMonitor.effectCancelled = sagaMonitor.effectCancelled || noop
sagaMonitor.actionDispatched = sagaMonitor.actionDispatched || noop
sagaMonitor.rootSagaStarted({ effectId, saga, args })
}
if (process.env.NODE_ENV !== 'production') {
if (is.notUndef(dispatch)) {
check(dispatch, is.func, 'dispatch must be a function')
}
if (is.notUndef(getState)) {
check(getState, is.func, 'getState must be a function')
}
if (is.notUndef(effectMiddlewares)) {
const MIDDLEWARE_TYPE_ERROR = 'effectMiddlewares must be an array of functions'
check(effectMiddlewares, is.array, MIDDLEWARE_TYPE_ERROR)
effectMiddlewares.forEach(effectMiddleware => check(effectMiddleware, is.func, MIDDLEWARE_TYPE_ERROR))
}
check(onError, is.func, 'onError passed to the redux-saga is not a function!')
}
let finalizeRunEffect
if (effectMiddlewares) {
const middleware = compose(...effectMiddlewares)
finalizeRunEffect = runEffect => {
return (effect, effectId, currCb) => {
const plainRunEffect = eff => runEffect(eff, effectId, currCb)
return middleware(plainRunEffect)(effect)
}
}
} else {
finalizeRunEffect = identity
}
const env = {
channel,
dispatch: wrapSagaDispatch(dispatch),
getState,
sagaMonitor,
onError,
finalizeRunEffect,
}
return immediately(() => {
const task = proc(env, iterator, context, effectId, getMetaInfo(saga), /* isRoot */ true, undefined)
if (sagaMonitor) {
sagaMonitor.effectResolved(effectId, task)
}
return task
})
}
乍一看一头雾水,这是什么,许多变量的含义还不清楚,先不care,继续分析。
runSaga最终返回的是immediately(callback),来看看这个是什么,对应源码:scheduler
const queue = []
let semaphore = 0 //英 [ˈseməfɔː(r)] 这里做信号讲
function exec(task) {
try {
suspend()
task()
} finally {
release()
}
}
export function asap(task) { //尽快执行任务
queue.push(task)
if (!semaphore) {
suspend()
flush()
}
}
export function immediately(task) { //立即执行任务
try {
suspend()
return task()
} finally {
flush()
}
}
function suspend() { semaphore++ } //暂停信号,也就是暂停
function release() { semaphore-- } //释放信号,也就是开始
function flush() { //执行queue中的所有tasks
release()
let task
while (!semaphore && (task = queue.shift()) !== undefined) {
exec(task)
}
}
有没有被惊到,scheduler源码如此简洁,里面的方法也不复杂,那么到底是做什么的呢。
暴露出两个方法,asap(task), immediately(task),分别是尽快执行,立即执行。
scheduler维护了queue队列,用来存储任务,非立即执行时会把任务方法queue中,立即执行时会暂停正在执行的任务,执行完当前任务后继续执行之前的任务队列,执行每一个任务时,会把信号置为暂停,执行完之后,释放信号,执行下一个任务。
scheduler充当了任务调度的角色。
runSaga产生了一个什么样的任务呢,继续分析:
() => {
const task = proc(env, iterator, context, effectId, getMetaInfo(saga),true, undefined)
return task
}
考虑最简单的情况(未传入sagaMonitor)
task调用了proc, getMetaInfo
getMetaInfo(saga)的saga就是我们传入的rootSaga,getMetaInfo是获取meta;
proc的第二个参数iterator就是通过rootSaga生成的遍历器对象,自带有方法next(),
effectId是一个自增的数值,初始值为0,每次执行
源码:
function runSaga(
{ channel = stdChannel(), dispatch, getState, context = {}, sagaMonitor, effectMiddlewares, onError = logError },
saga,
...args
) {
if (process.env.NODE_ENV !== 'production') {
check(saga, is.func, NON_GENERATOR_ERR)
}
const iterator = saga(...args)//saga就是rootSaga
proc内部有next()方法,类似于执行generator的next方法,返回task对象:
const task = newTask(env, mainTask, parentContext, parentEffectId, meta, isRoot, cont)
const executingContext = {
task,
digestEffect,
}
next()
// then return the task descriptor to the caller
return task
proc最终会调用effectRunnerMap类,根据effect的类型,执行对应的effectRunner
流程图:
最终runSaga和中间件里面的channel.put()通过channel联系了起来,也就是说用户发起dispatch后,通过channel触发runSaga(rootSaga)放入channel的effect。
那么channel是什么样的数据结构呢。
现在看下channel的数据结构:
channel文件包含了两个基本结构:channel和multicastChannel,上面由于没有传入channe,channel会调用默认设置stdChannel,stdChannel调用multicastChannel。
multicastChannel源码:
function multicastChannel() {
let closed = false
let currentTakers = []
let nextTakers = currentTakers
function checkForbiddenStates() {
const ensureCanMutateNextTakers = () => {
if (nextTakers !== currentTakers) {
return
}
nextTakers = currentTakers.slice()
}
const close = () => {
closed = true
const takers = (currentTakers = nextTakers)
nextTakers = []
takers.forEach(taker => {
taker(END)
})
}
return {
[MULTICAST]: true,
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]
if (taker[MATCH](input)) {
taker.cancel()
taker(input)
}
}
},
take(cb, matcher = matchers.wildcard) {
if (closed) {
cb(END)
return
}
cb[MATCH] = matcher
ensureCanMutateNextTakers()
nextTakers.push(cb)
cb.cancel = once(() => {
ensureCanMutateNextTakers()
remove(nextTakers, cb)
})
},
close,
}
}
multicastChannel返回了一个对象,
{
[MULTICAST]: true,
put(input) {},
take(cb, matcher = matchers.wildcard) {},
close:close
}
从源码看,multicastChannel内部维护了一个nextTakers副作用队列,而currentTakers只是在执行put之后对nextTakers的存储,在对nextTakers进行添加和移除操作时,会比较nextTakers和currentTakers是否相等,如果相等会把nextTakers重置为一个新的对象(开辟新内存),用来确保每次的nextTakers都是更改的,也就是操作的是一个新对象,和currentTakes不是同一个引用。
effectRunner在调用时传入了effect.payload,也就是上面提到的makeEffect方法返回的对象。
channel.take接收两个参数,
第一个是回调函数cb,其实是改造过的next()函数,
第二个是类型判断函数,matcher(pattern)返回的是接收input的类型判断函数,根据当前effect对应的type,从channel的nextTakers取出对应的effect,并执行。
export const string = pattern => input => input.type === String(pattern)
到这里redux-saga的监听原理已经分析完毕,上面的分析比较分散,下面进行一个总结。
核心原理:
在执行runSaga时,会把generator中生成的effect加入channel,在中间件调用时,执行put(action),判断action.type和channel中nextTakers中每一项taker的pattern是否相等,执行对应的taker,执行过后将该taker删除,这也就是说在执行过后需要继续加入taker,这也就是在进行监听时,为什么要把take写在while(true)里面。
下面是take的原理图:
![e883064e-b233-eb11-8da9-e4434bdf6706.png](http://p04.5ceimg.com/content/e883064e-b233-eb11-8da9-e4434bdf6706.png)
二:redux-saga的设计思想
归根结底还是订阅发布者模式,只是订阅的模式是通过generator的深度自动执行实现的,redux-saga实现了一个自动执行generator的函数。
redux-saga中还有许多其他辅助的逻辑,比如scheduler实现的任务调度等,这里不再深入讨论。
三:redux-saga和dva的关系
完善中