Story – a brief introduction of saga

Redux-saga, a resounding name, although it has been introduced in the last article, but after reading its source code, I can not help but solemnly introduce it again. This is a framework for managing application “side effects”, and although it is mostly used as Redux middleware, it is fundamentally independent of any other libraries and can be used alone. Using it to manage program side effects has the following advantages:

  1. Better tests
  2. Clearer code logic
  3. Easily manage startup and cancellation of side effects

PS: The code in this library is really well written, full of computer terminology, which makes me feel familiar. Fork, channel, Task, IO concept, Semaphore, buffer, Saga has won so much just by the concept layer

Some of the concepts

Effect

As the official website notes for the umptuously many times, an Effect is an execution unit of the Saga middleware. Generated through an internal API, it is a simple object that contains information, such as a type attribute, that the Saga middleware can use to select down-execute, block, dispatch actions And so on (take, fork, put)

saga

As for what is a saga, my personal understanding is a collection of some effects, which can be divided into work saga and root saga. Root saga is responsible for distributing actions, and Work Saga is responsible for specifying actions Combinative nesting between the two can operate according to Generator function syntax

function * worksaga(getstate){
    try{
    	yield call(some async op);
    	yield put({type:'SOME ACTION WHEN SUCCESS'});
    }catch(err){
        yield put({type:'SOME ACTION WHEN FAIL'}}})Copy the code
function* rootsaga() {
  yield* takeEvery("SOME ACTION", worksaga);
}
Copy the code

task

Each saga corresponds to a task, which is used to manage the operation of the iterator. There are many types of tasks: Main Task tracks the whole Main Flow; Fork task is the task created by Fork; Parent Task manages the Main task And several Fork Tasks

proc

The main execution logic is focused on proc, which defines the saga middleware to execute Effect logic, obtains function control by taking iterators of Generator functions, and calls iterator.next continuously by iterating between auxiliary functions

channel

Where task callbacks are stored and triggered,channel.take registers the callback, and channel.put matches the callback listening for the current action

(Source code has been deleted)

put(input) {
      const takers = (currentTakers = nextTakers)
      for (let i = 0, len = takers.length; i < len; i++) {
        const taker = takers[i]
        if (taker[MATCH](input)) {
          taker.cancel()
          taker(input)
        }
      }
    },
Copy the code
   take(cb, matcher = matchers.wildcard) {
      cb[MATCH] = matcher
      nextTakers.push(cb)
      cb.cancel = once(() = > {
        remove(nextTakers, cb)
      })
    },
Copy the code

The above list of some concepts is to better understand the following source code interpretation, source code is more complex, coupled with my level is limited, can not speak too fine, also do not need to speak too fine.

The source code

General in accordance with the convention, said before the source code is to review the use, but the previous has said, not familiar with can go to look at it again, here will not say, directly from the entrance to start analysis

The entrance

function sagaMiddleware({ getState, dispatch }) {
    boundRunSaga = runSaga.bind(null, {
      ...options,
      context,
      channel,
      dispatch,
      getState,
      sagaMonitor,
    })

    return next= > action= > {
      if (sagaMonitor && sagaMonitor.actionDispatched) {
        sagaMonitor.actionDispatched(action)
      }
      const result = next(action) // hit reducers
      channel.put(action)
      return result
    }
Copy the code

Saga’s entry point is the two apis that redux passes to the middleware, and there is a layer of Factory outside Function, for decoupling from the environment, which is omitted here. The entry is bound to a boundRunSaga function called by sagamiddleware. run, and then returns a higher-order function whose main logic is that when the action is dispatched, it is normally wrapped by other middleware The dispatch function, also known as the Next function, uses channel.put(action) to wake up the saga middleware’s own logic before returning the result, which is equivalent to opening a stream that handles side effects independently of the Redux stream.

Channel.put (action) triggers callbacks that listen for actions. When are these callbacks registered?

RunSaga and proc

RunSaga is the function called by sagamiddleware. run

In runSaga, we first get the iterator passed to root saga

constiterator = saga(... args);Copy the code

The environment is then generated, which can be thought of as the system environment in which the Task process runs

const env = {
  channel,
  dispatch: wrapSagaDispatch(dispatch),
  getState,
  sagaMonitor,
  onError,
  finalizeRunEffect,
};
Copy the code

It then immediately executes a function that creates a parent task that manages the root saga, monitors the total flow with some control functions such as cancel, executes an iterator to the saga function, and executes further depending on the Effect type returned by the iterator

immediately(() = > {
  const task = proc(
    env,
    iterator,
    context,
    effectId,
    getMetaInfo(saga),
    /* isRoot */ true.undefined
  );
  if (sagaMonitor) {
    sagaMonitor.effectResolved(effectId, task);
  }
  return task;
});
Copy the code

(Source code has been deleted)

export default function proc(env, iterator, parentContext, parentEffectId, meta, isRoot, cont) {
  next.cancel = noop;

  /** Creates a main task to track the main flow */
  const mainTask = { meta, cancel: cancelMain, status: RUNNING };
  /** Creates a new task descriptor for this generator. A task is the aggregation of it's mainTask and all it's forked tasks. **/
  const task = newTask(
    env,
    mainTask,
    parentContext,
    parentEffectId,
    meta,
    isRoot,
    cont
  );

  const executingContext = {
    task,
    digestEffect,
  };
  /** cancellation of the main task. We'll simply resume the Generator with a TASK_CANCEL **/
  function cancelMain() {
    if(mainTask.status === RUNNING) { mainTask.status = CANCELLED; next(TASK_CANCEL); }}/** attaches cancellation logic to this task's continuation this will permit cancellation to propagate down the call chain **/
  if (cont) {
    cont.cancel = task.cancel;
  }

  // kicks up the generator
  next();

  // then return the task descriptor to the caller
  return task;
}
Copy the code

Next and EffectRunner

Next is where the Effect command is executed in Saga Middleware. It finds the corresponding EffectRunner based on the effect. type and executes the Runner function

function runEffect(effect, effectId, currCb) {
  if (is.promise(effect)) {
    resolvePromise(effect, currCb);
  } else if (is.iterator(effect)) {
    // resolve iterator
    proc(env, effect, task.context, effectId, meta, /* isRoot */ false, currCb);
  } else if (effect && effect[IO]) {
    const effectRunner = effectRunnerMap[effect.type];
    effectRunner(env, effect.payload, currCb, executingContext);
  } else {
    // anything else returned as iscurrCb(effect); }}Copy the code

EffectRunnerMap [effect.type] is to locate the runner and execute. See effectRunnerMap

const effectRunnerMap = {
  [effectTypes.TAKE]: runTakeEffect,
  [effectTypes.PUT]: runPutEffect,
  [effectTypes.ALL]: runAllEffect,
  [effectTypes.RACE]: runRaceEffect,
  [effectTypes.CALL]: runCallEffect,
  [effectTypes.CPS]: runCPSEffect,
  [effectTypes.FORK]: runForkEffect,
  [effectTypes.JOIN]: runJoinEffect,
  [effectTypes.CANCEL]: runCancelEffect,
  [effectTypes.SELECT]: runSelectEffect,
  [effectTypes.ACTION_CHANNEL]: runChannelEffect,
  [effectTypes.CANCELLED]: runCancelledEffect,
  [effectTypes.FLUSH]: runFlushEffect,
  [effectTypes.GET_CONTEXT]: runGetContextEffect,
  [effectTypes.SET_CONTEXT]: runSetContextEffect,
};
Copy the code

Just some of the runners that we’re familiar with, let’s look at some of the runners that we’re familiar with

  1. put
function runPutEffect(env, { channel, action, resolve }, cb) {
  /** Schedule the put in case another saga is holding a lock. The put will be executed atomically. ie nested puts will execute after this put has terminated. **/
  asap(() = > {
    let result;
    try {
      result = (channel ? channel.put : env.dispatch)(action);
    } catch (error) {
      cb(error, true);
      return;
    }

    if (resolve && is.promise(result)) {
      resolvePromise(result, cb);
    } else{ cb(result); }});// Put effects are non cancellables
}
Copy the code

This is a direct dispatch(action). The difference between a put(action) and a normal direct dispatch is that it returns an Effect

  1. call
function runCallEffect(env, { context, fn, args }, cb, { task }) {
  // catch synchronous failures; see #152
  try {
    const result = fn.apply(context, args);
    if (is.promise(result)) {
      resolvePromise(result, cb);
      return;
    }
    if (is.iterator(result)) {
      // resolve iterator
      proc(
        env,
        result,
        task.context,
        currentEffectId,
        getMetaInfo(fn),
        /* isRoot */ false,
        cb
      );
      return;
    }
    cb(result);
  } catch (error) {
    cb(error, true); }}Copy the code

The call logic, as you might guess, is to wrap the iterator’s control in cb and wait until promise resolve, which blocks.

  1. take
function runTakeEffect(env, { channel = env.channel, pattern, maybe }, cb) {
  const takeCb = (input) = > {
    if (input instanceof Error) {
      cb(input, true);
      return;
    }
    if(isEnd(input) && ! maybe) { cb(TERMINATE);return;
    }
    cb(input);
  };
  try {
    channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null);
  } catch (err) {
    cb(err, true);
    return;
  }
  cb.cancel = takeCb.cancel;
}
Copy the code

A take is a callback to a channel that waits for the action to arrive

  1. fork

    Forking returns the result first, rather than blocking the iterator, obviously requires a new task to handle the fork process, but does not wait for the task to complete, but returns directly

function runForkEffect(env, { context, fn, args, detached }, cb, { task: parent }) {
  const taskIterator = createTaskIterator({ context, fn, args });
  const meta = getIteratorMetaInfo(taskIterator, fn);

  immediately(() = > {
    const child = proc(
      env,
      taskIterator,
      parent.context,
      currentEffectId,
      meta,
      detached,
      undefined
    );

    if (detached) {
      cb(child);
    } else {
      if (child.isRunning()) {
        parent.queue.addTask(child);
        cb(child);
      } else if (child.isAborted()) {
        parent.queue.abort(child.error());
      } else{ cb(child); }}});// Fork effects are non cancellables
}
Copy the code
  1. takeEvery

TakeEvery is a high-level API, the underlying implementation is take+fork, the source logic is not good to see, the official website example is more clear

export function takeEvery(pattern, saga) {
  function* takeEveryHelper() {
    while (true) {
      yield take(pattern);
      yieldfork(saga); }}return fork(takeEveryHelper);
}
Copy the code

Wait for aciton to be initiated,fork a task, and wait for action to be initiated

conclusion

  1. Redux-saga works independently with a system it has built to deal with side effects
  2. Each saga function represents a set of side effects that need to be done. The Saga function is internally represented as a Task, which listens to the function and mounts cancellation methods at any time. The proc takes care of the execution logic of the Task
  3. Proc is where each task is processed. The iterator for the task is first fetched, and the corresponding EffectRunner execution is selected in the built-in next function based on the Effect returned by each yield statement to complete the blocking logic
  4. An Effect is the minimum unit to run and can be interpreted as an instruction to Saga Middleware
  5. Take Effect registers with the channel and waits for the action to arrive before executing the iterator
  6. Fork Effect generates a new task management and returns immediately, without blocking the iterator
  7. TakeEvery is built on top of fork and take. It waits for action to arrive and forks a task because fork does not block the iterator and can respond to each action

In addition, the cancellation of iterators is a complicated part of Saga, which is not covered here. We will see if there is a chance to issue another issue separately.