每一个不曾起舞的日子,都是对生活的辜负
这期分享一下redux-saga
源码实现,即使以前完全没用过redux-saga
也没关系,我会先从如何使用讲起,然后再逐步替换redux-saga
提供的常用api
。
redux-saga自己用Generator实现了一套异步流程管理,所以如果不了解Generator的可以先了解一下
Redux-Saga
源码理解还是优点难度的
我们先从小demo
来了解redux-saga
的使用
const dispatch = useDispatch();
const userInfo = useSelector((state) => state.userInfo);
const fetchData = useCallback(() => {
dispatch({ type: "FETCH_USER_INFO" });
}, [dispatch]);
return (
<div className="App">
<h1>Hello Redux-Saga</h1>
<button onClick={fetchData}>click</button>
<h2>{JSON.stringify(userInfo, null, 2)}</h2>
</div>
);
这个例子很简单,点击按钮的时候,发送一个action
,改变store
中的userInfo
,将最新的userInfo
展示出来
redux-saga
提供了中间件,所以在创建store
的时候需要应用这个中间件
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import reducer from "./reducer";
import rootSaga from "./saga";
const sagaMiddleware = createSagaMiddleware();
let store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
export default store;
可以看到我们从redux-saga
中拿到createSagaMiddleware
,然后执行一遍拿到真正的中间件sagaMiddleware
,并通过applyMiddleware
将其合并到react-redux
中间件中,最后我们需要一个rootSaga
,这是一个Generator
,我们会先启动一次它,如:sagaMiddleware.run(rootSaga)
rootSaga
是处理action
的地方,我们在点击按钮的时候dispatch了一个acion: {type: "FETCH_USER_INFO"}
,那我们肯定要有地方来处理这个action
/* saga.js */
import { call, put, takeEvery } from "redux-saga/effects";
fetchUserInfoAPI() {
return new Promise((resolve) => {
const mockData = {
id: "1",
name: "yqx",
age: 18
};
resolve(mockData);
});
}
function* fetchUserInfo() {
try {
const user = yield call(fetchUserInfoAPI);
yield put({ type: "FETCH_USER_SUCCEEDED", payload: user });
} catch (e) {
yield put({ type: "FETCH_USER_FAILED", payload: e.message });
}
}
function* rootSaga() {
yield takeEvery("FETCH_USER_INFO", fetchUserInfo);
}
export default rootSaga;
我们看到rootSaga
就只做了一件事情takeEvery("FETCH_USER_INFO", fetchUserInfo)
,takeEvery
第一个参数正好等于我们之前dispatch
的acion
的type
,毫无疑问,这个rootSaga
的作用就是监听这个acion
,当acion
出现时,回调fetchUserInfo
开始执行,takeEvery
会在每次该acion出现都会执行一次回调,而这个回调中通过call(fetchUserInfoAPI)
发起了api
请求,同时结果赋值给user
,最后再通过put({ type: "FETCH_USER_SUCCEEDED", payload: user })
将结果发到reducer
中去出来,这里的put其实就是redux
中的dispatch
,所以它会触发reducer
的执行,reducer
如下:
/* reducer.js */
const initState = {
userInfo: null,
error: ""
};
function reducer(state = initState, action) {
switch (action.type) {
case "FETCH_USER_SUCCEEDED":
return { ...state, userInfo: action.payload };
case "FETCH_USER_FAILED":
return { ...state, error: action.payload };
default:
return state;
}
}
export default reducer;
当reducer
接收到action.type = FETCH_USER_SUCCEEDED
时,会将内容存在userInfo
中,这样我们主页面就会展示最新的userInfo
了,效果如下
像预期一样,点击按钮的时候,发送了action
,然后takeEvery
中的fetchUserInfo
开始执行,通过call
执行网络请求,把结果赋值给user
,再通过put
发送真正的action
,reducer
处理该action
,改变了store
中userInfo
的状态
下面是流程示例图:
整个流程还是比较简单的,redux-saga
使用起来也不难,刚刚我们已经使用了它常见的api
,这些api
也是我们今天手写的主要目标
- createSagaMiddleware:返回一个中间件实例
sagaMiddleware
- sagaMiddleware.run: 运行我们写的
rootSaga
的入口- takeEvery:控制并发流程,对
acion
进行处理- call:调用参数方法并返回结果
- put:真正发送
action
,用来和Reac-Redux
通讯
下面我们来开始逐步替换上述用到的api
首先从入口分析,我们从redux-saga
中引用了createSagaMiddleware
,这个函数的作用是执行它得到一个中间件,同时它上面有一个属性run
,它接收一个generator
,这样我们可以得到它的基本结构
// sagaMiddlewareFactory其实就是我们外面使用的createSagaMiddleware
function sagaMiddlewareFactory() {
// 返回的是一个Redux中间件,需要符合他的范式,我主页中对redux中间件源码分析有介绍中间件范式
const sagaMiddleware = function (store) {
return function (next) {
return function (action) {
// 内容先写个空的
let result = next(action);
return result;
}
}
}
// sagaMiddleware上还有个run方法,启动生成器的,暂时留空
sagaMiddleware.run = () => { }
return sagaMiddleware;
}
export default sagaMiddlewareFactory;
之前有说过,redux-saga
有一套自己的异步事件处理机制,这个机制有一个channel
中心,来保存注册的回调以及取出回调,我们先把这个实现了(暂时不用深入理解该段代码,后面我们用到了你就会清楚为什么这么写了)
export function multicastChannel() {
// 一个变量存储我们所有注册的事件和回调
const currentTakers = [];
// 保存事件和回调的函数
// Redux-Saga里面take接收回调cb和匹配方法matcher两个参数
// 事实上take到的事件名称也被封装到了matcher里面
function take(cb, matcher) {
cb['MATCH'] = matcher;
currentTakers.push(cb);
}
function put(input) {
const takers = currentTakers;
for (let i = 0, len = takers.length; i < len; i++) {
const taker = takers[i]
// 这里的'MATCH'是上面take塞进来的匹配方法
// 如果匹配上了就将回调拿出来执行
if (taker['MATCH'](input)) {
taker(input);
}
}
}
return {
take,
put
}
}
有了channel
后,我们的中间件sagaMiddleware
只干了一件事
const sagaMiddleware = function (store) {
return function (next) {
return function (action) {
let result = next(action);
// 就只加了这一行代码
channel.put(action);
return result;
}
}
}
所以很明显,当我们点击按钮发送一个action
时,最后会执行channel.put(action)
,通知redux-saga
有一个action
来了,麻烦你处理一下。channel.put
执行的事情也就是从currentTakers
中取出匹配的taker
并将action
作为参数传给taker
并执行
现在我们已经了解了发送action
背后的流程,他会从channel
中取出匹配到之前注册过的回调并执行,那么问题来了,channel
什么时候注册回调呢?
在sagaMiddleware.run(rootSaga)
中注册了回调,完整的createSagaMiddleware
代码应该是这样滴
/* redux-saga */
function sagaMiddlewareFactory() {
let boundRunSaga
const sagaMiddleware = function ({ dispatch, getState }) {
// 将getState, dispatch通过bind传给runSaga
boundRunSaga = runSaga.bind(null, {
channel,
dispatch,
getState,
})
return function (next) {
return function (action) {
let result = next(action);
channel.put(action);
return result;
}
}
}
sagaMiddleware.run = (...args) => boundRunSaga(...args)
return sagaMiddleware;
}
export default sagaMiddlewareFactory;
(...args) => boundRunSaga(...args)
这个的...args
其实就是我们传进去的rootSaga
,当我们执行sagaMiddleware.run(rootSaga)
实际执行了runSaga({channel, dispatch, getState}, rootSaga)
,runSaga
做的事情肯定有通过channel.take
来注册回调函数,但它把这个步骤分了好几层,我们一层层剥开
/* runSaga.js */
import proc from './proc';
export function runSaga(
{ channel, dispatch, getState },
saga,
...args
) {
// saga是一个Generator,运行后得到一个迭代器
const iterator = saga(...args);
const env = {
channel,
dispatch,
getState,
};
proc(env, iterator);
}
runSaga
会把我们传进去的rootSaga
执行一遍,得到迭代器iterator,再把迭代器传给proc。我们知道,生成器每次遇到yield
都会暂停执行,除非通过iterator.next()
,这样才会执行到下一个yield
。
proc
做的事情主要就是写一个next
,去执行iterator
,如果没结束,又在合适的时机继续next
,这样就达到了自主控制generator
的自动执行,不需要我们外部一遍一遍手动调用iterator.next()
export default function proc(env, iterator) {
// 调用next启动迭代器执行
next();
// 执行iterator
function next(arg, isErr) {
let result;
if (isErr) {
result = iterator.throw(arg);
} else {
result = iterator.next(arg);
}
// digestEffect(消化剩余的effect)是处理当前步骤返回值的函数,继续执行的next也由他来调用
if (!result.done) {
digestEffect(result.value, next)
}
}
}
到了这一步,我们发现iterator.next
执行了一次,而这里的iterator
是通过rootSaga()
得到的,所以rootSaga
会执行到takeEvery("FETCH_USER_INFO", fetchUserInfo)
,并把返回值存到了result.value
中,如果result.done
为false
,也就是说如果生成器还没执行完全执行完,就会执行digestEffect
,那么我们的rootSaga
第一次iterator.next()
得到的是{value: takeEvery("FETCH_USER_INFO", fetchUserInfo), done: false}
,第二次执行iterator.next()
得到的是{value: undefined, done: true}
。所以如果只是执行一次,done
为false
,会继续执行digestEffect
在继续看digestEffect
的实现之前,我们有一个点还没搞明白,那就是takeEvery("FETCH_USER_INFO", fetchUserInfo)
的返回值到底是什么?其实takeEvery
是一个由take
和fork
这两个低级effect
组成的高级effect
,大概长这么个样子(先不用深入理解,后面自然明白了为什么)
export function takeEvery(pattern, saga) {
function* takeEveryHelper() {
while (true) {
yield take(pattern);
yield fork(saga);
}
}
return fork(takeEveryHelper);
}
所以takeEvery
的返回值就等于fork
的返回值,而fork
和take
的返回值其实差不多,只是payload
不同(所有effect
的结构都是下面这样的,比如call,put,take,fork,takeEvery
)
// 这里代码简化了,只支持IO这种effect,官方源码中还支持promise和iterator
const makeEffect = (type, payload) => ({
IO: true,
type,
payload
})
export function take(pattern) {
return makeEffect('TAKE', { pattern })
}
export function fork(fn) {
return makeEffect('FORK', { fn })
}
fork
的payload
存了{ fn },take的payload存了{ pattern }
所以takeEvery("FETCH_USER_INFO", fetchUserInfo)
的返回值实际等于{IO: true, type: 'FORK', payload: { fn: takeEveryHelper }}
注意:这里
fork
存的fn
不是我们传给takeEvery
的fetchUserInfo
,而是takeEvery
内部自定义的生成器takeEveryHelper
现在我们再来看digestEffect
// effect = {IO: true, type: 'FORK', payload: { fn: fetchUserInfo }}
function digestEffect(effect, next) {
// 解决竞态问题
let effectSettled;
function currNext(res, isErr) {
// 如果已经运行过了,直接return
if (effectSettled) {
return
}
effectSettled = true;
next(res, isErr);
}
runEffect(effect, currNext);
}
这里调用了runEffect
,同时把next
封装了一下,解决竞态问题,我们可以忽略
function runEffect(effect, next) {
if (effect && effect.IO) {
const effectRunner = effectRunnerMap[effect.type]
effectRunner(env, effect.payload, next);
} else {
next();
}
}
runEffect
执行了effectRunner
,而effctRunner
是通过effectRunnerMap
找到effect
的type
所对应的内置函数,这里的effect
就是{IO: true, type: 'FORK', payload: { fn: takeEveryHelper }}
,所以effectRunnerMap[effect.type] = effectRunnerMap['FORK']
effectRunnerMap
结构如下
/*
env = { channel, dispatch, getState }
fn = takeEveryHelper
cb = currNext = next = rootSaga下一次yield
*/
function runForkEffect(env, { fn }, cb) {
// 运行fn得到一个迭代器
const taskIterator = fn();
// 直接将taskIterator给proc处理
proc(env, taskIterator);
// 直接调用cb,不需要等待proc的结果
cb();
}
const effectRunnerMap = {
'FORK': runForkEffect,
};
我们先分析cb()
这行代码
function runForkEffect(env, { fn }, cb) {
// const taskIterator = fn();
// proc(env, taskIterator);
cb(); // 先分析这行
}
这里的cb
执行其实就是调用了rootSaga
执行得到的iterator.next()
,在runSaga
中的proc
我们执行了rootSaga
的第一次iterator.next
,这时候相当于rootSaga
阻塞在yield takeEvery("FETCH_USER_INFO", fetchUserInfo)
,这一次cb()
使得rootSaga
自动执行下一步了,假设我们的rootSaga
是这样写的
function* rootSaga() {
yield takeEvery("FETCH_USER_INFO", fetchUserInfo);
yield takeEvery("FETCH_OTHER", fetchOther);
}
这一次cb()
相当于就执行了takeEvery("FETCH_OTHER", fetchOther)
,这就是为什么rootSaga
可以写多个yield takeEvery
,并且不需要我们手动去iterator.next
的原因
再来分析runForkEffect
剩下两行代码的作用,将takeEveryHelper
生成器执行得到迭代器taskIterator
,再将env
以及迭代器传给proc
并执行,proc
内部的代码我们都分析过了,这里再梳理一下proc
工作流程,方便我们理解
整个流程用一句话概括就是,proc
会将传进去的generator
先执行一遍,然后通过effect.type
找到对应的runXXXEffec
并执行(runXXXEffect
可以先不用理解,用到的时候再回来看)
这里索性我把后面要用到的call
、put
对应的effect
以及runXXXEffect
全都写出来,方便你们理解
const makeEffect = (type, payload) => ({
IO: true,
type,
payload
})
export function take(pattern) {
return makeEffect('TAKE', { pattern })
}
export function fork(fn) {
return makeEffect('FORK', { fn })
}
export function call(fn, ...args) {
return makeEffect('CALL', { fn, args })
}
export function put(action) {
return makeEffect('PUT', { action })
}
effect
所对应的runXXXEffect
如下
runForkEffect
function runForkEffect(env, { fn }, cb) {
const taskIterator = fn();
proc(env, taskIterator);
cb();
}
runTakeEffect
function runTakeEffect(env, { channel = env.channel, pattern }, cb) {
const matcher = input => input.type === pattern;
channel.take(cb, matcher);
}
runCallEffect
function runCallEffect(env, { fn, args }, cb) {
const result = fn.apply(null, args);
if (isPromise(result)) {
return result
.then(data => cb(data))
.catch(error => cb(error, true));
}
cb(result);
}
runPutEffect
function runPutEffect(env, { action }, cb) {
const result = env.dispatch(action);
cb(result);
}
我们刚刚已经知道了proc
处理流程,知道这点,就很容易理解takeEvery
的runForkEffect
中未分析的代码了,其实就是把takeEveryHelper
先执行一次,遇到了take(pattern)
,拿到take
所对应的runTakeEffect
执行一次
function* takeEveryHelper() {
while (true) {
yield take(pattern); // 执行到了这里
yield fork(saga);
}
}
runTakeEffect
的输入输出也简单,将当前take
所在的生成器生成的iterator.next
存到channel
中心。
至此,我们sagaMiddleware.run(rootSaga)
流程结束,我们如愿以偿的往channel
中心存了一个生成器回调
当点击按钮的时候,我们执行的是dispatch
,会走到我们之前写的saga
中间件
const sagaMiddleware = function ({ dispatch, getState }) {
// 省略其他内容
return function (next) {
return function (action) {
let result = next(action);
channel.put(action); // dispatch的时候执行了这里
return result;
}
}
}
// 省略其他内容
dispatch(action)
相当于channel.put(action)
,而channel.put
会从channel
中心注册的回调数组中取一个匹配到的函数并执行,而这里我们取到的其实就是我们之前take(pattern)
注册的函数,该函数包含了takeEveryHelper().next()
这段代码,而takeEveryHelper
在最开始已经执行过一次了,所以这里我们再运行的话,就执行了fork(saga)
,而这个saga
正是我们之前传的rootSaga
。而fork
执行又会返回一个effect
,这个effect
又会到proc
中,proc
取到对应的runForkEffect
并执行,runForkEffect
会将传进来的fn
也就是rootSaga
执行一次,同时将fork
所在的生成器takeEveryHelper
又执行一次next
,这样,又执行了take(pattern)
,往channel
中心又注册了一个回调
刚刚我们聊到rootSaga
会被执行一次,那么代码就运行到了
function* fetchUserInfo() {
try {
const user = yield call(fetchUserInfoAPI); // 运行到了这里
yield put({ type: "FETCH_USER_SUCCEEDED", payload: user });
} catch (e) {
yield put({ type: "FETCH_USER_FAILED", payload: e.message });
}
}
而call
函数所对应的runCallEffect
会将传进来的函数执行一遍,并将结果cb(res)
出去,这里的cb
就是call
所在生成器fetchUserInfo().next
,既然call
执行了cb(res)
,那么fetchUserInfo
会继续往下执行,来到了put
函数,而put
函数所对应的runPutEffect
会将传进来的action
,dispatch
出去,这个dispatch
就是redux
的dispatch
,到这里,其实就已经改变了redux
中store
存储的userInfo
了,主页面发生更新,put
函数同时还会cb(result)
,这里的cb
跟call
的cb
是一样的,这也是为什么我们写的fetchUserInfo
生成器会自己一步一步执行
到这里Redux-Saga
常用api
已经被我们撕完了,我们实现了fork, take, takeEvery, put, call, createSagaMiddleware,sagaMiddleware.run
。不难发现fork,put,call
都会在最后执行一次cb()
,自动将当前的generator
执行下一步,而take
最终目的是存注册函数,该函数实际执行的就是当前generator
,而这个回调需要我们手动dispatch
才会触发。