头图
The previous articles talked about redux react-redux and today I will talk about redu-sage, why should we take this middleware separately? Presumably everyone knows, because this middleware is very common, for us to handle asynchronous requests and side effects in redux or react-redux, we can use redux-thunk for simple asynchrony, and it can also be done, but for more complex situations saga It's easier to get up, and callback hell is not easy to happen!

concept

redux-saga is a library for managing side effects of applications (side effects, such as obtaining data asynchronously, accessing browser caches, etc.). Its goal is to make side effects management easier, more efficient to execute, simpler to test, and more effective in handling failures. easy.

Pre-knowledge

Chinese document: https://redux-saga-in-chinese.js.org/index.html

English document: https://redux-saga.js.org/

There are prerequisites for learning saga. If the following knowledge points are not clear, it may be more difficult to learn. It is recommended to learn first;

Basic properties and implementation

effect

Concept: In redux-saga , Sagas are all implemented with the Generator function. We yield pure JavaScript objects from Generator to express Saga logic. We call those objects Effect

Note that in the following code, Interface represents the meaning of the interface, payload represents the parameter, and the uppercase English represents the command;

call

Block calling saga, the code will continue to execute only after the saga called by the call has a result returned;
use:

yield call(Interface, payload);

fork

Call saga non-blocking, without waiting for the saga code called by fork to continue execution;

yield fork(Interface, payload);

all

Blocking call can call multiple saga at the same time, similar to promise.all;

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

take

take creates a command object and tells middleware to wait for the action of a certain pattern matched by the redux dipatch;

const action = yield take(PATTERN);

put

This function is used to create the dispatchEffect, which can modify the state in the redux store, which is actually the encapsulation of the dispatch in redux

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

The above effect source code is relatively simple to implement. In fact, it is a simple mark to tell the follow-up program what operation I am here! Just post the core principle code directly.

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)
}

Here are the two files marked with constants and give the address of the source code directly!

createSagaMiddleware

The function that handles the logic of createSagaMiddleware the source code is sagaMiddlewareFactory

source code

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

source code

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

Function: accept the traverser object passed by runSaga, call the next function of the traverser object, and call the corresponding function in the effectRunnerMap with the mark of the effect

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

Function: Stored here is the specific processing logic of side effects such as take, call, etc., including the operation of modifying the state in the store

source code

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

Need to be initialized in createSagaMiddleware
We use take and put to communicate with the redux store, and channel summarizes the communication between these effects and external event sources or sagas;

source code

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
  }
}

Summarize

The above is the core logic code of some basic effects and the overall process of saga. Here is a brief summary of the process:

  1. Initialize the channel in createSagaMiddleware, and obtain the control right of the store released from the middleware of redux;
  2. Use bind to re-assign the runSaga function to sagaMiddleware.run and append the control of the store and the initialized channel;
  3. Obtain the iterator object in runSaga, and call the proc file to process the iterator;
  4. The proc is mainly responsible for executing the traverser object, and specifically confirms which effect is mainly processed by the current traverser object through the IO mark and effectRunnerMap, and calls the corresponding function in the effectRunnerMap for processing;
    I personally think that apply and bind are also a kind of magical effect! Bracket laugh

machinist
460 声望33 粉丝

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