react-saga原理探索

胡永逸
2023-12-01

redux-saga原理探索

这篇文章是为了取探索saga的异步工作流和effect的处理,探究他的思路和工作原理。

redux的applyMiddleware

从头开始撸

saga是一个redux的中间件,而redux提供了一个applyMiddleware(...middlewares)函数,

middleware它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,可以接收store的dispatch和getState方法,我们来做一个简单的测试去理解redux的中间件。

  • 第一步,logger 可以说是redux中间件的范式,也就是说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)// 应用中间件
)
  • 第二步,applyMiddleware的实现,整体看起来很简单,执行他只是返回了一个函数,接收createStore的构造器。这里并没有执行任何东西,只是得到了一个函数而已。
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)))
}
  • 第三步,程序程序回到我们的第一步,在执行createStore,他接收三个参数。reducers,state,enhancer中间件函数。这里得到的是第二步返回的函数**(createStore: StoreCreator)**,在案例里面是由中间件的。他会执行第二步返回的函数。我们回到第二步看。
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

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。

简单案例-demo

我按照源码,理了几个简单的案例去实现一个简单redux-saga。

demo的文件总共有五个:

  • middleware.ts:用于构建redux的中间件介入redux
  • effects.ts:创建各种各样的副作用函数,如:take\fork\call\put
  • runSaga:saga的run相关配置
  • proc:分发effects,及next逻辑控制相关
  • effectRunnerMap:对应的effects运行方式。
sagaMiddleware中间件 — middleware.ts

根据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;
effects.ts

这里用于生成我们的副作用函数

// 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);
  }
runSaga.ts
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);
}
proc.ts
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);
    }
}
runnerEffectsMap.ts
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;
引入中间件及saga案例
saga案例
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;
分析
  1. 第一步,在我们的案例里,saga里面有rootsaga,用了call和put副作用函数,在saga案例,执行调用就调用了effects对应的副作用函数,call得到的就是{IO:true,payload:{fn: ƒ, args: Array(0)},type:‘CALL’},put得到的是{IO:true,payload:{},type:‘PUT’}这样类似的结构。这是整个结构最开始执行的地方。

  2. 第二步,在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执行完毕。

  3. 第三步,到达具体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

 类似资料: