头图
前面几篇文章讲了redux react-redux 今天就来讲讲redu-sage,为什么要单独拿这个中间件来说呢?想必大家都知道,因为这个中间件很普遍,对于我们在redux或者react-redux中处理异步请求以及副作用,简单的异步我们可以是用redux-thunk,也是可以完成,但是对于比较复杂的情况saga应付起来就比较容易,也不易发生回调地狱!

概念

redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。

前置知识

中文文档:https://redux-saga-in-chinese...

英文文档:https://redux-saga.js.org/

学习saga是有前提条件的如果以下知识点还不太清楚,那学起来可能会比较吃力,建议先行学习;

基本属性以及实现

effect

概念:在 redux-saga 的世界里,Sagas 都用 Generator 函数实现。我们从 Generator 里 yield 纯 JavaScript 对象以表达 Saga 逻辑。 我们称呼那些对象为 Effect

注意下述代码中Interface代表接口的意思,payload代表参数,大写的英文代表指令;

call

阻塞调用saga,只有call调用的saga有结果返回以后代码才会继续执行;
使用:

yield call(Interface, payload);

fork

非阻塞调用saga,无需等待fork调用的saga代码继续执行;

yield fork(Interface, payload);

all

阻塞调用可同时调用多个saga,类似于promise.all;

yield all([
  Interface(payload),
  Interface1(payload1),
]);

take

take创建一个命令对象,告诉middleware等待redux dipatch匹配的某个pattern的action;

const action = yield take(PATTERN);

put

这个函数用于创建dispatchEffect,可以修改redux store中的状态,其实就是redux中dispatch的封装

yield put({type: ACTION, payload: payload});

以上几个effect 源码实现起来比较简单,其实就是进行一个简单的标记,告诉后续的程序我这里是什么操作而已!就直接贴核心原理代码了。

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

// 标记操作类型
const makeEffect = (type, payload) => ({ [IO]: IO, type, payload });

export function take(pattern) {
  return makeEffect(effectTypes.TAKE, { pattern })
}
export function put(action) {
  return makeEffect(effectTypes.PUT, { action })
}
// call的fn是一个promise
export function call(fn, ...arg) {
  return makeEffect(effectTypes.CALL, { fn, arg })
}
// fork的fn是一个generator函数
export function fork(fn, ...arg) {
  return makeEffect(effectTypes.FORK, { fn, arg })
}
// all的fns是一个promise组成的数组
export function all(fns) {
  return makeEffect(effectTypes.ALL, fns)
}

两个标记常量的文件这里直接给源码的地址吧!

createSagaMiddleware

源码中处理createSagaMiddleware这个逻辑的函数名叫sagaMiddlewareFactory

源码

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

export default function createSagaMiddleware() {

  let boundRunSaga;
  // 因为需要比对actiony和pattern,需要保证使用的是一个channel所以在这里初始化一次channel即可
  let channel = stdChannel()
  // 根据redux 的middleware对于中间件的处理我们可以了解这里热入参是getStore, dispatch
  // 并且返回一个next => action => next(action)的函数,不了解的小伙伴可以去翻看下我之前写的redux的middleware的源码
  function sagaMiddleware({ getStore, dispatch }) {
    // 因为我们希望runSaga可以获取到store的控制权,并且接收sagaMiddleware.run函数的参数,所以我们
    // 在这里用bind缓存赋值给boundRunSaga,并将控制权函数传入,因为不需要改变作用域所以第一个参数为null
    boundRunSaga = runSaga.bind(null, { channel, getStore, dispatch })

    return next => action => {
      const result = next(action)
      channel.put(action)
      return result
    }
  }
  sagaMiddleware.run = (...args) => boundRunSaga(...args)

  return sagaMiddleware
}

runSaga

源码

import proc from "./proc"
export default function runSaga({ channel, getStore, disparch }, saga, ...args) {
  // 这个saga就是generator方法,我们需要执行才能获取到遍历器对象
  // 我们需要拿到遍历器对象才能拿到里面的状态,执行里面的effect
  // 这步骤我们需要我们替用户操作
  const iterator = saga(args)
  // 根据generator惰性求值的特点,我们单独声明一个文件(proc)去处理generator的next方法
  // proc需要处理的是遍历器对象,以及过程中需要修改状态所以需要{ getStore, disparch }, iterator作为参数
  const env = { channel, getStore, disparch }
  proc(env, iterator)
}

proc

功能:接受runSaga传递过来的遍历器对象,调用遍历器对象的next函数,并且以及effect的标记调用effectRunnerMap中对应的函数

import effectRunnerMap from "./effectRunnerMap";
import { IO } form "./symbols";

export default function proc(env, iterator, cb) {
  // 这里面我们需要处理next函数,所以我们需要自己定义下next
  // 首次调用是不需要参数的
  next();
  function next(arg, isErr) {
    let result;
    // 执行中我们需要判断是否存在错误,确定无错误的时候才正常执行遍历器对象的next函数
    if (isErr) {
      // 在这里的arg是具体的错误信息
      result = iterator.throw(arg)
    }
    else {
      result.next(arg)
    }
    // result {value, done: true/false}
    // 如果done为fasle,说明遍历未结束,需要继续遍历
    if (!result.done) {
      digesEffect(result.value, next)
    }
    else {
      // 遍历结束
      if (cb && typeof cb === "function") {
        cb(result)
      }
    }
  }

  function runEffect(effect, currCb) {
    // 判断这里的effect方法是不是saga内部定义的
    if (effect && effect[IO]) {
      // 根据标记获取对应的方法
      const effectRunner = effectRunnerMap[effect.type]
      effectRunner(env, effect.payload, currCb)
    }
    else {
      // 如果不是内部定义的effect,则直接执行currCb,进行下一次next
      currCb()
    }
  }

  // 我们需要在digesEffect在处理具体的effect比如take/put/call等等
  function digesEffect(effect, cb) {
    // 在这里我们需要判断一下effect的执行状态如果执行结束就不需要重复执行
    let effectSettled;
    function currCb(res, isErr) {
      if (effectSettled) {
        return
      }
      effectSettled = true
      cb(res, isErr)
    }
    runEffect(effect, currCb)
  }
}

effectRunnerMap

功能:这里存放的是take、call等副作用的具体处理逻辑包括修改store中state的操作

源码

import effectTypes from './effectTypes'
import proc from "./proc"
import { promise, iterator } from './is'
// 这个文件主要是和effect方法中的标记相对应根据当时标记获取这里对应的方法
// channel 这样获取是因为源码中的take是可以接受外界传进来的channel的,默认使用env当中的
function runTakeEffect(env, { channel = env.channel, pattern }, cb) {
  // 我们只有发起一次dispatch拿到对应的pattern
  //并且pattern和dispatch的action匹配上才会去执行cb
  const matcher = input => input.type === pattern;
  // 匹配以后我们需要把cb 和 pattern关联以后保存起来等待dispatch之后调用
  // 所以我们声明一个channel来保存
  channel.take(cb, matcher)
}
function runPutEffect(env, { action }, cb) {
  // put 其实就是修改store中的state的过程,所以直接执行dispatch就可以了,
  // 同样的我们执行只有继续调用cb,并把dispatch的执行结果返回
  const result = env.dispatch(action)
  cb(result)
}
function runCallEffect(env, { fn, args }, cb) {
  // call这里的fn可能是promise,也可能是generator函数,也可能就是普通函数需要区分
  // 源码中专门判断返回的result的类型是不是promise类型,是一个叫is的静态文件
  const result = fn.apply(null, args)
  // 源码中是调用的resolvePromise函数来判断的,在resolvePromise中引用了is文件
  if (promise(result)) {
    // 在then中回调cb
    result.then(resp => cb(resp)).catch(error => cb(error, true))
    return
  }
  // iterator也是从is静态文件取出来的
  if (iterator(result)) {
    // 在proc函数上加一个新的参数,目的是在遍历器结果done为true的时候才去执行cb从而达到阻塞的效果
    proc(env, result, cb)
    return
  }
  // 如果是普通函数的我们直接调用cb
  cb(result)
}
function runForkEffect(env, { fn, args }, cb) {
  // 先执行fn, fn是generator函数,执行fn先拿到遍历器对象,然后在执行遍历器对象的next
  // 所以我们继续交给proc来处理就好了
  // 这里需要注意的是啊这个apply,我们之前标记fork函数的时候对args进行了解构,所以这里的args是一个类数组对象
  // 而用户调用fork的是传入的第二个参数是payload,所以这里我们其实应该写fn(args[0])才能获取到正确的payload,
  // 但是为了更好的兼容,源码中使用了fn.apply(args),利用apply接受一个类数组参数的原理,对参数进行解构
  const iterator = fn.apply(args)
  proc(env, iterator)
  // 处理完成完以后,直接调用cb即可,因为fork是非阻塞的
  cb()
}
function runAllEffect(env, fns, cb) {
  // 这里的fns是遍历器对象组成的数组,我们遍历这个数组就可以拿到每一个遍历器对象
  // 然后继续使用proc文件处理这个遍历器对象
  const len = fns.length;
  for (let i = 0; i < len; i++) {
    proc(env, fns[i])
  }
}

const effectRunnerMap = {
  [effectTypes.TAKE]: runTakeEffect,
  [effectTypes.PUT]: runPutEffect,
  [effectTypes.CALL]: runCallEffect,
  [effectTypes.FORK]: runForkEffect,
  [effectTypes.ALL]: runAllEffect,
}
export default effectRunnerMap

channel

需要在createSagaMiddleware中初始化
我们使用take和put来与redux store进行通信,channel概括了这些effect与外部事件源或sagas之间的通信;

源码

import { MATCH } form "./symbols";
export function stdChannel() {

  // 声明一个变量来保存,因为有可能是多个所以使用数组
  let currentTakers = [];

  function take(cb, matcher) {
    cb[MATCH] = matcher
    currentTakers.push(cb)
  }

  function put(input) {
    const takers = currentTakers;
    // 因为currentTakers是动态变化的如果这里不赋值给len有可能会造成死循环
    for (let i = 0, len = takers.length; i < len; i++) {
      const taker = takers[i];
      if (taker[MATCH](input)) {
        taker(input)
      }
    }
  }
  return {
    take, put
  }
}

总结

以上就是一些基础的effect的核心逻辑代码,以及saga整体流程,这里简单做个流程总结:

  1. 在createSagaMiddleware中初始化channel,并且获取从redux的middleware中释放出来的store的控制权;
  2. 用bind将runSaga函数重新赋值给sagaMiddleware.run 并追加store的控制权以及经过初始化的channel;
  3. 在runSaga中获取遍历器对象(iterator),并调用proc文件处理遍历器对象(iterator);
  4. proc主要负责执行遍历器对象,并通过IO标记和effectRunnerMap具体确认当前遍历器对象主要处理的effect是哪一种,并调用effectRunnerMap中对应的函数进行处理;
    个人觉得这个apply和bind也算是一种妙用吧!括弧笑

machinist
460 声望33 粉丝

javaScript、typescript、 react全家桶、vue全家桶、 echarts、node、webpack