Alien Signals 技术分析之依赖追踪与链接 (Tracking & Linking)(五)
AI 摘要 (由 deepseek/deepseek-chat-v3-0324 生成)
本文深入解析响应式系统的核心机制——依赖追踪与链接(Tracking & Linking),通过startTracking、link和endTracking三个核心函数实现自动依赖管理。系统在执行计算属性或副作用时启动追踪,通过activeSub标记当前订阅者,在读取信号时建立双向依赖关系,并在执行结束后清理无效依赖。这种动态构建的依赖图使系统能精确识别数据关系,为后续变更通知奠定基础,是响应式编程自动化的关键所在。
在上一章 响应式系统核心 (Reactive System Core) 中,我们了解了协调整个响应式流程的幕后引擎。我们提到了像 link
、startTracking
和 endTracking
这样的核心函数,它们是实现自动化的关键。但它们具体是如何工作的呢?系统究竟是如何知道“谁依赖谁”的?
本章,我们将深入探讨响应式系统中最神奇的部分之一:依赖追踪与链接 (Tracking & Linking)。这正是 alien signals
如何动态地发现和记录数据之间关系的过程。
侦探出动:为何需要自动追踪?
让我们再次回到熟悉的例子:
import { signal, computed, effect } from './src/index.js';
const firstName = signal("张");
const lastName = signal("三");
// 计算属性 fullName 依赖 firstName 和 lastName
const fullName = computed(() => {
console.log("计算全名...");
return firstName() + lastName(); // 读取 firstName 和 lastName
});
// 副作用 greetingEffect 依赖 fullName
effect(() => {
console.log("打招呼:", "你好, " + fullName() + "!"); // 读取 fullName
});
当我们执行 firstName("李")
时,我们期望 fullName
会自动更新,并且 greetingEffect
会重新执行并打印出 "你好, 李三!"。但是,系统是怎么知道 firstName
的变化需要通知 fullName
,而 fullName
的更新(即使它没有直接变化,只是重新计算了)又需要触发 greetingEffect
呢?
如果系统不知道这些依赖关系,那它就无法进行精确的通知和更新。就像一个没有线索的侦探,无法破案一样。
依赖追踪与链接 就是 alien signals
响应式系统扮演“侦探”角色的过程:
- 追踪 (Tracking): 当一个订阅者(比如
fullName
这个 计算属性 (Computed) 或greetingEffect
这个 副作用 (Effect))开始执行它的计算或副作用函数时,系统会启动“监视模式”。 - 发现 (Discovery): 在执行过程中,该订阅者会读取其他响应式源(比如
firstName()
或fullName()
)。系统会“看到”这些读取操作。 - 链接 (Linking): 系统会记录下:“哦,
fullName
读取了firstName
和lastName
”以及“greetingEffect
读取了fullName
”。它会在这些数据源和读取它们的订阅者之间建立一个明确的联系档案。
这个过程是全自动的,我们不需要手动声明这些依赖关系。系统在运行时动态地构建出这张“关系网”或称为依赖图 (Dependency Graph)。
追踪与链接的核心概念
这个“侦探”工作主要依赖于三个核心操作,这些操作由我们在上一章提到的 响应式系统核心 (Reactive System Core) 提供:
1. 开始追踪 (startTracking
)
这就像侦探开始对目标人物(订阅者)进行跟踪。
- 何时发生? 当一个 计算属性 (Computed) 需要重新计算其值时(在
updateComputed
内部),或者一个 副作用 (Effect) 被触发执行其函数时(在effect
创建时或notifyEffect
内部)。 - 做什么?
- 设置一个全局变量
activeSub
,让它指向当前正在执行的这个订阅者(Computed 或 Effect 对象)。这相当于告诉系统:“现在是 X 在活动,注意他接触了什么!” - 准备清理该订阅者旧的依赖列表(
deps
)。因为这次重新执行可能会读取不同的依赖项,旧的依赖关系可能不再有效。 - 设置一个
Tracking
标志到订阅者对象上,表示它正处于依赖收集状态。
- 设置一个全局变量
2. 读取与链接 (link
)
当目标人物(activeSub
)接触到一个线索(读取一个 信号 (Signal) 或 计算属性 (Computed))时,侦探需要记录下来并建立档案。
- 何时发生? 当
activeSub
存在(即系统处于追踪模式下)时,并且代码执行了someSignal()
或someComputed()
这样的读取操作。 - 做什么?
- 读取操作(比如
signalGetterSetter
或computedGetter
)会检查全局的activeSub
是否有值。 - 如果有,就调用核心的
link(dependency, subscriber)
函数(dependency
是被读取的信号或计算属性对象,subscriber
就是activeSub
)。 link
函数负责建立双向链接:- 将
subscriber
添加到dependency
的订阅者列表 (subs
) 中。(记录下:“这个信号被这个订阅者关注了”) - 将
dependency
添加到subscriber
的依赖列表 (deps
) 中。(记录下:“这个订阅者依赖于这个信号”)
- 将
- 这样,依赖图中的一条边就建立起来了。
- 读取操作(比如
3. 结束追踪 (endTracking
)
侦探完成了本次跟踪任务。
- 何时发生? 在订阅者的函数执行完毕后(比如
updateComputed
或notifyEffect
的finally
块中)。 - 做什么?
- 清理旧依赖: 这是
endTracking
的一个重要工作。在startTracking
时,系统准备清空旧依赖。在追踪过程中,每次link
操作都会标记一个依赖是“新的”或“仍然有效”。endTracking
会检查哪些旧的依赖在这次执行中没有被再次link
,并将这些无效的链接从双方(依赖的subs
和订阅者的deps
)中断开。这确保了依赖关系总是最新的。这个清理工作通常由内部的clearTracking
函数完成。 - 清除之前设置的
Tracking
标志。 - 恢复全局的
activeSub
到它之前的值(可能为undefined
或外层的另一个订阅者)。
- 清理旧依赖: 这是
工作流程:探索如何建立档案
让我们通过 fullName
首次计算的场景,看看这个过程是如何运作的:
- 触发计算: 当代码首次读取
fullName()
时,computedGetter
被调用。 - 检查状态:
fullName
发现自己是Dirty
(需要计算)。 - 准备追踪:
processComputedUpdate
调用updateComputed
,接着调用startTracking(fullName)
。 activeSub
被设置为fullName
对象。fullName
的旧deps
准备被清理。fullName
被标记为Tracking
。
- 执行 Getter: 系统开始执行
fullName
的计算函数:() => firstName() + lastName()
。 - 读取
firstName()
: firstName
的signalGetterSetter
被调用(无参数读取模式)。- 它检测到
activeSub
是fullName
(非undefined
)。 - 调用
link(firstName, fullName)
。 fullName
被添加到firstName
的subs
列表。firstName
被添加到fullName
的deps
列表,并被标记为有效。
- 读取
lastName()
: - 类似地,
lastName
的signalGetterSetter
检测到activeSub
是fullName
。 - 调用
link(lastName, fullName)
。 fullName
被添加到lastName
的subs
列表。lastName
被添加到fullName
的deps
列表,并被标记为有效。
- 类似地,
- Getter 执行完毕: 计算函数返回结果 "张三"。
- 结束追踪:
updateComputed
的finally
块调用endTracking(fullName)
。 clearTracking
运行:由于firstName
和lastName
都被标记为有效,没有旧的、无效的依赖需要清理。fullName
的Tracking
标志被移除。activeSub
恢复为之前的值(在这个例子中可能是undefined
)。
- 缓存与返回:
fullName
缓存结果 "张三",清除Dirty
标记,并返回值。
现在,系统已经成功记录了:fullName
依赖于 firstName
和 lastName
。如果之后 firstName
变化,propagate
就知道要通知 fullName
了。
同样的过程也发生在 greetingEffect
首次执行时,它会通过 startTracking(greetingEffect)
、读取 fullName()
时调用 link(fullName, greetingEffect)
、以及最后的 endTracking(greetingEffect)
,建立起 fullName
到 greetingEffect
的依赖链接。
内部机制:追踪与链接的代码实现
让我们简单看下 system.ts
中这几个核心函数的简化逻辑。
startTracking(sub)
// src/system.ts (简化示意)
function startTracking(sub: Subscriber): void {
// 1. 重置 depsTail 指针,为添加/检查新依赖做准备
sub.depsTail = undefined;
// 2. 设置 Tracking 标志,并清除可能存在的旧状态标志 (Notified, Recursed, Propagated)
sub.flags = (sub.flags & ~(SubscriberFlags.Notified | SubscriberFlags.Recursed | SubscriberFlags.Propagated)) | SubscriberFlags.Tracking;
}
解释:
depsTail
是一个指向订阅者依赖链表(deps
)末尾的指针,用于优化link
操作。重置它表示开始一轮新的依赖检查。- 设置
Tracking
标志,同时清除一些可能由propagate
设置的状态标志,确保一个干净的追踪环境。
link(dep, sub)
// src/system.ts (简化示意)
function link(dep: Dependency, sub: Subscriber): Link | undefined {
// (省略了一些优化检查,比如检查是否已经链接)
// 1. 创建一个新的 Link 对象,存储依赖 (dep) 和订阅者 (sub)
const newLink: Link = {
dep, // 被依赖的对象 (e.g., Signal)
sub, // 订阅者对象 (e.g., Computed, Effect)
nextDep: undefined, // 指向 sub 的下一个依赖链接
prevSub: undefined, // 指向 dep 的上一个订阅者链接
nextSub: undefined, // 指向 dep 的下一个订阅者链接
};
// 2. 将新链接添加到 sub 的依赖列表 (deps) 末尾
const depsTail = sub.depsTail; // 获取 sub 当前的依赖尾指针
if (depsTail === undefined) { // 如果 sub 还没有依赖
sub.deps = newLink; // newLink 成为第一个依赖
} else {
depsTail.nextDep = newLink; // 将 newLink 接在当前尾部后面
}
sub.depsTail = newLink; // 更新 sub 的依赖尾指针为 newLink (标记为最新访问/有效)
// 3. 将新链接添加到 dep 的订阅者列表 (subs) 末尾
const subsTail = dep.subsTail; // 获取 dep 当前的订阅者尾指针
if (dep.subs === undefined) { // 如果 dep 还没有订阅者
dep.subs = newLink; // newLink 成为第一个订阅者
} else {
newLink.prevSub = subsTail; // newLink 的上一个订阅者是旧尾部
subsTail!.nextSub = newLink;// 旧尾部的下一个订阅者是 newLink
}
dep.subsTail = newLink; // 更新 dep 的订阅者尾指针为 newLink
return newLink; // 返回创建的链接对象
}
解释:
- 这段代码的核心是创建一个
Link
对象,并将其正确地插入到两个双向链表中:订阅者的deps
链表和依赖的subs
链表。 depsTail
和subsTail
指针用于快速定位链表末尾,方便添加新链接。- 更新
sub.depsTail = newLink
非常关键,它标记了这个依赖在当前追踪轮次中是活跃的,endTracking
时会用到。
endTracking(sub)
// src/system.ts (简化示意)
function endTracking(sub: Subscriber): void {
const depsTail = sub.depsTail; // 获取本轮追踪最后访问的依赖链接
// 1. 检查是否有未被访问的旧依赖需要清理
if (depsTail !== undefined) {
// 如果 depsTail 后面还有链接 (depsTail.nextDep),说明那些是旧的、本轮未访问的
const nextDep = depsTail.nextDep;
if (nextDep !== undefined) {
clearTracking(nextDep); // !! 调用 clearTracking 清理从 nextDep 开始的旧链接 !!
depsTail.nextDep = undefined; // 断开与旧链接的连接
}
} else if (sub.deps !== undefined) {
// 如果 depsTail 是 undefined 但 deps 存在,说明本轮没有任何依赖被访问
clearTracking(sub.deps); // !! 清理所有旧链接 !!
sub.deps = undefined; // 清空依赖列表
}
// 2. 清除 Tracking 标志
sub.flags &= ~SubscriberFlags.Tracking;
}
解释:
endTracking
的主要职责是利用depsTail
指针来识别并清理那些在当前执行周期内没有被重新link
的旧依赖。- 它通过调用
clearTracking
(未显示,但其作用是从依赖的subs
链表中移除链接) 来完成清理工作。 - 最后清除
Tracking
标志,表示依赖追踪结束。
依赖追踪流程图
让我们用一个简化的序列图来展示这个过程:
这个图清晰地展示了 startTracking
、link
和 endTracking
如何协同工作,在订阅者执行期间动态地构建和维护依赖关系。
总结
在本章中,我们深入了解了 master
响应式系统自动管理依赖的关键机制:依赖追踪与链接 (Tracking & Linking)。
- 这是响应式系统能够自动响应变化的基础。系统无需我们手动声明,就能在运行时动态发现“谁依赖谁”。
- 依赖追踪 (Tracking) 通过
startTracking
和endTracking
界定一个订阅者(计算属性 (Computed) 或 副作用 (Effect))的执行上下文,并使用全局标记activeSub
来标识当前活动的订阅者。 - 链接 (Linking) 通过
link
函数实现。当处于追踪状态的订阅者读取一个依赖(信号 (Signal) 或 计算属性)时,link
会在它们之间建立双向链接,更新依赖的订阅者列表 (subs
) 和订阅者的依赖列表 (deps
)。 endTracking
负责清理在当前执行周期内不再需要的旧依赖关系,确保依赖图的准确性。
这个过程就像一个不知疲倦的侦探,时刻观察着数据的流动,精确地记录下所有的关联,为后续的精确通知(propagate
)打下坚实的基础。
下一步
我们现在知道了依赖关系是如何被动态发现和建立起来的 (link
)。那么,构成这些关系的两端——那个被依赖的东西(比如 信号 (Signal))和那个依赖别人的东西(比如 计算属性 (Computed) 或 副作用 (Effect))——它们在系统内部是如何表示的?它们有哪些共同的特性和不同的职责?
在下一章,我们将分别聚焦于这两个核心角色:依赖 (Dependency) 和 订阅者 (Subscriber),深入了解它们的数据结构和在响应式系统中的具体作用。