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(发布/订阅)系统有两点本质区别:
- 回调不订阅特定事件:每个 payload 都会被派发给每一个已注册的回调。也就是说,回调无法"只关心某类事件",而是必须自己判断 payload 是否与自己相关。
- 回调可以被整体或部分地延迟执行:通过
waitFor(),一个回调可以等待其他回调执行完毕后再继续,从而在多个 Store 之间建立确定的依赖顺序。
从源码结构看,Dispatcher.js 是一个泛型类
Dispatcher<TPayload>,内部维护了_callbacks、_isDispatching、_isHandled、_isPending四张状态表,这正是它区别于普通事件发射器的核心数据结构。
为什么需要这种设计?在 Flux 中,一个用户操作往往需要多个 Store 同时响应,而这些 Store 之间存在数据依赖。例如"选择国家后自动选择默认城市、再计算航班价格",如果不保证执行顺序,价格计算就可能读到过期的城市数据。Dispatcher 的waitFor()正是为了解决这种"依赖更新"的问题而生的。
API 全景
Dispatcher 暴露了 5 个核心方法,文档中的完整签名如下:
| 方法 | 签名 | 说明 |
|---|---|---|
register | register(function callback): string | 注册一个回调,每次 dispatch 时都会被调用。返回一个 token(令牌),可用于waitFor() |
unregister | unregister(string id): void | 根据 token 移除一个已注册的回调 |
waitFor | waitFor(array<string> ids): void | 等待指定 token 对应的回调执行完毕,再继续执行当前回调。只能在回调响应某次 dispatch 的过程中调用 |
dispatch | dispatch(object payload): void | 向所有已注册回调派发一个 payload |
isDispatching | isDispatching(): 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 会保证按CountryStore→CityStore→FlightPriceStore的顺序依次调用各 Store 注册的回调。
waitFor 的底层原理:pending / handled 状态机
waitFor之所以能"保证顺序",靠的是 Dispatcher 内部维护的_isPending和_isHandled两张状态表,以及_invokeCallback的记账逻辑。整个 dispatch 周期分为三个阶段(见 Dispatcher.js):
_startDispatching(payload):把每个回调的_isPending和_isHandled重置为false,保存_pendingPayload,并将_isDispatching置为true。- 遍历
_callbacks执行:对每个未被标记为 pending 的回调调用_invokeCallback(id)。_invokeCallback会先把该回调标记为_isPending[id] = true,再执行回调,最后标记_isHandled[id] = true(见 Dispatcher.js)。 _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目录下生成去掉语法糖的Dispatcher与invariant模块,可以直接拷贝到任何目录单独使用——仓库中的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),仅供参考