依赖跟踪基础(I)
Source: Dev.to
请提供您希望翻译的具体内容,我将把它翻译成简体中文并保持原有的格式。
什么是 Dependency Tracking?
Dependency Tracking 是一种用于自动收集和记录数据片段之间关系的技术。它使系统能够在底层数据变化时精确触发 recomputation 或 side effects——这成为 fine‑grained reactivity 的基础。
一个简单的类比是 Excel:当你更改一个单元格时,所有依赖于它的其他单元格会自动重新计算。这正是 Dependency Tracking 的核心思想。
依赖追踪中的三个关键角色
基于依赖追踪的响应式系统通常由三个组件组成:
- Source (Signal) – 基本的可变值,状态的最小单元。
- Computed (Derived Value) – 从源派生的纯函数,通常会缓存并惰性求值。
- Effect (Side Effect) – 与外部世界交互的操作(DOM 更新、数据获取、日志记录等)。
它们形成了一个清晰的依赖图:

依赖追踪是如何工作的
依赖追踪通常分为三个步骤:
1. 追踪(收集依赖)
在执行计算属性或副作用时,系统会使用 getter 记录访问了哪些源。活动的计算会被存入一个栈中,这个栈是收集依赖的核心机制。
export interface Computation {
dependencies: Set<any>;
execute: () => void;
}
const effectStack: Computation[] = [];
export function subscribe(current: Computation, subscriptions: Set<any>): void {
subscriptions.add(current);
current.dependencies.add(subscriptions);
}
function createSignal<T>(value: T) {
const subscribers = new Set<any>();
const getter = () => {
const currEffect = effectStack[effectStack.length - 1];
if (currEffect) subscribe(currEffect, subscribers);
return value;
};
const setter = (newValue: T) => {
if (newValue === value) return;
value = newValue;
subscribers.forEach(sub => sub.execute());
};
return { getter, setter };
}
2. 通知(重新运行依赖)
当源数据更新时,系统会通知所有依赖的计算。清理工作至关重要——如果不清理,旧条件下的陈旧依赖会继续触发。
function effect(fn: () => void) {
const runner: Computation = {
execute: () => {
cleanupDependencies(runner);
runWithStack(runner, fn);
},
dependencies: new Set<any>(),
};
runner.execute(); // 初始运行一次
}
function cleanupDependencies(computation: Computation) {
computation.dependencies.forEach(subscription => {
subscription.delete(computation);
});
computation.dependencies.clear();
}
export function runWithStack<T>(computation: Computation, fn: () => T): T {
effectStack.push(computation);
try {
return fn();
} finally {
effectStack.pop();
}
}
3. 调度(批处理与优化)
调度器可以防止冗余执行,并高效合并更新。
function schedule(job) {
queueMicrotask(job);
}
不同的框架会实现更高级的调度机制,但上述内容是其基本思想。
Pull‑based vs. Push‑based Reactivity
| Type | Description | Examples |
|---|---|---|
| Pull‑based | UI 查询 数据变化(例如 diffing) | 虚拟 DOM(React) |
| Push‑based | 数据 推送 更新给依赖者 | Signals, MobX, Solid.js |
基于 Signals 的系统通常采用推送式更新,这大幅减少了不必要的重新渲染,并避免全局 diffing。
处理动态依赖
依赖跟踪的一个棘手方面是动态依赖,例如:
effect(() => {
if (userId()) {
fetchProfile(userId());
}
});
如果条件发生变化,系统必须:
- 停止跟踪旧的依赖
- 仅在相关时跟踪新的依赖
这就是为什么在大多数运行时中,清理和基于栈的执行是必不可少的。
框架比较:它们如何实现依赖追踪
| Framework | Mechanism | Scheduler |
|---|---|---|
| Solid.js | 运行时,基于栈的追踪 | 微任务 + 批处理 |
| Vue 3 | 基于 Proxy 的运行时追踪 | 作业队列(宏任务) |
| MobX | 全局包装的 getter | 微任务 |
| Svelte | 编译时静态分析 | 同步或微任务 |
React 的依赖模型非常不同,将在下一篇文章中详细探讨。
结论
依赖追踪为细粒度的响应式提供了基础架构。通过自动收集数据与计算之间的关系,系统能够精确且高效地进行更新。掌握依赖追踪有助于你设计更好的 UI 架构、优化渲染工作流,并理解现代响应式框架为何以这种方式工作。
下一篇文章:依赖追踪(II)
在下一章节中,我们将分析 React 的依赖模型,它与细粒度系统的区别,以及为何 Signals 提供了更简洁的解决方案。敬请期待!