每一个不曾起舞的日子,都是对生活的辜负
这期分享一下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才会触发。