咋深入Redux-Saga,要不手写一个

686 阅读11分钟

Snipaste_2022-05-12_11-28-25.png

每一个不曾起舞的日子,都是对生活的辜负

这期分享一下redux-saga源码实现,即使以前完全没用过redux-saga也没关系,我会先从如何使用讲起,然后再逐步替换redux-saga提供的常用api

redux-saga自己用Generator实现了一套异步流程管理,所以如果不了解Generator的可以先了解一下

Redux-Saga源码理解还是优点难度的

image.png

我们先从小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第一个参数正好等于我们之前dispatchaciontype,毫无疑问,这个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了,效果如下

sagaDemo_.gif

像预期一样,点击按钮的时候,发送了action,然后takeEvery中的fetchUserInfo开始执行,通过call执行网络请求,把结果赋值给user,再通过put发送真正的actionreducer处理该action,改变了storeuserInfo的状态 下面是流程示例图:

Untitled-2022-05-05-1546.png

整个流程还是比较简单的,redux-saga使用起来也不难,刚刚我们已经使用了它常见的api,这些api也是我们今天手写的主要目标

  1. createSagaMiddleware:返回一个中间件实例sagaMiddleware
  2. sagaMiddleware.run: 运行我们写的rootSaga的入口
  3. takeEvery:控制并发流程,对acion进行处理
  4. call:调用参数方法并返回结果
  5. put:真正发送action,用来和Reac-Redux通讯

image.png

下面我们来开始逐步替换上述用到的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什么时候注册回调呢?

image.png

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.donefalse,也就是说如果生成器还没执行完全执行完,就会执行digestEffect,那么我们的rootSaga第一次iterator.next()得到的是{value: takeEvery("FETCH_USER_INFO", fetchUserInfo), done: false},第二次执行iterator.next()得到的是{value: undefined, done: true}。所以如果只是执行一次,donefalse,会继续执行digestEffect

在继续看digestEffect的实现之前,我们有一个点还没搞明白,那就是takeEvery("FETCH_USER_INFO", fetchUserInfo)的返回值到底是什么?其实takeEvery是一个由takefork这两个低级effect组成的高级effect,大概长这么个样子(先不用深入理解,后面自然明白了为什么)

export function takeEvery(pattern, saga) {
  function* takeEveryHelper() {
    while (true) {
      yield take(pattern);
      yield fork(saga);
    }
  }

  return fork(takeEveryHelper);
}

所以takeEvery的返回值就等于fork的返回值,而forktake的返回值其实差不多,只是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 })
}

forkpayload存了{ fn },take的payload存了{ pattern } 所以takeEvery("FETCH_USER_INFO", fetchUserInfo)的返回值实际等于{IO: true, type: 'FORK', payload: { fn: takeEveryHelper }}

注意:这里fork存的fn不是我们传给takeEveryfetchUserInfo,而是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找到effecttype所对应的内置函数,这里的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工作流程,方便我们理解

Untitled-2022-05-05-1546.png

整个流程用一句话概括就是,proc会将传进去的generator先执行一遍,然后通过effect.type找到对应的runXXXEffec并执行(runXXXEffect可以先不用理解,用到的时候再回来看)

image.png

这里索性我把后面要用到的callput对应的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处理流程,知道这点,就很容易理解takeEveryrunForkEffect中未分析的代码了,其实就是把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中心又注册了一个回调

image.png

刚刚我们聊到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会将传进来的actiondispatch出去,这个dispatch就是reduxdispatch,到这里,其实就已经改变了reduxstore存储的userInfo了,主页面发生更新,put函数同时还会cb(result),这里的cbcallcb是一样的,这也是为什么我们写的fetchUserInfo生成器会自己一步一步执行

到这里Redux-Saga常用api已经被我们撕完了,我们实现了fork, take, takeEvery, put, call, createSagaMiddleware,sagaMiddleware.run。不难发现fork,put,call都会在最后执行一次cb(),自动将当前的generator执行下一步,而take最终目的是存注册函数,该函数实际执行的就是当前generator,而这个回调需要我们手动dispatch才会触发。

image.png