细粒度信号(Signals)与虚拟 DOM 重渲染模型有何差异?为什么 Preact / Solid / Vue 都在拥抱 Signals 思想?

Signals(细粒度响应式信号,目前也已进入 TC39 JavaScript 语言标准提案 Stage 1) 代表了与 React 传统“组件级全量重渲染 + Virtual DOM Diff”截然不同的更新哲学:它将更新粒度从组件树(Component Tree)下沉到了单个数据值与单个 DOM 文本/属性节点(Fine-grained Reactivity)。

一、 虚拟 DOM 模型(Pull-based)vs Signals 模型(Push-Pull 细粒度)对比

维度 React 传统模型(Component Re-render + VDOM) Signals 细粒度模型(Solid / Preact Signals / Vue Vapor)
状态变更时的执行范围 重新执行当前组件函数及其下属所有未 memo 的子组件函数,生成新 VDOM 树再 Diff 组件函数仅在挂载时执行 1 次!状态更新时仅精准执行订阅了该 Signal 的那一行 DOM 更新闭包
更新复杂度决定因素 与组件树总节点规模成正比($O(\text{Tree Size})$) 仅与实际绑定了该状态的动态节点个数成正比($O(\text{Changed Bindings})$)
心智负担 需时刻注意闭包过期(Stale Closure)、依赖数组遗漏与 useMemo 引用稳定 自动运行时追踪依赖(Auto-tracking),无依赖数组,无闭包过期问题

二、 手写 25 行极简 Signals 引擎(含 signal、computed 与 effect)

Signals 的核心原理是维护一个全局调用栈 activeEffect:读取 sig.value(Getter)时自动把当前正在运行的 effect 加入订阅集合;修改 sig.value = newVal(Setter)时精确通知订阅集合重跑。

let activeObserver = null

export function createSignal(initialValue) {
  let value = initialValue
  const subscribers = new Set()

  const read = () => {
    // 1. 依赖收集(Track):若当前处于 effect 或 computed 求值上下文中,自动登记订阅
    if (activeObserver) subscribers.add(activeObserver)
    return value
  }

  const write = (nextValue) => {
    if (Object.is(value, nextValue)) return
    value = nextValue
    // 2. 派发更新(Trigger):克隆一份快照遍历执行,避免重收集死循环
    for (const sub of [...subscribers]) sub()
  }

  return [read, write]
}

export function createEffect(fn) {
  const execute = () => {
    activeObserver = execute
    try {
      fn()
    } finally {
      activeObserver = null
    }
  }
  execute()
}

// 验证:细粒度自动追踪
const [count, setCount] = createSignal(1)
createEffect(() => {
  console.log('仅在 count 变化时精准打印:', count())
})
setCount(2) // 自动输出: 仅在 count 变化时精准打印: 2

同分类其他题目