•
阅读时长 2 分钟

观察 Redux 中间件里的 dispatch 和 next

目录

Redux 的 reducer 接收普通 action,为什么有些项目却可以 dispatch 一个函数,还能 await 它?这不是 reducer 学会了异步,而是中间件在前面接住了函数。

中间件里的 dispatch 和 next 很像,但一个重新进入链路,一个继续往后走。用两次加一就能观察出区别。

运行一个只有 thunk 和 logger 的 store

创建空目录,执行 npm init -y、npm install —save-exact redux@4.2.1。把代码存为 flow.mjs,运行 node flow.mjs:

import redux from "redux";
import assert from "node:assert/strict";
const { createStore, applyMiddleware } = redux;
const events = [];
const thunk =
  ({ dispatch, getState }) =>
  (next) =>
  (action) => {
    if (typeof action === "function") return action(dispatch, getState);
    return next(action);
  };
const logger = () => (next) => (action) => {
  events.push(action.type);
  return next(action);
};
const store = createStore(
  (state = 0, action) => (action.type === "add" ? state + 1 : state),
  applyMiddleware(thunk, logger),
);
const result = await store.dispatch(async (dispatch) => {
  dispatch({ type: "add" });
  await Promise.resolve();
  dispatch({ type: "add" });
  return "done";
});
assert.equal(result, "done");
assert.equal(store.getState(), 2);
assert.deepEqual(events, ["add", "add"]);
await assert.rejects(
  store.dispatch(async () => {
    throw new Error("请求失败");
  }),
  /请求失败/,
);
console.log({ result, state: store.getState(), events });

输出 result 为 done,state 为 2,events 是两个 add。失败的异步函数也能由调用方捕获,检查通过后不会额外输出错误。

一次 dispatch 怎样经过中间件

外面的函数 action 先到 thunk。thunk 调用这个函数,没有继续 next,所以 logger 和 reducer 不会收到函数本身。

函数内部 dispatch 的普通对象会重新经过 thunk,随后交给 next,再到 logger,最后到 reducer。这就是两个 add 都进入日志的原因。

如果把 logger 放在 thunk 前面,它还会看到函数 action。中间件顺序会改变观察结果。再比如在中间件里无条件 dispatch(action),会重新进入自己,可能形成无限循环。

return 也很重要。thunk 返回函数的结果,logger 返回 next 的结果,外部才拿得到 Promise。如果中间某一层丢掉 return,异步任务仍可能运行,但调用方已经不知道什么时候完成。

异步入口有了,竞态还在

reducer 仍然只计算状态变化,不启动请求。请求放进 thunk,并不会自动解决过期响应覆盖新响应的问题,需要请求标识、取消,或者在 reducer 中判断结果对应的请求是否仍有效。

实际项目可以用 Redux Toolkit 的默认中间件,不需要照着这个例子重新造一套。把 next、dispatch 和返回值的关系弄清楚,排查现有异步流程就容易一些。接口解释参考 Redux Middleware 文档。