Flux Dispatcher 完全指南:从 API 到 waitFor 依赖编排原理
2026/9/21 2:05:32 网站建设 项目流程

Flux Dispatcher 完全指南:从 API 到 waitFor 依赖编排原理

【免费下载链接】fluxApplication Architecture for Building User Interfaces项目地址: https://gitcode.com/gh_mirrors/fl/flux

Flux 是一套用于构建用户界面的单向数据流应用架构,而Dispatcher是整个架构中唯一不依赖 React、也不依赖任何第三方库的"枢纽"组件——所有 Action 都经由它广播给各个 Store 的回调函数。本指南以仓库中的 docs/Dispatcher.md 为骨架,结合 Dispatcher.js 源码与 Dispatcher-test.js 测试用例,深入讲解 Dispatcher 的 API、waitFor()依赖编排机制,以及它与 Store 协作的底层原理,读完即可在自己的 Flux 应用中正确使用 Dispatcher 并规避循环依赖等经典陷阱。

Dispatcher 是什么:与通用发布/订阅系统的本质区别

Dispatcher用于把 payload(载荷)广播给所有已注册的回调函数。它和通用的 pub-sub(发布/订阅)系统有两点本质区别:

  1. 回调不订阅特定事件:每个 payload 都会被派发给每一个已注册的回调。也就是说,回调无法"只关心某类事件",而是必须自己判断 payload 是否与自己相关。
  2. 回调可以被整体或部分地延迟执行:通过waitFor(),一个回调可以等待其他回调执行完毕后再继续,从而在多个 Store 之间建立确定的依赖顺序。

从源码结构看,Dispatcher.js 是一个泛型类Dispatcher<TPayload>,内部维护了_callbacks_isDispatching_isHandled_isPending四张状态表,这正是它区别于普通事件发射器的核心数据结构。

为什么需要这种设计?在 Flux 中,一个用户操作往往需要多个 Store 同时响应,而这些 Store 之间存在数据依赖。例如"选择国家后自动选择默认城市、再计算航班价格",如果不保证执行顺序,价格计算就可能读到过期的城市数据。Dispatcher 的waitFor()正是为了解决这种"依赖更新"的问题而生的。

API 全景

Dispatcher 暴露了 5 个核心方法,文档中的完整签名如下:

方法签名说明
registerregister(function callback): string注册一个回调,每次 dispatch 时都会被调用。返回一个 token(令牌),可用于waitFor()
unregisterunregister(string id): void根据 token 移除一个已注册的回调
waitForwaitFor(array<string> ids): void等待指定 token 对应的回调执行完毕,再继续执行当前回调。只能在回调响应某次 dispatch 的过程中调用
dispatchdispatch(object payload): void向所有已注册回调派发一个 payload
isDispatchingisDispatching(): boolean查询 Dispatcher 当前是否正处于派发过程中

register:token 的生成规则

从 Dispatcher.js 可以看到 token 的生成实现:

var _prefix = 'ID_'; register(callback) { var id = _prefix + this._lastID++; // 例如 'ID_1'、'ID_2'、'ID_3' ... this._callbacks[id] = callback; return id; }

_lastID从 1 开始自增,所以第一次注册返回的 token 是'ID_1',第二次是'ID_2',依此类推。token 的类型在源码中被定义为DispatchToken = string(见 Dispatcher.js),它只是一个字符串标识,不是Promise 或句柄。

unregister:按 token 移除回调

Dispatcher.js 中的实现使用了invariant做防御性校验:如果传入的 token 没有对应任何已注册回调,会直接抛出错误'Dispatcher.unregister(...):does not map to a registered callback.'。移除后,该回调在后续 dispatch 中不会再被调用(测试用例'should properly unregister callbacks'验证了这一点,见 Dispatcher-test.js)。

实战示例:航班目的地表单

文档用"航班目的地表单"这一假想场景完整演示了 Dispatcher 的核心用法。假设表单在选中国家后自动选择该国家的默认城市,并根据国家 + 城市计算基础票价。

第一步:创建 Dispatcher 与 Store

var flightDispatcher = new Dispatcher(); // 记录当前选中的国家 var CountryStore = {country: null}; // 记录当前选中的城市 var CityStore = {city: null}; // 记录当前选中城市的基础票价 var FlightPriceStore = {price: null};

注意:这里的CountryStore等只是普通 JavaScript 对象,用于演示 Dispatcher 的机制。在真实 Flux 应用中,Store 通常基于EventEmitter或仓库提供的FluxStore基类构建,并在构造时通过dispatcher.register(...)注册回调(详见下文"与 FluxStore 的协作")。

第二步:派发"城市更新" payload

用户更改了选中的城市后,通过dispatch广播:

flightDispatcher.dispatch({ actionType: 'city-update', selectedCity: 'paris', });

该 payload 被CityStore消化:

flightDispatcher.register(function (payload) { if (payload.actionType === 'city-update') { CityStore.city = payload.selectedCity; } });

注意这里体现了 Dispatcher 的第一个特性:这个回调虽然只关心city-update,但它每次 dispatch 都会被调用,只是通过if判断主动忽略了不相关的 payload。

第三步:派发"国家更新" payload

flightDispatcher.dispatch({ actionType: 'country-update', selectedCountry: 'australia', });

该 payload 同时被两个 Store 消化:

CountryStore.dispatchToken = flightDispatcher.register(function (payload) { if (payload.actionType === 'country-update') { CountryStore.country = payload.selectedCountry; } });

注册CountryStore回调时,把返回的 token 保存在CountryStore.dispatchToken上。有了这个 token,后续回调就可以用waitFor()声明自己对它的依赖。

第四步:用 waitFor 保证依赖顺序

CityStore的回调需要根据新国家计算默认城市时,它必须先确认CountryStore已经更新完毕:

CityStore.dispatchToken = flightDispatcher.register(function (payload) { if (payload.actionType === 'country-update') { // 注意:此时 CountryStore.country 可能尚未更新! flightDispatcher.waitFor([CountryStore.dispatchToken]); // 执行到这里时,CountryStore.country 已保证被更新 // 为新国家选择默认城市 CityStore.city = getDefaultCityForCountry(CountryStore.country); } });

waitFor([CountryStore.dispatchToken])的含义是:暂停当前回调的执行,先确保CountryStore的回调已经执行完毕,再继续。这就是 Dispatcher 的第二个特性——回调可以被整体或部分地延迟。

waitFor 可以链式调用

依赖是可以叠加的。FlightPriceStore的价格计算同时依赖国家和城市,可以这样写:

FlightPriceStore.dispatchToken = flightDispatcher.register(function (payload) { switch (payload.actionType) { case 'country-update': case 'city-update': flightDispatcher.waitFor([CityStore.dispatchToken]); FlightPriceStore.price = getFlightPriceStore( CountryStore.country, CityStore.city, ); break; } });

最终,country-updatepayload 会保证CountryStoreCityStoreFlightPriceStore的顺序依次调用各 Store 注册的回调。

waitFor 的底层原理:pending / handled 状态机

waitFor之所以能"保证顺序",靠的是 Dispatcher 内部维护的_isPending_isHandled两张状态表,以及_invokeCallback的记账逻辑。整个 dispatch 周期分为三个阶段(见 Dispatcher.js):

  1. _startDispatching(payload):把每个回调的_isPending_isHandled重置为false,保存_pendingPayload,并将_isDispatching置为true
  2. 遍历_callbacks执行:对每个未被标记为 pending 的回调调用_invokeCallback(id)_invokeCallback会先把该回调标记为_isPending[id] = true,再执行回调,最后标记_isHandled[id] = true(见 Dispatcher.js)。
  3. _stopDispatching():删除_pendingPayload,把_isDispatching恢复为false。这一步骤放在finally块中,即使回调抛异常也会执行,确保 Dispatcher 不会卡死在"派发中"状态。

waitFor的执行逻辑(见 Dispatcher.js)正是基于这两张表:

waitFor(ids) { for (var ii = 0; ii < ids.length; ii++) { var id = ids[ii]; if (this._isPending[id]) { // 目标回调已经(或正在)执行:若它还没执行完,说明形成循环依赖,直接抛错 invariant( this._isHandled[id], 'Dispatcher.waitFor(...): Circular dependency detected while waiting for `%s`.', id, ); continue; // 已执行完毕,无需再等待 } invariant( this._callbacks[id], 'Dispatcher.waitFor(...): `%s` does not map to a registered callback.', id, ); this._invokeCallback(id); // 立即同步执行目标回调 } }

这段代码揭示了几个关键事实:

  • waitFor同步递归执行目标回调,而不是异步等待。因此调用链上不存在"微任务/宏任务"调度,顺序是确定性的。
  • 如果一个目标回调已经被执行完毕_isPending为 true 且_isHandled为 true),waitFor直接continue跳过,不会重复执行——这正是多个回调对同一 store 调用waitFor不会重复更新的原因。
  • 如果一个目标回调正在执行中_isPending为 true 但_isHandled为 false),说明出现了循环等待,直接抛出'Circular dependency detected'错误。

三个必须遵守的约束(附源码级验证)

Dispatcher 对使用方式有硬性约束,违反即抛错。这些行为都被测试用例逐一验证过:

1. 禁止在 dispatch 过程中再次 dispatch。

dispatcher.register((payload) => { dispatcher.dispatch(payload); // 抛错! });

Dispatcher.js 中dispatch首先检查_isDispatching,为 true 时抛出'Cannot dispatch in the middle of a dispatch.'。对应测试:'should throw if dispatch() while dispatching'(Dispatcher-test.js)。

2. 禁止在非派发状态下调用waitFor

dispatcher.waitFor([tokenA]); // 抛错:Must be invoked while dispatching.

waitFor的第一行 invariant 就是检查_isDispatching(见 Dispatcher.js)。对应测试:'should throw if waitFor() while not dispatching'(Dispatcher-test.js)。

3. 禁止形成循环依赖。

包括"自我等待"和"互相等待"两种形态:

// 自我循环:A 等待 A const tokenA = dispatcher.register((payload) => { dispatcher.waitFor([tokenA]); // 抛错:Circular dependency detected }); // 相互循环:A 等待 B,B 又等待 A const tokenA = dispatcher.register((payload) => { dispatcher.waitFor([tokenB]); }); const tokenB = dispatcher.register((payload) => { dispatcher.waitFor([tokenA]); });

对应测试:'should throw on self-circular dependencies''should throw on multi-circular dependencies'(Dispatcher-test.js)。循环依赖检测正是通过上述 pending/handled 状态机在运行时完成的。

此外还有两个值得注意的边界行为:

  • 失败的 dispatch 不会破坏状态一致性:即使某个回调抛异常,finally块中的_stopDispatching()也会执行,后续 dispatch 依然可用。测试'should remain in a consistent state after a failed dispatch'(Dispatcher-test.js)验证了这一点。
  • waitFor传入不存在的 token 会抛错:对应测试'should throw if waitFor() with invalid token'(Dispatcher-test.js)。

与 FluxStore / ReduceStore 的协作:dispatchToken 从哪来

文档示例中dispatchToken是手动保存的,而真实 Flux 应用中,token 通常由 Store 基类自动管理。看 FluxStore.js 的构造函数:

constructor(dispatcher) { ... this.__dispatcher = dispatcher; this._dispatchToken = dispatcher.register((payload) => { this.__invokeOnDispatch(payload); }); } getDispatchToken() { return this._dispatchToken; // 供其他 Store 的 waitFor 使用 }

也就是说,每个基于FluxStore创建的 Store 在实例化时就会把自己的分发回调注册进 Dispatcher,并通过getDispatchToken()暴露 token。于是 Store 之间的依赖可以写成:

class CityStore extends FluxStore { __onDispatch(payload) { if (payload.actionType === 'country-update') { this.getDispatcher().waitFor([CountryStore.getDispatchToken()]); this._city = getDefaultCityForCountry(CountryStore.getCountry()); } } }

从源码结构看,FluxStore__invokeOnDispatch(FluxStore.js)会在每轮 dispatch 开始时重置__changed,并在子类处理完 payload 后通过 EventEmitter 广播change事件——这就是"Store 变更通知视图"的底层机制,而这一切的入口都始于 Dispatcher 的那次register

在项目中使用 Dispatcher

通过 npm 安装

Flux 以 npm 模块发布,在package.json中添加依赖或直接运行npm install flux即可。安装后通过命名空间访问:

const Dispatcher = require('flux').Dispatcher;

从仓库构建

克隆本仓库并进入flux目录后运行npm install,Gulp 构建任务会自动生成Flux.js文件,之后可以这样引入:

const Dispatcher = require('path/to/this/directory/Flux').Dispatcher;

构建过程还会在lib目录下生成去掉语法糖的Dispatcherinvariant模块,可以直接拷贝到任何目录单独使用——仓库中的flux-todomvc等示例应用就是这么做的。

推荐的单例模式

在实际应用中,整个应用通常共享一个Dispatcher 实例。仓库示例的写法可供参考:

// examples/flux-todomvc/src/data/TodoDispatcher.js import {Dispatcher} from 'flux'; export default new Dispatcher();
// examples/flux-flow/src/AppDispatcher.js(带 Flow 泛型约束) import type {Action} from './AppActions'; import {Dispatcher} from 'flux'; const dispatcher: Dispatcher<Action> = new Dispatcher(); export default dispatcher;

之所以强调单例,是因为如果多个模块各自new Dispatcher(),Store 之间就无法通过waitFor建立依赖(token 属于不同实例,互不可见),整个数据流的确定性顺序也就无从谈起。

小结

Flux 的 Dispatcher 用不到两百行代码实现了三个关键能力:全量广播(每个 payload 到达每个回调)、依赖编排waitFor保证执行顺序)、运行时防护(禁止嵌套 dispatch、禁止非派发期调用 waitFor、检测循环依赖)。理解它的核心在于记住_isPending/_isHandled状态机:waitFor是同步递归、天然去重、且能识别循环的。若想深入验证这些行为,可以直接阅读 Dispatcher.js 源码,并运行仓库中的 Dispatcher-test.js 测试套件——十个测试用例覆盖了从基本广播到失败恢复的全部边界场景。

【免费下载链接】fluxApplication Architecture for Building User Interfaces项目地址: https://gitcode.com/gh_mirrors/fl/flux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询