手写redux-saga

186 阅读4分钟

前言

在react开发实践中,由于过多复杂异步交互逻辑经常发生,使用redux-thunk,react-promise,async await等方案实现异步交互,始终无法完美适配应用各种场景。于是redux-saga出生了,基于generator的原理实现的全部异步流程。

saga的使用

  1. 调用createSagaMiddleware创建saga中间件函数
  2. 使用applyMiddleware将中间件传入,此处主要是为了获取redux中的数据操作权力。
  3. 启动saga函数,sagaMiddleware.run(rootSaga)
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "../redux-saga";

import { getPayload, rootSaga } from "./saga"; // 拿到定义好的saga

const sagaMiddleware = createSagaMiddleware();

// 创建一个计数器
function createCounter({count = 0} = {}, action) {
  switch (action.type) {
    case "sagaPayload": { // 使用saga进行异步
      console.log('sagaPayload action',action);
      // return state + 1;
      return {
        count: count + 1,
        payload: action.payload
      }
    }
    default: return { count: count, payload: [] };
  }
}

// 将数据源导出使用
const store = createStore(
  createCounter, 
  // applyMiddleware(sagaMiddleware, reduxThunk, reduxPromise, reduxLogger)
  applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(rootSaga); // 将想使用的saga注入

export default store;

createSagaMiddleware

创建saga中间件,并执行初始化,将回调与key进行关联

import { stdChannel } from "./channel";
import runSaga from "./runSaga";

export default function createSagaMiddleware() {
  let boundRunSaga;
  let channel = stdChannel(); // 拿到数据存储区域,用于公用数据存储
  function sagaMiddleware({ getState, dispatch }) {
    boundRunSaga = runSaga.bind(null, { channel, getState, dispatch }); // 使用bind传入参数,并返回新函数可供调用, 传入存储结构 channel
    return next => action => {
      let result = next(action);
      channel.put(action); // 当用户调用时(触发dispatch),执行下一个generator的next
      return result
    }
  }

  // run接收的就是实际的saga函数(generator), 使用展开符,否则接收的是个数组
  sagaMiddleware.run = (...args) => boundRunSaga(...args);
  // 返回的这个函数用于连接redux。
  return sagaMiddleware;
}

stdChannel

import { MATCH } from "./symbols";

// 额外开一个区域存储数据
export function stdChannel() {
  const currentTakes = [];
  
  function take(cb, matcher) {
    cb[MATCH] = matcher; // 创建关联关系,以便将来查找 
    currentTakes.push(cb);
  }

  function put(input) {
    const takers = currentTakes;
    for(let i=0, len = takers.length; i<len; i++) {
      const taker = takers[i];
      if (taker[MATCH](input)) { // 这里这个函数是effectRunnerMap中定义的 matcher
        taker(input); // 这里是 take 接收的cb
      }
    }
  }

  return {
    take,
    put
  }
}

runSaga

因为saga是个generator函数,所有要先触发,拿到迭代器对象。

import proc from "./proc";

export default function runSaga({channel, getState, dispatch }, saga, ...args) {
  const iterator = saga(...args); // 拿到迭代器对象
  const env = { channel, getState, dispatch }; // 传入需要的数据,以便将来使用
  proc(env, iterator); // 执行遍历器
}

proc

执行遍历器,直至generator对象的 done 属性为 true 这里是获取用户使用 call fork put 这些generator函数执行的时刻 会拿到这些函数的返回值,res.value,并进行一系列操作。

import effectRunnerMap from "./effectRunnerMap";
import { IO } from "./symbols";
export default function proc(env, iterator, cb) {
  // console.log("proc", env, iterator);
  next(); // 执行遍历器next
  
  function next(arg, isErr) {
    let res;
    if (isErr) {
      res = iterator.throw(arg);
    } else {
      res = iterator.next(arg);
    }

    // res => {value:, done: false/true}
    if (!res.done) {
      digestEffect(res.value, next);
    } else {
      if (typeof cb === "function") cb(res);
    }
  }

  function digestEffect(effect, cb) {
    // 标记是否结束,且状态不可变 类似 promise 中的状态;
    let effectSettled;
  
    function currCb(res, isErr) {
      if (effectSettled) return;
      effectSettled = true;
      cb(res, isErr)
    }
  
    runEffect(effect, currCb);
  }
  
  function runEffect(effect, currCb) {
    // 判断是否是effect对象
    // console.log("runEffect", effect);
    if (effect && effect[IO]) {
      const effectRunner = effectRunnerMap[effect.type];
      effectRunner(env, effect.payload, currCb);
    } else {
      currCb(); // 如果不是直接执行即可
    }
  }
}

effectRunnerMap

这里是真正执行对应操作的函数,runTakeEffect是收集 key 匹配的回调,将来在更新dispatch时,会触发收集的回调函数。

import effectTypes from "./effectTypes";
import proc from "./proc";

// 返回对应类型需要执行的函数
const effectRunnerMap = {
  [effectTypes.TAKE]: runTakeEffect,
  [effectTypes.PUT]: runPutEffect,
  [effectTypes.CALL]: runCallEffect,
  [effectTypes.FORK]: runForkEffect,
  [effectTypes.ALL]: runAllEffect,
}

// 给用户暴露 channel 参数,可自定义传入、
function runTakeEffect(env, {channel = env.channel, pattern}, cb) {
  // console.log("runTakeEffect", arguments);
  const matcher = input => input.type === pattern;
  channel.take(cb, matcher);
}

function runPutEffect(env, {action}, cb) {
  // console.log("runPutEffect", arguments);
  const res = env.dispatch(action);
  cb(res);
}

function runCallEffect(env, {fn, args}, cb) {
  // console.log("runCallEffect", arguments);
  const res = fn.apply(null, args);
  if (isPromise(res)) { // fn是promise
    res
      .then(data => cb(data))
      .catch(err => cb(err, true))
  } else if (isGenerator(res)) {
    proc(env, res, cb);
  } else {
    cb(res);
  }
  
}

function runForkEffect(env, {fn, args}, cb) {
  // console.log("runForkEffect", arguments);
  // fn 是generator
  const taskIterator = fn.apply(null, args); // args是一个数组,需要展开
  proc(env, taskIterator);
  cb(); // 下一个next
}

function runAllEffect(env, effects, cb) {
  const length = effects.length;
  for (let i = 0; i < length; i++) {
    proc(env, effects[i])
  }
}

function isPromise(fn) {
  return typeof fn.then === "function"
}

function isGenerator(obj) {
  return typeof obj.next === "function"
}

export default effectRunnerMap;

effects

真正暴露给用户使用的函数,用于获取副作用,将传入的key,参数,回调等一一建立联系,以便将来执行时一一匹配。


import effectTypes from "./effectTypes";
import { IO } from "./symbols";

// 标记effect对象
const makeEffect = (type, payload) => {
  return { [IO]: IO, type, payload }
} 

// 以下函数,因为是generator对象,所以在迭代器执行过程中能够获取makeEffect()的返回值,虽然并没有显式返回。
// yield call() => iterator.value(在这里获取)
// 代码中在 proc.js 下 digestEffect 能够清楚看见。

export function take(pattern) {
  // console.log("pattern", pattern);
  return makeEffect(effectTypes.TAKE, {pattern})
}

export function put(action) {
  return makeEffect(effectTypes.PUT, {action})
}

export function call(fn, ...args) {
  return makeEffect(effectTypes.CALL, { fn, args })
}

export function fork(fn, ...args) {
  return makeEffect(effectTypes.FORK, { fn, ...args })
}

export function all(effects) {
  return makeEffect(effectTypes.ALL, effects)
}

创建使用函数

这是使用层,定义异步逻辑


import { all, call, put, take, fork } from "../redux-saga/effects";

// worker 真正的工作函数
export function* getPayloadUseSaga() {
  const res = yield call(getOriginPayload); // 调用层 拿到结果
  // console.log('res', res);
  yield put({ type: "sagaPayload", payload: res.data}) // 通知层,通知redux该更新数据了
}

// watcher 函数,连接对应 key 和回调的关系
export function* getPayload() {
  while (true) {
    const action = yield take("getSagaPayload");
    console.log("action getSagaPayload", action);
    yield fork(getPayloadUseSaga, action)
  }
}

const getOriginPayload = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ type: "origin", data: [1, 2, 3, 4] })
    }, 1000);
  })
}

// 点击事件。发送对应key的事件响应
export const handle = (payload) => {
  console.log('payload', payload);
  return { type: "getSagaPayload", payload }
}

// all 执行多个saga
export function* rootSaga() {
  yield all([getPayload()])
}

组件调用

import React, { } from 'react';
import { connect } from "react-redux";
import { handle } from '../store/saga'; 
const Index = (props) => {
  
  const sagaPayload = () => {
    console.log("点击");
    props.handle(123)
  }

  return (
    <div>
      <h1>首页</h1>
      <h3>count: {props.count}</h3>

      <ul>
        {props.payload.map(v => {
          return <li key={v}>{v}</li>
        })}
        
      </ul>

      <p>
        <button onClick={sagaPayload}>saga获取数据</button>
      </p>
    </div>
  )
}

// 拿到state。并决定哪些值要映射到index的props上
const mapStateToProps = function(state) {
  console.log("state", state);
  return state;
}

// 映射dispatch action,减少手动调用dispatch的步骤
const mapDispatchToPorps = {
  handle
}

// 使用react-redux connect返回一个新的组件。对其进行props的额外添加和数据监听等
const WrappedIndex = connect(
  mapStateToProps,
  mapDispatchToPorps
)(Index);

export default WrappedIndex;

结语

以上便实现了redux-saga的大部分功能,处理逻辑确实有些复杂,需要多看几遍才能理解。其中的核心思路是通过yield"暂停"函数,使用iterator.value"隔空(非显式)"获取函数的返回值,从而进行逻辑处理。github代码