Redux react-redux redux redux redux redux redux redux Redux Redux-thunk can be used to handle asynchronous requests and side effects in redux or React-Redux, but saga is much easier to handle in more complex cases.

concept

Redux-saga is a library for managing Side effects (Side effects, such as asynchronously fetching data, accessing browser caches, etc.) in applications, with the goal of making Side effects management easier, execution more efficient, testing simpler, and handling failures easier.

Front knowledge

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

English document: redux-saga.js.org/

There are preconditions for learning saga. If the following points are not clear, it may be difficult to learn. It is suggested to learn saga first.

  • generator
  • promise
  • redux
  • Redux middleware
  • react

Basic properties and implementation

effect

Concept: In the redux-Saga world, Sagas are implemented with Generator functions. We yield pure JavaScript objects from the Generator to express Saga logic. We call those objects effects

The Interface is the Interface. The payload is the payload.

call

Block the call to saga, and the code will continue to execute only after the result of the call saga is returned. Use:

yield call(Interface, payload);
Copy the code

fork

A non-blocking call to saga without waiting for the saga code of the fork call to continue;

yield fork(Interface, payload);
Copy the code

all

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

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

take

Take creates a command object that tells middleware to wait for action on a pattern that redux Dipatch matches.

const action = yield take(PATTERN);
Copy the code

put

This function creates a dispatchEffect that changes the state in the Redux Store, which is essentially a wrapper around the Dispatch in Redux

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

The above several effect source code implementation is relatively simple, in fact, is a simple tag, tell the subsequent program I here is what operation! I’ll just stick to the core principle code.

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

// Mark the operation type
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 })
}
// Fn of call is a promise
export function call(fn, ... arg) {
  return makeEffect(effectTypes.CALL, { fn, arg })
}
// fork fn is a generator function
export function fork(fn, ... arg) {
  return makeEffect(effectTypes.FORK, { fn, arg })
}
// FNS of all is an array of promises
export function all(fns) {
  return makeEffect(effectTypes.ALL, fns)
}
Copy the code

Two mark constant file here directly to the source address it!

  • Symbols file
  • EffectType file
  • Is the file

createSagaMiddleware

The function in the source code that handles the createSagaMiddleware logic is called sagaMiddlewareFactory

The source code

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

export default function createSagaMiddleware() {

  let boundRunSaga;
  // We need to compare actiony with Pattern. We need to make sure that we use the same channel
  let channel = stdChannel()
  // Redux's middleware handles middleware with getStore, dispatch
  Return next => action => next(action). Check out the middleware source code for Redux
  function sagaMiddleware({ getStore, dispatch }) {
    // Since we want runSaga to take control of the store and receive arguments to the sagamiddleware. run function, we
    // Assign to boundRunSaga using the bind cache here and pass in the control function, the first argument is null because there is no need to change scope
    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
}
Copy the code

runSaga

The source code

import proc from "./proc"
export default function runSaga({ channel, getStore, disparch }, saga, ... args) {
  // This is the saga generator method that we need to execute to get the traverser object
  // We need to get the traverser object to get the state inside
  // We need to do this step for the user
  const iterator = saga(args)
  // According to the generator's lazy evaluation nature, we declare a separate file (proc) to handle generator's next method
  // proc deals with the iterator object and needs to modify the state of the disparch} {getStore, disparch}, iterator
  const env = { channel, getStore, disparch }
  proc(env, iterator)

` ``js
Copy the code

}

### proc Take the traverser object passed by runSaga, call the next function of the traverser object, And the effectRunnerMap function is called with the effectRunnerMap flag. Js import effectRunnerMap from./effectRunnerMap; import { IO } form "./symbols"; Proc (env, iterator, cb) {export default function proc(env, iterator, cb) {// Next (); function next(arg, isErr) { let result; // We need to determine if there is an error, If (isErr) {result = iterator.throw(arg)} else {result.next(arg)} // Result {value, done: true/false} // If done is fasle, the traversal is not complete and you need to continue traversal if (! result.done) { digesEffect(result.value, If (cb && typeof cb === "function") {cb(result)}}} function runEffect(effect, CurrCb) {/ / figure out whether the effect method of saga defined inside the if (effect && effect [IO]) {/ / method to obtain the corresponding according to mark const effectRunner = EffectRunnerMap [effect.type] effectRunner(env, effect.payload, currCb)} else {// If an effect is not internally defined, effectRunnerMap is effectRunnerMap. Next currCb()}} function digesEffect(effect, digesEffect) Cb) {// Let effectSettled; function currCb(res, isErr) { if (effectSettled) { return } effectSettled = true cb(res, isErr) } runEffect(effect, currCb) } }Copy the code

effectRunnerMap

Function: This store stores the specific processing logic of side effects such as take, call, etc., including the operation of changing the state of the store

The source code

import effectTypes from './effectTypes'
import proc from "./proc"
import { promise, iterator } from './is'
// This file corresponds to the token in the effect method
// This is done because the source code takes channels that are passed in from outside the environment. Env is used by default
function runTakeEffect(env, { channel = env.channel, pattern }, cb) {
  // We can only send dispatch once to get the corresponding pattern
  // Cb will be executed only when pattern matches the dispatch action
  const matcher = input= > input.type === pattern;
  // After matching, we need to associate CB and pattern and save them to be called after dispatch
  // So we declare a channel to save
  channel.take(cb, matcher)
}
function runPutEffect(env, { action }, cb) {
  // Put is the process of changing the state in the store.
  // Call cb again and return the result of dispatch
  const result = env.dispatch(action)
  cb(result)
}
function runCallEffect(env, { fn, args }, cb) {
  // call Fn may be a promise, a generator, or a normal function
  // The result is a static file called IS
  const result = fn.apply(null, args)
  ResolvePromise refers to the is file in the resolvePromise file
  if (promise(result)) {
    // Call cb in then
    result.then(resp= > cb(resp)).catch(error= > cb(error, true))
    return
  }
  // The iterator is also taken from the is static file
  if (iterator(result)) {
    // Adds a new argument to the proc function to block cb only when the traverser result done is true
    proc(env, result, cb)
    return
  }
  // If it is a normal function we call cb directly
  cb(result)
}
function runForkEffect(env, { fn, args }, cb) {
  // Execute fn, which is generator, to fetch the traverser object, and next to the traverser object
  // So we'll just leave it to the proc to handle
  // The args is an array-like object that we destruct when we mark fork
  // The user calls fork and the second parameter is payload, so we should write fn(args[0]) to get the correct payload.
  // But for better compatibility, the source code uses fn.apply(args), using the principle that apply accepts an array of class parameters, to deconstruct the parameters
  const iterator = fn.apply(args)
  proc(env, iterator)
  // After the processing is complete, call cb directly because fork is non-blocking
  cb()
}
function runAllEffect(env, fns, cb) {
  // FNS is an array of traversal objects. We can get each traversal object by iterating through the array
  // Then proceed to process the traverser object using the proc file
  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
Copy the code

channel

We need to initialize in createSagaMiddleware that we use take and PUT to communicate with the Redux Store, and channel Outlines the communication between these effects and external event sources or SAGAs;

The source code

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

  // Declare a variable to hold, since it can be multiple, use arrays
  let currentTakers = [];

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

  function put(input) {
    const takers = currentTakers;
    Because currentTakers is dynamic and if you don't assign a value to Len here it might cause a loop
    for (let i = 0, len = takers.length; i < len; i++) {
      const taker = takers[i];
      if (taker[MATCH](input)) {
        taker(input)
      }
    }
  }
  return {
    take, put
  }
}
Copy the code

conclusion

The above are some basic core logic codes of effect and the overall process of Saga. Here is a summary of the process:

  1. Initialize a channel in createSagaMiddleware and take control of stores released from Redux’s Middleware;
  2. Reassign the runSaga function to sagamiddleware. run with bind and append control of the store and the initialized channel;
  3. Get the iterator object (iterator) in runSaga and call the proc file to process the iterator object (iterator);
  4. The proc is responsible for executing the iterator object, and through the IO tag and effectRunnerMap, identifies the effect that the iterator object is processing, and calls the corresponding effectRunnerMap function to process it.

Personally, I think the apply and bind is also a wonderful use. Bracket to smile