Code前端首页关于Code前端联系我们

Redux-thunk&redux-saga(2)

terry 2年前 (2023-09-08) 阅读数 147 #Vue

redux-saga-profile

​ redux-saga,一个响亮的名字,虽然在上一篇文章中介绍过,但是看完源码我还是忍不住郑重的重新介绍一下。这是管理计划的“副作用”。虽然该框架大多数情况下都是作为redux中间件使用的,但从基本使用上来说,它并不依赖于其他库,可以单独使用。使用它来管理程序副作用具有以下好处:

  1. 更好的测试
  2. 更清晰的代码逻辑
  3. 轻松管理副作用的激活和取消

PS:这个库的代码写得真的很好。充满了计算机专业术语,让我感觉舒服又熟悉。 Fork、通道、任务、io概念、信号量、缓冲区,仅基于概念层,saga胜太多

一些概念

效果

官网已经多次强调,Effect是saga中间件的执行单元。它是通过内部 API 生成的。它是一个简单的对象,包含一些信息,例如类型属性。根据这些信息,saga中间件可能会选择向下执行。阻止、采取行动等操作(take、分叉、放)

传奇

什么是saga,我个人的看法是它是效果的集合,可以分为work saga和root saga。 root saga 负责分发操作,worker saga 负责响应特定操作。两者之间的组合嵌套可以基于操作的生成器函数语法

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'})
    }
}
 
function* rootsaga() {
  yield* takeEvery("SOME ACTION", worksaga);
}
 

任务

​ 每个 saga 对应一个任务,用于管理迭代器操作。任务有很多种类型。 Main任务主要遵循整个Main流程。 Fork任务是通过fork创建的任务。父任务管理主任务和各种分支。

任务

过程

主要执行逻辑集中在proc中,它定义了saga中间件逻辑来执行Effect。从 Generator 函数中获取迭代器可以控制函数,并利用辅助函数之间的相互迭代来不断调用 iterator.next

频道

保存任务回调并触发任务回调,channel.take记录回调,channel.put匹配并监听当前action的回调,触发

(源码已删除)

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)
        }
      }
    },
 
   take(cb, matcher = matchers.wildcard) {
      cb[MATCH] = matcher
      nextTakers.push(cb)
      cb.cancel = once(() => {
        remove(nextTakers, cb)
      })
    },
 

上面列出一些概念是为了更好的理解下面源码的解读,源码比较复杂加上本人水平有限,不可能解释的太详细,也没有必要解释太多详细解释。

源代码

​ 一般来说,按照惯例,使用前应该先检查一下源代码,但这在上一篇文章中已经提到过。如果你不熟悉,你可以读一遍再读一遍。这里就不讲了,直接从入口开始分析

入口

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
    }
 

​ saga入口仍然接收redux传递给中间件的两个API。事实上,外面有一层工厂函数将自身与环境解耦。这里就省略了。入口与 boundRunSaga 功能相关。就是sagaMiddleware.run调用的函数,返回的高阶函数的主要逻辑是,当action被dispatch时,照常调用其他中间件封装的dispatch函数,即下面的函数,但是之前返回结果,使用channel.put(action)唤醒saga。中间件本身的逻辑相当于打开一个流程来独立于redux流程来处理副作用。

​ 如前所述,

激活监视操作的回调。这些回调请求是什么时候注册的?

runSaga 和 proc

​ runSaga 是 sagaMiddleware.run 调用的函数

在runSaga中,首先获取传递给根saga的迭代器

const iterator = saga(...args);
 

然后生成环境,可以认为是任务流程执行的系统环境

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

然后立即运行一个函数,该函数用于创建父任务来管理 root saga 并监控整体流程。它上面有一些控制功能,比如取消,然后执行saga函数迭代器并根据迭代器返回。效果类型继续执行

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

(源码已删除)

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

next 和 EffectRunner

​ 之后,Effect 命令会在 saga 中间件中执行。它会根据Effect.type类型找到对应的EffectRunner并运行这个runner函数

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 is
    currCb(effect);
  }
}
 

其中effectRunnerMap[effect.type]就是找到主教并处决,见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,
};
 

是我们所知道的一些效果类型对应的跑步者,我们来看看一些常见的跑步者

显然是直接send(action),与常规直接send的区别在于put(action)返回一个效果,这对于测试很有用

  1. 打电话
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);
  }
}
 

​ 调用的逻辑可以猜到,即把迭代器的控制包装在cb中,然后在promise解决的时候执行,相当于阻塞。

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

​ take正在等待行动的到来。据说这是控制权的逆转。基本上它的目的是在通道中注册take的回调并等待操作到达时唤醒通道

  1. 叉子

    fork 首先返回结果,而不是阻塞迭代器的执行。显然需要一个新的任务来负责fork过程,但是它并没有等待任务完成,而是立即返回

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
}
 
  1. take每

take Every 是高层API,底层实现是take+fork,源码逻辑不太好看,官网上的例子比较清晰

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

​ 等待aciton启动,分配任务执行并继续监听action的启动,无需等待返回

总结

  1. redux-saga通过自己搭建的一系列副作用处理系统独立工作
  2. 每个 saga 函数都代表一些需要执行的副作用。 saga 函数在内部表示为任务。该任务负责监视函数的执行,并具有随时执行与其关联的取消的方法。任务执行逻辑由proc
  3. 处理
  4. proc 是处理每个任务的地方。首先,获取任务的迭代器。内置的next函数中根据每个yield语句返回的Effect选择对应的EffectRunner执行,完成阻塞逻辑
  5. Effect是最小的操作单元,可以理解为传递给saga中间件的指令
  6. take Effect 注册到通道,等待操作到达,然后执行迭代器
  7. fork效果生成新的任务管理器并立即返回,不会阻塞迭代器
  8. take 一切都建立在 fork 和 take 之上。它等待操作的到来并分出要执行的任务。由于 fork 不会阻塞迭代器,因此它可以响应任何操作

另外,取消迭代器是saga中相对复杂的部分,这里不再赘述。我会看看将来是否有可能单独出版一期。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门