依赖跟踪基础(I)

发布: (2026年1月7日 GMT+8 08:39)
5 min read
原文: Dev.to

Source: Dev.to

请提供您希望翻译的具体内容,我将把它翻译成简体中文并保持原有的格式。

什么是 Dependency Tracking?

Dependency Tracking 是一种用于自动收集和记录数据片段之间关系的技术。它使系统能够在底层数据变化时精确触发 recomputationside effects——这成为 fine‑grained reactivity 的基础。

一个简单的类比是 Excel:当你更改一个单元格时,所有依赖于它的其他单元格会自动重新计算。这正是 Dependency Tracking 的核心思想。

依赖追踪中的三个关键角色

基于依赖追踪的响应式系统通常由三个组件组成:

  1. Source (Signal) – 基本的可变值,状态的最小单元。
  2. Computed (Derived Value) – 从源派生的纯函数,通常会缓存并惰性求值。
  3. Effect (Side Effect) – 与外部世界交互的操作(DOM 更新、数据获取、日志记录等)。

它们形成了一个清晰的依赖图:

signal duty

依赖追踪是如何工作的

依赖追踪通常分为三个步骤:

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

TypeDescriptionExamples
Pull‑basedUI 查询 数据变化(例如 diffing)虚拟 DOM(React)
Push‑based数据 推送 更新给依赖者Signals, MobX, Solid.js

基于 Signals 的系统通常采用推送式更新,这大幅减少了不必要的重新渲染,并避免全局 diffing。

处理动态依赖

依赖跟踪的一个棘手方面是动态依赖,例如:

effect(() => {
  if (userId()) {
    fetchProfile(userId());
  }
});

如果条件发生变化,系统必须:

  • 停止跟踪旧的依赖
  • 仅在相关时跟踪新的依赖

这就是为什么在大多数运行时中,清理和基于栈的执行是必不可少的。

框架比较:它们如何实现依赖追踪

FrameworkMechanismScheduler
Solid.js运行时,基于栈的追踪微任务 + 批处理
Vue 3基于 Proxy 的运行时追踪作业队列(宏任务)
MobX全局包装的 getter微任务
Svelte编译时静态分析同步或微任务

React 的依赖模型非常不同,将在下一篇文章中详细探讨。

结论

依赖追踪为细粒度的响应式提供了基础架构。通过自动收集数据与计算之间的关系,系统能够精确且高效地进行更新。掌握依赖追踪有助于你设计更好的 UI 架构、优化渲染工作流,并理解现代响应式框架为何以这种方式工作。

下一篇文章:依赖追踪(II)

在下一章节中,我们将分析 React 的依赖模型,它与细粒度系统的区别,以及为何 Signals 提供了更简洁的解决方案。敬请期待!

Back to Blog

相关文章

阅读更多 »