Alien Signals 技术分析之依赖追踪与链接 (Tracking & Linking)(五)

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

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

本文深入解析响应式系统的核心机制——依赖追踪与链接(Tracking & Linking),通过startTracking、link和endTracking三个核心函数实现自动依赖管理。系统在执行计算属性或副作用时启动追踪,通过activeSub标记当前订阅者,在读取信号时建立双向依赖关系,并在执行结束后清理无效依赖。这种动态构建的依赖图使系统能精确识别数据关系,为后续变更通知奠定基础,是响应式编程自动化的关键所在。

10.16s
~15939 tokens

在上一章 响应式系统核心 (Reactive System Core) 中,我们了解了协调整个响应式流程的幕后引擎。我们提到了像 linkstartTrackingendTracking 这样的核心函数,它们是实现自动化的关键。但它们具体是如何工作的呢?系统究竟是如何知道“谁依赖谁”的?

本章,我们将深入探讨响应式系统中最神奇的部分之一:依赖追踪与链接 (Tracking & Linking)。这正是 alien signals 如何动态地发现和记录数据之间关系的过程。

侦探出动:为何需要自动追踪?

让我们再次回到熟悉的例子:

typescript
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 响应式系统扮演“侦探”角色的过程:

  1. 追踪 (Tracking): 当一个订阅者(比如 fullName 这个 计算属性 (Computed) 或 greetingEffect 这个 副作用 (Effect))开始执行它的计算或副作用函数时,系统会启动“监视模式”。
  2. 发现 (Discovery): 在执行过程中,该订阅者会读取其他响应式源(比如 firstName()fullName())。系统会“看到”这些读取操作。
  3. 链接 (Linking): 系统会记录下:“哦,fullName 读取了 firstNamelastName”以及“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() 这样的读取操作。
  • 做什么?
    • 读取操作(比如 signalGetterSettercomputedGetter)会检查全局的 activeSub 是否有值。
    • 如果有,就调用核心的 link(dependency, subscriber) 函数(dependency 是被读取的信号或计算属性对象,subscriber 就是 activeSub)。
    • link 函数负责建立双向链接
      • subscriber 添加到 dependency 的订阅者列表 (subs) 中。(记录下:“这个信号被这个订阅者关注了”)
      • dependency 添加到 subscriber 的依赖列表 (deps) 中。(记录下:“这个订阅者依赖于这个信号”)
    • 这样,依赖图中的一条边就建立起来了。

3. 结束追踪 (endTracking)

侦探完成了本次跟踪任务。

  • 何时发生? 在订阅者的函数执行完毕后(比如 updateComputednotifyEffectfinally 块中)。
  • 做什么?
    • 清理旧依赖: 这是 endTracking 的一个重要工作。在 startTracking 时,系统准备清空旧依赖。在追踪过程中,每次 link 操作都会标记一个依赖是“新的”或“仍然有效”。endTracking 会检查哪些旧的依赖在这次执行中没有被再次 link,并将这些无效的链接从双方(依赖的 subs 和订阅者的 deps)中断开。这确保了依赖关系总是最新的。这个清理工作通常由内部的 clearTracking 函数完成。
    • 清除之前设置的 Tracking 标志。
    • 恢复全局的 activeSub 到它之前的值(可能为 undefined 或外层的另一个订阅者)。

工作流程:探索如何建立档案

让我们通过 fullName 首次计算的场景,看看这个过程是如何运作的:

  1. 触发计算: 当代码首次读取 fullName() 时,computedGetter 被调用。
  2. 检查状态: fullName 发现自己是 Dirty(需要计算)。
  3. 准备追踪: processComputedUpdate 调用 updateComputed,接着调用 startTracking(fullName)
    • activeSub 被设置为 fullName 对象。
    • fullName 的旧 deps 准备被清理。
    • fullName 被标记为 Tracking
  4. 执行 Getter: 系统开始执行 fullName 的计算函数:() => firstName() + lastName()
  5. 读取 firstName():
    • firstNamesignalGetterSetter 被调用(无参数读取模式)。
    • 它检测到 activeSubfullName (非 undefined)。
    • 调用 link(firstName, fullName)
      • fullName 被添加到 firstNamesubs 列表。
      • firstName 被添加到 fullNamedeps 列表,并被标记为有效。
  6. 读取 lastName():
    • 类似地,lastNamesignalGetterSetter 检测到 activeSubfullName
    • 调用 link(lastName, fullName)
      • fullName 被添加到 lastNamesubs 列表。
      • lastName 被添加到 fullNamedeps 列表,并被标记为有效。
  7. Getter 执行完毕: 计算函数返回结果 "张三"。
  8. 结束追踪: updateComputedfinally 块调用 endTracking(fullName)
    • clearTracking 运行:由于 firstNamelastName 都被标记为有效,没有旧的、无效的依赖需要清理。
    • fullNameTracking 标志被移除。
    • activeSub 恢复为之前的值(在这个例子中可能是 undefined)。
  9. 缓存与返回: fullName 缓存结果 "张三",清除 Dirty 标记,并返回值。

现在,系统已经成功记录了:fullName 依赖于 firstNamelastName。如果之后 firstName 变化,propagate 就知道要通知 fullName 了。

同样的过程也发生在 greetingEffect 首次执行时,它会通过 startTracking(greetingEffect)、读取 fullName() 时调用 link(fullName, greetingEffect)、以及最后的 endTracking(greetingEffect),建立起 fullNamegreetingEffect 的依赖链接。

内部机制:追踪与链接的代码实现

让我们简单看下 system.ts 中这几个核心函数的简化逻辑。

startTracking(sub)

src/system.ts (简化示意)
typescript
// 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 (简化示意)
typescript
// 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 链表。
  • depsTailsubsTail 指针用于快速定位链表末尾,方便添加新链接。
  • 更新 sub.depsTail = newLink 非常关键,它标记了这个依赖在当前追踪轮次中是活跃的,endTracking 时会用到。

endTracking(sub)

src/system.ts (简化示意)
typescript
// 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 标志,表示依赖追踪结束。

依赖追踪流程图

让我们用一个简化的序列图来展示这个过程:

这个图清晰地展示了 startTrackinglinkendTracking 如何协同工作,在订阅者执行期间动态地构建和维护依赖关系。

总结

在本章中,我们深入了解了 master 响应式系统自动管理依赖的关键机制:依赖追踪与链接 (Tracking & Linking)

  • 这是响应式系统能够自动响应变化的基础。系统无需我们手动声明,就能在运行时动态发现“谁依赖谁”。
  • 依赖追踪 (Tracking) 通过 startTrackingendTracking 界定一个订阅者(计算属性 (Computed) 或 副作用 (Effect))的执行上下文,并使用全局标记 activeSub 来标识当前活动的订阅者。
  • 链接 (Linking) 通过 link 函数实现。当处于追踪状态的订阅者读取一个依赖(信号 (Signal) 或 计算属性)时,link 会在它们之间建立双向链接,更新依赖的订阅者列表 (subs) 和订阅者的依赖列表 (deps)。
  • endTracking 负责清理在当前执行周期内不再需要的旧依赖关系,确保依赖图的准确性。

这个过程就像一个不知疲倦的侦探,时刻观察着数据的流动,精确地记录下所有的关联,为后续的精确通知(propagate)打下坚实的基础。

下一步

我们现在知道了依赖关系是如何被动态发现和建立起来的 (link)。那么,构成这些关系的两端——那个被依赖的东西(比如 信号 (Signal))和那个依赖别人的东西(比如 计算属性 (Computed) 或 副作用 (Effect))——它们在系统内部是如何表示的?它们有哪些共同的特性和不同的职责?

在下一章,我们将分别聚焦于这两个核心角色:依赖 (Dependency)订阅者 (Subscriber),深入了解它们的数据结构和在响应式系统中的具体作用。