这篇文章是为了取探索saga的异步工作流和effect的处理,探究他的思路和工作原理。
从头开始撸
saga是一个redux的中间件,而redux提供了一个applyMiddleware(...middlewares)
函数,
middleware它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,可以接收store的dispatch和getState方法,我们来做一个简单的测试去理解redux的中间件。
function logger({ getState }:any) {
return (next:Dispatch) => (action:any) => {
console.log('will dispatch', action)
// 调用 middleware 链中下一个 middleware 的 dispatch。
let returnValue = next(action)
console.log('state after dispatch', getState())
// 一般会是 action 本身,除非
// 后面的 middleware 修改了它。
return returnValue
}
}
创建我们的redux仓库,用于在react-redux的provider上承载store
let store = createStore(
todos,// reducers
[ 'Use Redux' ],//state
applyMiddleware(logger)// 应用中间件
)
export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer<any> {
// 返回函数。
return (createStore: StoreCreator) => <S, A extends AnyAction>(
reducer: Reducer<S, A>,
...args: any[]
) => {
// 这里就没有enhancer了,也就不会进去enhancer的处理逻辑。就会创建出reducer
const store = createStore(reducer, ...args)
let dispatch: Dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
// 遍历applyMiddleware的参数,为每个middleware装入getState和dispatch
const chain = middlewares.map(middleware => middleware(middlewareAPI))
//compose是用于组合我们的reducer,会从最后一个执行到第一个,顺序执行,
// 本例来说 这个dispatch就是我们定义的logger返回的函数,函数执行得到的就是dispatch即 action=>{}这样的函数,其实就是dispatch,在他的前后多了两个console而已。当我们dispatch的时候,我们就会走到这里来,按中间件从后往前的顺序去
// (next:Dispatch) => (action:any) => {
//console.log('will dispatch', action)
// 调用 middleware 链中下一个 middleware 的 dispatch。
//let returnValue = next(action)
//console.log('state after dispatch', getState())
// 一般会是 action 本身,除非
// 后面的 middleware 修改了它。
return returnValue
}
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
export default function compose(...funcs: Function[]) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return <T>(arg: T) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args: any) => a(b(...args)))
}
export default function createStore<
S,
A extends Action,
Ext = {},
StateExt = never
>(
reducer: Reducer<S, A>,
preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function.'
)
}
// 判断state是否函数,是函数就会将这个赋值给enhancer,认为是中间件,也就是说我们初始化可以不给默认的state状态值
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
preloadedState = undefined
}
// 判断enhancer是否存在,存在确保是function
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// 执行enhancer,执行第二步返回的函数
return enhancer(createStore)(
reducer,
preloadedState as PreloadedState<S>
) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
执行applyMiddleware得到一个接收createStore的函数,然后执行createSore检测是否由enhancer中间件函数,有就会调用applyMiddleware得到函数,然后compose组合多个中间件,此时就形成了由右向左的一层一层的dispatch方法,也就是说我们store.dispatch调用的是我们最右边的中间件,然后这个中间件调用next指向下一个中间件调用,最后才会分发到原始的dispatch方法。
redux-saga是用利用generator函数的特性来控制,先看下generator函数会返回什么,
function* gen(){
yield 'hello';
yield 'test';
return true;
}
let runs=gen();
//{next: ƒ, throw: ƒ, return: ƒ, Symbol(Symbol.iterator): ƒ}
console.log(runs);
//{value: 'hello', done: false}
console.log(runs.next())
//{value: 'test', done: false}
console.log(runs.next())
//{value: true, done: true}
console.log(runs.next())
generator函数执行返回的是一个iterator遍历器,有一个暂停和继续的效果,我们主动触发iterator的next()才会走向下一个yield。
我按照源码,理了几个简单的案例去实现一个简单redux-saga。
demo的文件总共有五个:
根据redux中间件的标准范式创建
import { Dispatch,Action } from "redux";
import {runSaga} from './runSaga';
const sagaMiddlewareFactory=()=>{
let boundRunSaga:any;
const sagaMiddleware=({dispatch,getState}:any)=>{
console.log('middleware init');
// 给runsaga绑定上dispatch和getState方法。
boundRunSaga=runSaga.bind(null,{
// channel,
dispatch,
getState,
})
return (next:Dispatch)=>(action:Action)=>{
console.log("middleware start");
const result=next(action);
console.log("middleware end");
return result;
}
}
sagaMiddleware.run=(...arg:any)=>{
// 运行runsaga
boundRunSaga(...arg);
}
return sagaMiddleware;
}
export default sagaMiddlewareFactory;
这里用于生成我们的副作用函数
// effect构造函数
const makeEffect = (type: string, payload: { pattern?: any; fn?: any; args?: any[]; action?: any }) => ({
IO: true,
type,
payload
})
// 用于命令middleware在store上等待指定的action,发起与pattern匹配上之后。会将generator暂停
export function take(pattern: any) {
return makeEffect('TAKE', { pattern })
}
// 命令middleware以非阻塞的形式执行fn
export function fork(fn: any) {
return makeEffect('FORK', { fn })
}
// 命令middleware以参数args调用函数fn,返回promise的函数
//{
//IO:true
//payload:{fn: ƒ, args: Array(0)}
//type:'CALL'}
// 调用之后就形成了这样一种格式的effects对象
}
export function call(fn: any, ...args: any[]) {
return makeEffect('CALL', { fn, args })
}
// 触发store.dispatch
export function put(action: any) {
return makeEffect('PUT', { action })
}
export function takeEvery(pattern:any, saga:any) {
function* takeEveryHelper() {
while (true) {
yield take(pattern);
yield fork(saga);
}
}
return fork(takeEveryHelper);
}
import proc from './proc';
export function runSaga({channel,dispatch,getState}:any,saga:any,...arg:any){
// 执行我们的generator函数。得到iterator遍历器
const iterator=saga(...arg);
const env={channel,dispatch,getState};
// 具体分发逻辑。
proc(env,iterator);
}
import effectRunnerMap from './effectRunnerMap';
export default function proc(env:any,iterator:any){
function next(arg:any,isErr:any){
let result;
// 这个因为我们调用的call,这里result={value:{IO:true,payload:{fn: ƒ, args: Array(0)},type:'CALL'},done:false}
if(isErr){
result=iterator.throw(arg);
}else{
result =iterator.next(arg);
}
if(!result.done){
//所以进入查找对应runEffect,next函数传递下去,
digestEffect(result.value,next);
}
}
next();
function runEffect(effect:any,currCb:any){
if(effect && effect.IO){
// CALL第一次进来到这个,
// 查找对应effectRunner
//参数env有store的getState和dispatch,payload是我们使用副作用时候的参数,currcb是当前的回调。
const effectRunner=effectRunnerMap[effect.type];
effectRunner(env,effect.payload,currCb);
}else{
currCb();
}
}
function digestEffect(effect:any,cb:any){
let effectSettled:any;
//封装回调
function currCb(res:any,isErr:any){
if(effectSettled){
return;
}
effectSettled=true;
cb(res,isErr);
}
//开始runEffect
runEffect(effect,currCb);
}
}
import proc from "./proc";
// 简单判断是不是promise
function isPromise(obj: Promise<any>) {
return obj && typeof obj.then === 'function';
}
function runTakeEffect(env: { channel: any; }, { channel = env.channel, pattern }: any, cb: any) {
const matcher = (input: { type: any; }) => input.type === pattern;
channel.take(cb, matcher);
}
function runForkEffect(env: any, { fn }: any, cb: () => void) {
const taskIterator = fn();
proc(env, taskIterator);
cb();
}
function runPutEffect(env: { dispatch: (arg0: any) => any; }, { action }: any, cb: (arg0: any) => void) {
const result = env.dispatch(action);
cb(result);
}
function runCallEffect(env: any, { fn, args }: any, cb: (arg0: any, arg1: boolean | undefined) => void) {
// 调用fetchUserInfoAPI,得到的是promise,回到了proc去看currCb
const result = fn.apply(null, args);
if (isPromise(result)) {
return result
.then((data: any) => cb(data,false))
.catch((error: any) => cb(error, true));
}
cb(result,false);
}
const effectRunnerMap = {
'TAKE': runTakeEffect,
'FORK': runForkEffect,
'PUT': runPutEffect,
'CALL': runCallEffect
};
export default effectRunnerMap;
import { call, put, takeEvery,} from '../saga-demo/effects';
import { fetchUserInfoAPI } from './api';
// import { call, put, takeEvery,} from 'redux-saga/effects';
// export function* fetchUserInfo() {
// yield put({ type: "FETCH_USER_FAILED", payload: 'message' });
// }
export function* rootSaga() {
const user = yield call(fetchUserInfoAPI);
yield put({ type: "FETCH_USER_SUCCEEDED", payload: user });
}
// fetchUserInfoAPI
export function fetchUserInfoAPI() {
return new Promise((resolve) => {
const mockData = {
id: '123',
name: '小明',
age: 18
};
resolve(mockData)
});
}
import { createStore, applyMiddleware } from 'redux'
import todos from './reducers'
import {rootSaga,fetchUserInfo} from './sagas'
import createSagaMiddleware from '../saga-demo/middleware';
// import createSagaMiddleware from 'redux-saga';
let sagaMiddleware=createSagaMiddleware();
let store = createStore(
todos,
[ 'Use Redux' ],
applyMiddleware(sagaMiddleware)
)
//
sagaMiddleware.run(rootSaga);
export default store;
第一步,在我们的案例里,saga里面有rootsaga,用了call和put副作用函数,在saga案例,执行调用就调用了effects对应的副作用函数,call得到的就是{IO:true,payload:{fn: ƒ, args: Array(0)},type:‘CALL’},put得到的是{IO:true,payload:{},type:‘PUT’}这样类似的结构。这是整个结构最开始执行的地方。
第二步,在redux集成sagamiddleware的地方,我们引入了rootsaga,并run起来了。run的是runsaga这个文件,这个文件主要是执行我们的rootsaga的generator函数,得到一个遍历器。进入(proc)查找运行对应副作用函数的文件(runEffect),这个函数进来就会执行一次在runsaga处得到iterator.next(),iterator.next()的结果是{value:{IO:true,payload:{fn: ƒ, args: Array(0)},type:‘CALL’},done:false},具体可以去查看proc处的内容,proc执行完毕。
第三步,到达具体runnerEffectsMap文件,我们第一个是call,去对应文件看下。调用异步函数,根据成功失败调用proc的currCb函数,给成功或者错误的结果。就回到了next函数,调用,就走到了put的逻辑,再次从第一步开始。
这几步,走下来,已经完成了我们正常利用saga去调用函数,得到结果放入redux数据,最后用于渲染页面,他的异步逻辑,和redux一点关系都没有。
参考文章:https://juejin.cn/post/6885223002703822855#heading-2
我的案例地址:https://github.com/lqy0101/fe-code-analysis/tree/main/example/react