- 前端
- UI组件
【免费下载链接】fast
The adaptive interface system for modern web experiences.
SubscriberSet是@microsoft/fast-element响应式观察系统中最核心的订阅管理实现,其构造函数负责把被观察对象(source)与其订阅者(Subscriber)绑定在一起。本篇基于 FAST Element 的 API 参考文档 展开,结合 notifier.ts 源码与 notifier.pw.spec.ts 测试用例,带你完整掌握该构造函数的签名、参数语义,以及它在 FAST Element 模板绑定、属性观察等场景中的底层工作原理。
SubscriberSet 在响应式系统中的定位
在@microsoft/fast-element中,响应式更新的基础是「被观察对象 → Notifier → 订阅者(Subscriber)」三方协作:
- 被观察对象(subject/source):状态发生变化的任何对象;
- Notifier:负责向订阅者广播变化;
- Subscriber:实现
handleChange(source, args)方法的对象,在收到通知后执行具体响应逻辑。
SubscriberSet正是Notifier接口的一个具体实现,官方 API 文档将其描述为:“An implementation of Notifier that efficiently keeps track of subscribers interested in a specific change notification on an observable source.” 即:它为某个特定变化通知高效地维护一份订阅者清单,并在变化发生时逐个通知。
从 notifier.ts 的源码 可以看到,其设计目标是明确的:
This set is optimized for the most common scenario of 1 or 2 subscribers. With this in mind, it can store a subscriber in an internal field, allowing it to avoid Array#push operations. If the set ever exceeds two subscribers, it upgrades to an array automatically.
也就是说,绝大多数真实场景中一个属性的订阅者只有 1~2 个,SubscriberSet为此做了极致的内存与性能优化,这正是本篇文章要重点剖析的内容。
构造函数签名与参数详解
SubscriberSet构造函数的官方签名(见 fast-element.subscriberset.constructor.md)如下:
constructor(source: any, initialSubscriber?: Subscriber);对应参数表:
| 参数 | 类型 | 说明 |
|---|---|---|
source | any | 被观察的对象,订阅者将从这个对象接收通知(即后续handleChange回调中的 subject) |
initialSubscriber | Subscriber | 可选的初始订阅者,构造时即完成第一个订阅 |
对照 notifier.ts 中的实际实现:
export class SubscriberSet implements Notifier { private sub1: Subscriber | undefined = void 0; private sub2: Subscriber | undefined = void 0; private spillover: Subscriber[] | undefined = void 0; public readonly subject: any; public constructor(subject: any, initialSubscriber?: Subscriber) { this.subject = subject; this.sub1 = initialSubscriber; } }两个值得注意的细节:
source 与 subject 的对应关系:API 文档中的参数名是
source,而实现中的只读属性名为subject(fast-element.subscriberset.source.md 也标注该属性为readonly source: any)。二者指向同一个东西——被观察对象本身。构造函数内部把它存入this.subject,供后续notify()广播时作为回调的第一个参数使用。initialSubscriber 的落地方式:初始订阅者被直接存入内部字段
sub1,不需要走subscribe()的去重判断流程。这意味着「先订阅后观察」的常见初始化模式(例如模板绑定建立时先传入首个子订阅者)在构造阶段就完成了,零额外开销。
内部存储结构:为 1~2 个订阅者设计的零数组路径
SubscriberSet的性能秘密在于它的三个内部存储字段(源码位置:notifier.ts):
sub1:第一个订阅者(也是构造函数的initialSubscriber落点);sub2:第二个订阅者;spillover:订阅者数量超过 2 时「升级」出来的数组。
这种设计的直接收益是:在最常见的 1~2 个订阅者场景下,完全不需要分配数组、不需要调用Array#push、不需要索引查找。订阅、退订、通知全部是字段级别的常数时间操作,既省内存又省 GC 压力。
升级机制:订阅者超过两个时自动切换到数组
当第三个订阅者到来时,subscribe()会触发一次自动升级。看 subscribe 的实现:
public subscribe(subscriber: Subscriber): void { const spillover = this.spillover; if (spillover === void 0) { if (this.has(subscriber)) { return; // 已存在,去重直接返回 } if (this.sub1 === void 0) { this.sub1 = subscriber; // 槽位 1 为空,直接占用 return; } if (this.sub2 === void 0) { this.sub2 = subscriber; // 槽位 2 为空,直接占用 return; } // 前两个槽位都满了,升级为数组 this.spillover = [this.sub1, this.sub2, subscriber]; this.sub1 = void 0; this.sub2 = void 0; } else { const index = spillover.indexOf(subscriber); if (index === -1) { spillover.push(subscriber); // 已升级,走数组追加 } } }关键行为可以总结为:
- 去重:无论处于哪个阶段,重复订阅同一订阅者都会被忽略(
has()检查或indexOf检查),确保notify()时每个订阅者只会收到一次通知; - 槽位优先:前两个订阅者直接占用
sub1/sub2,不做数组分配; - 自动升级:第三个订阅者加入时,一次性把前两个订阅者连同新订阅者灌入数组
[sub1, sub2, subscriber],随后清空字段槽位。升级是不可逆的——后续订阅都走数组路径。
相应地,unsubscribe()(notifier.ts 第 130-145 行)在字段阶段直接置空对应槽位,在数组阶段用splice移除;has()(notifier.ts 第 87-91 行)在字段阶段做两个相等性比较,数组阶段做indexOf查找。
通知广播:notify 如何把变化分发给订阅者
notify(args)是订阅集合的最终出口(notifier.ts 第 151-171 行):
public notify(args: any): void { const spillover = this.spillover; const subject = this.subject; if (spillover === void 0) { const sub1 = this.sub1; const sub2 = this.sub2; if (sub1 !== void 0) { sub1.handleChange(subject, args); } if (sub2 !== void 0) { sub2.handleChange(subject, args); } } else { for (let i = 0, ii = spillover.length; i < ii; ++i) { spillover[i].handleChange(subject, args); } } }通知的核心语义是:对每个订阅者调用其handleChange(subject, args)方法,其中subject就是构造函数传入的source对象,args则是本次变化的附加信息(在属性观察场景下通常是属性名)。字段阶段直接展开两个 if 分支,避免了数组遍历和迭代器开销;数组阶段则用经典的for循环顺序遍历。
在 PropertyChangeNotifier 中的应用:按属性分组的订阅集合
SubscriberSet最常见的实际使用场景是作为PropertyChangeNotifier的分组存储。查看 PropertyChangeNotifier 的实现:
export class PropertyChangeNotifier implements Notifier { private subscribers: Record<string, SubscriberSet> = {}; private subjectSubscribers: SubscriberSet | null = null; public readonly subject: any; public constructor(subject: any) { this.subject = subject; } public notify(propertyName: string): void { this.subscribers[propertyName]?.notify(propertyName); this.subjectSubscribers?.notify(propertyName); } public subscribe(subscriber: Subscriber, propertyToWatch?: string): void { let subscribers: SubscriberSet; if (propertyToWatch) { subscribers = this.subscribers[propertyToWatch] ?? (this.subscribers[propertyToWatch] = new SubscriberSet(this.subject)); } else { subscribers = this.subjectSubscribers ?? (this.subjectSubscribers = new SubscriberSet(this.subject)); } subscribers.subscribe(subscriber); } // ... }这里new SubscriberSet(this.subject)体现了构造函数第一个参数的实际传递方式:
- 被观察对象(subject)会被反复传入各个按属性名分组的
SubscriberSet实例,保证所有订阅者在收到通知时拿到的都是同一个来源对象; - 不带属性名订阅(监听对象任意变化)的订阅者进入
subjectSubscribers; - 带属性名订阅(只关心某个属性)的订阅者进入
subscribers[propertyName]对应的集合,实现「按属性隔离、互不干扰」的通知语义。
在 FAST Element 的模板绑定体系里,@observable装饰的属性 setter 在值变化时就会调用getNotifier(source).notify(propertyName)(见 observable.ts),从而驱动依赖该属性的模板节点更新。
继承扩展:表达式通知器如何复用 SubscriberSet
除了直接实例化,SubscriberSet还被用于继承扩展。在 observable.ts 中,FAST Element 的表达式求值器正是通过继承复用订阅集合能力:
class ExpressionNotifierImplementation<TSource = any, TReturn = any> extends SubscriberSet implements ExpressionNotifier<TSource, TReturn> { public needsRefresh: boolean = true; // ... 表达式求值相关字段 constructor( private expression: Expression<TSource, TReturn>, initialSubscriber?: Subscriber, private isVolatileBinding: boolean = false, ) { super(expression, initialSubscriber); } // ... }注意这里super(expression, initialSubscriber)的用法:表达式函数本身被当作source传入。这印证了构造函数第一个参数设计为any的用意——SubscriberSet不关心 source 的具体类型,它只负责把 source 原样传递给订阅者的handleChange回调,因此无论是普通对象还是函数,都能作为被观察来源。同时initialSubscriber参数也让表达式在首次创建时就能附带一个订阅者,省去一次额外的subscribe()调用。
测试验证:1 到 10 个订阅者的全量行为保证
仓库提供了非常完整的单元测试来验证订阅集合的所有行为,见 notifier.pw.spec.ts。测试使用参数化方式,对 1~10 个订阅者的每种数量都运行同一组断言,覆盖四个关键契约:
- 可逐个加入:
subscribe()后has()必然返回true; - 可逐个移除:
unsubscribe()后has()返回false; - 可全部通知:
notify(args)时每个订阅者的handleChange(source, args)收到完全一致的 source 与 args; - 自动去重:同一订阅者重复
subscribe()两次,notify()时其handleChange只被调用一次(invocationCount === 1)。
其中第 3 条直接验证了构造函数参数语义——测试中断言handleChange收到的source与构造时传入的sourceValue严格相等(expect(source).toBe(sourceValue)),这正是source参数在运行时作用的最直接证据。而 1~10 的遍历范围恰好覆盖了「字段阶段(1~2)」「升级临界点(3)」「数组阶段(4~10)」全部内部路径。
完整使用示例:从构造到通知
综合以上分析,一个典型的SubscriberSet使用流程如下:
import { SubscriberSet, type Subscriber } from "@microsoft/fast-element"; // 1. 定义订阅者:实现 handleChange 即可 const subscriber: Subscriber = { handleChange(subject: any, args: any) { console.log(`收到来自 ${subject.name} 的变化:`, args); }, }; // 2. 构造订阅集合,source 是被观察对象 const source = { name: "counter" }; const set = new SubscriberSet(source, subscriber); // initialSubscriber 直接成为第一个订阅者 // 3. 追加订阅者 const anotherSubscriber: Subscriber = { handleChange(subject: any, args: any) { console.log(`第二个订阅者收到:`, args); }, }; set.subscribe(anotherSubscriber); // 4. 广播变化,所有订阅者都会收到同一 source 与 args set.notify("valueChanged"); // 输出: // 收到来自 counter 的变化: valueChanged // 第二个订阅者收到: valueChanged // 5. 退订 set.unsubscribe(subscriber); console.log(set.has(subscriber)); // false在实际项目中,你几乎不需要直接手动创建SubscriberSet——它通常由PropertyChangeNotifier在subscribe()内部自动构造(notifier.ts 第 210-224 行),或者由模板绑定系统在创建表达式通知器时通过继承使用。但理解其构造函数的行为,对于排查模板更新异常、评估@observable属性订阅开销,以及阅读 FAST Element 响应式内核代码,都是必不可少的基础。
小结
SubscriberSet构造函数以(source, initialSubscriber?)两个参数完成了「绑定被观察对象 + 收纳首个订阅者」两件事,并通过sub1/sub2/spillover三段式存储实现了「1~2 个订阅者零数组开销、超过 2 个自动升级为数组」的性能策略。它既是Notifier接口的标准实现,又是PropertyChangeNotifier按属性分组的存储单元,还是表达式通知器的继承基类。相关源码与测试见 packages/fast-element/src/observation/notifier.ts、packages/fast-element/src/observation/notifier.pw.spec.ts 与 packages/fast-element/src/observation/observable.ts,类级 API 参考可继续阅读 fast-element.subscriberset.md 和 fast-element.subscriber.md。
- 前端
- UI组件
【免费下载链接】fast
The adaptive interface system for modern web experiences.
相关推荐
fast-element SubscriberSet.notify() 方法深度解析:FAST 响应式系统的订阅通知分发机制
fast element SubscriberSet.notify 方法深度解析:FAST 响应式系统的订阅通知分发机制 本篇技术指南围绕 @microsoft
前端UI组件深入理解Ultra-Pull-To-Refresh的构造函数与初始化流程
深入理解Ultra Pull To Refresh的构造函数与初始化流程 Ultra Pull To Refresh是Android平台上一款功能强大的下拉刷新
移动开发UI组件FAST Element 样式引擎核心:深入解析 @microsoft/fast-element 的 `css()` 函数与 ElementStyles 机制
FAST Element 样式引擎核心:深入解析 @microsoft/fast element 的 css 函数与 ElementStyles 机制 导读 c
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考