Alien Signals 技术分析之依赖 (Dependency)(六)

8 分钟
deepseek/deepseek-chat-v3-0324 Logo

AI 摘要 (由 deepseek/deepseek-chat-v3-0324 生成)

本文深入解析响应式系统中的依赖(Dependency)机制,重点阐述其作为数据发布者的核心角色。依赖(如Signal和Computed)通过维护订阅者列表(subs)实现变化通知,使用链表结构高效管理订阅关系。文章详细描述了link函数建立依赖关系、propagate函数触发更新的完整流程,并通过代码示例展示了Dependency接口的实现方式。这种机制确保了数据变化时能精准通知所有相关订阅者,是响应式系统高效运作的关键基础。

10.67s
~9748 tokens

在上一章 依赖追踪与链接 (Tracking & Linking) 中,我们了解了响应式系统是如何像侦探一样,自动发现并记录下数据之间的关联。我们看到了 link 函数如何在两个实体之间建立起联系。那么,这些被联系起来的实体,它们各自扮演着什么角色呢?

本章,我们将聚焦于这个连接关系中的一方:那个被依赖的对象。在 alien signals 的世界里,我们称之为 依赖 (Dependency)

什么是依赖?为什么需要它?

想象一下,你正在运营一本非常受欢迎的电子杂志。每当新一期杂志发布时,你需要通知所有订阅了这本杂志的读者。为了做到这一点,你必须维护一份订阅者名单。没有这份名单,你就无法有效地将新内容推送给感兴趣的人。

在响应式系统中,依赖 (Dependency) 就扮演着这本“电子杂志”的角色。它是一个可以被其他单元(我们称之为 订阅者 (Subscriber))追踪或“订阅”的对象。当这个依赖对象的状态发生变化时(比如杂志发布了新一期),它需要知道应该通知谁。

alien signals 中,最典型的依赖就是我们已经学过的:

  • 信号 (Signal):存储基本数据值的“公告板”。
  • 计算属性 (Computed):根据其他依赖计算出新值的“智能菜谱”。

它们都需要能够被追踪,并且在自身变化时通知那些依赖它们的对象。为了实现这一点,每个依赖对象内部都需要维护着一个列表,记录了所有“订阅”了它的 订阅者 (Subscriber)。

没有依赖这个概念和它维护的订阅者列表,依赖追踪与链接 (Tracking & Linking) 就失去了意义,变化通知(propagate)也无从谈起。

依赖的核心职责:维护订阅者列表

依赖最核心的职责就是:知道谁依赖我

它通过内部维护一个列表(通常是一个链表结构,为了高效添加和删除)来实现这一点。这个列表存储了所有当前依赖于这个依赖对象的 订阅者 (Subscriber)。

我们来看看这个列表是如何被使用和维护的:

  1. 被追踪时(link:当一个 订阅者 (Subscriber)(比如一个 effectcomputed)在执行并读取这个依赖的值时,依赖追踪与链接 (Tracking & Linking) 机制会启动。核心函数 link 会被调用,将这个正在读取的订阅者添加到当前依赖对象的订阅者列表中。这就像有新读者订阅了你的杂志,你把他加入了订阅者名单。
  2. 值变化时(propagate:当依赖对象的值发生变化时(比如 signal 被赋予了新值),它需要通知所有关心它变化的订阅者。这时,它会取出自己的订阅者列表,并调用 响应式系统核心 (Reactive System Core) 提供的 propagate 函数。propagate 函数会遍历这个列表中的每一个订阅者,并通知它们:“嘿,你依赖的数据变了,你可能需要更新了!” 这就像杂志社按照订阅者名单,给每个读者发送新刊通知邮件。

重要提示: “依赖 (Dependency)”本身更像是一个角色接口,而不是像 signalcomputed 那样可以通过一个特定函数直接创建出来的东西。我们通常创建的是 signalcomputed,而它们在系统内部扮演了依赖的角色。

依赖的内部实现

理解依赖如何在内部存储和管理订阅者列表,有助于我们更清晰地认识响应式系统的运作方式。

非代码内部流程

让我们回顾一下,当一个副作用 (Effect) 读取一个信号 (Signal) 时,以及之后信号变化时,依赖对象(即信号)内部发生了什么:

场景 1:Effect 读取 Signal

  1. effect 函数开始执行。
  2. startTracking 被调用,全局 activeSub 被设置为这个 effect 对象。
  3. effect 函数内部代码执行 mySignal() 来读取信号值。
  4. mySignal() 的读取逻辑检测到 activeSub 存在。
  5. 调用核心函数 link(mySignalObject, activeEffectObject)
  6. 关键点: link 函数会将 activeEffectObject (订阅者) 添加到 mySignalObject (依赖) 内部维护的订阅者列表 (subs) 中。同时也会建立反向链接。

场景 2:Signal 值变化

  1. 代码执行 mySignal(newValue) 写入新值。
  2. 信号的写入逻辑比较新旧值,发现值已改变。
  3. 信号对象(作为依赖)获取它自己的订阅者列表 (subs)。
  4. 关键点: 调用核心函数 propagate(mySignalObject.subs),将整个订阅者列表传递给 propagate
  5. propagate 遍历这个列表,逐个通知列表中的订阅者(比如之前的 effect)它们需要更新。

简化序列图

这个图清晰地展示了依赖(信号)是如何在被读取时记录订阅者 (subs 被填充),以及在值变化时使用这个 subs 列表来发起通知的。

代码实现:Dependency 接口与对象的结构

alien signals 的源代码 src/system.ts 中,定义了一个 Dependency 接口,它明确了扮演“依赖”角色的对象需要具备哪些属性:

src/system.ts (简化)
text
// src/system.ts (简化)
export interface Dependency {
	subs: Link | undefined;     // 指向订阅者链表的第一个链接 (Link)
	subsTail: Link | undefined; // 指向订阅者链表的最后一个链接 (Link),用于快速添加
}

// Link 对象结构 (简化)
export interface Link {
	sub: Subscriber;            // 指向订阅者对象
	nextSub: Link | undefined; // 指向下一个订阅者链接 (构成链表)
	prevSub: Link | undefined; // 指向上一个订阅者链接 (双向链表)
	// ... 其他字段,如 dep, nextDep ...
}

解释:

  • Dependency 接口要求实现者必须有 subssubsTail 这两个属性。
  • subs 指向一个由 Link 对象构成的链表的头部。每个 Link 对象都包含一个指向实际订阅者 (Subscriber) 对象的引用 (sub)。
  • subsTail 指向这个链表的尾部,这使得 link 函数在添加新的订阅者时,可以直接在链表末尾操作,效率更高。

那么,我们常用的 signalcomputed 是如何满足这个接口,扮演依赖角色的呢?

信号 (Signal) 作为依赖:

当我们调用 signal(initialValue) 时,它内部会创建一个对象,并将其绑定到返回的 signalGetterSetter 函数上。这个内部对象就包含了 Dependency 所需的属性:

src/index.ts (signal 创建简化示意)
typescript
// src/index.ts (signal 创建简化示意)
function signal<T>(initialValue?: T) {
	// 这个对象扮演了 Dependency (+ Signal 特定属性) 的角色
	const signalObject = {
		currentValue: initialValue, // 信号自身的值
		subs: undefined,           // 初始没有订阅者 (符合 Dependency 接口)
		subsTail: undefined,       // 初始订阅者链表尾指针为空 (符合 Dependency 接口)
	};
	return signalGetterSetter.bind(signalObject) as /* ...类型 */;
}

计算属性 (Computed) 作为依赖:

类似地,当我们调用 computed(getterFn) 时,创建的 computedObject 不仅扮演了 订阅者 (Subscriber) 的角色(因为它需要依赖其他东西),它同时也扮演了 Dependency 的角色,因为其他 effectcomputed 可能依赖于这个计算属性的值。

src/index.ts (computed 创建简化示意)
typescript
// src/index.ts (computed 创建简化示意)
function computed<T>(getter: (previousValue?: T) => T): () => T {
	// 这个对象扮演了 Dependency (+ Computed 特定属性 + Subscriber 特定属性) 的角色
	const computedObject: Computed<T> = {
		currentValue: undefined,  // 缓存的计算值
		subs: undefined,          // 初始没有订阅者依赖它 (符合 Dependency 接口)
		subsTail: undefined,      // (符合 Dependency 接口)
		deps: undefined,          // 它依赖的列表 (作为 Subscriber)
		depsTail: undefined,      // (作为 Subscriber)
		flags: /* ... */,       // 状态标记 (作为 Subscriber)
		getter: getter,           // 计算函数 (Computed 特定)
	};
	return computedGetter.bind(computedObject) as () => T;
}

linkpropagate 如何使用 subssubsTail:

  • link(dep, sub):link 函数被调用,dep 参数就是我们的依赖对象(比如 signalObjectcomputedObject)。link 函数会创建一个新的 Link 对象(包含 sub),然后使用 dep.subsTail 找到依赖的订阅者链表末尾,并将新 Link 添加进去,最后更新 dep.subsTail 指向这个新添加的 Link
  • propagate(subs): 当信号或计算属性的值改变后,它会调用 propagate(this.subs)。这里的 this.subs 就是依赖对象内部存储的订阅者链表的头节点。propagate 函数接收到这个链表头节点后,就会沿着 nextSub 指针遍历整个链表,通知每一个链接的 sub (订阅者)。

通过这种方式,每个依赖对象都能有效地维护自己的订阅者列表,并在需要时准确地通知它们。

总结

在本章中,我们聚焦于响应式关系中的一方:依赖 (Dependency)

  • 依赖是在响应式系统中可以被其他单元追踪的对象,典型的例子是 信号 (Signal) 和 计算属性 (Computed)。
  • 它的核心职责是维护一个订阅者列表 (subs),记录所有依赖于它的 订阅者 (Subscriber)。
  • 这个列表在 依赖追踪与链接 (Tracking & Linking) 过程中由 link 函数填充。
  • 当依赖自身的值发生变化时,它会使用这个列表,通过 propagate 函数来通知所有相关的订阅者。
  • 依赖对象内部通常通过链表(subs 指向头,subsTail 指向尾)来高效地管理订阅者。

理解了“依赖”这个角色,我们就能更好地明白数据源是如何知道在变化时应该通知谁的。它就像响应式系统中的信息发布者,时刻准备着向它的听众广播最新消息。

下一步

我们已经探讨了信息发布者(依赖)。那么,信息的接收者是谁呢?那个依赖于“依赖”并对它的变化做出反应的单元是什么?

在下一章,我们将深入探讨响应式连接的另一端:订阅者 (Subscriber)。我们将了解它的作用、类型(如 ComputedEffect 如何扮演订阅者角色)以及它是如何被依赖通知并执行相应更新的。