2025.09.01

响应式原理:数据与副作用的关联机制

🎯 什么是响应式?

想象一个数字 count:当它发生变化时,所有依赖它的地方会通知并执行更新——这就是响应式的核心思想。

// 创建一个响应式变量
const count = ref(0)

// 当 count 变化时,这个函数会自动执行
effect(() => {
  console.log('count.value ==> ', count.value)
})

// 1秒后修改 count 的值
setTimeout(() => {
  count.value = 1
}, 1000)

运行结果

  • 初始加载: 🌈 count.value ==> 0
  • 1秒后: 🌈 count.value ==> 1

🤔 核心问题

要实现响应式,需要解决两个关键问题:

  1. 谁在使用这个变量?—— 依赖收集(Track): 必须在数据读取时, 能够识别并记录哪些副作用函数 (effect) 依赖了它。
  2. 变量修改后通知谁?——触发更新(Trigger): 当数据变化时, 能够通知所有依赖它的副作用函数 (effect) 重新执行。

🔧 第一步:添加 getter 和 setter

通过 class 封装, 利用 gettersetter 来拦截对变量的读取和修改:

class RefImpl {
  _value

  constructor(value) {
    this._value = value
  }

  get value() {
    console.log('get value') // 读取时触发
    return this._value
  }

  set value(newValue) {
    console.log('set value', newValue) // 修改时触发
    this._value = newValue
  }
}

📝 第二步:追踪正在执行的 effect

如何知道是哪一个 effect 正在读取 count.value 呢?我们使用一个全局变量来记住当前正在执行的 effect 函数。

// effect.ts
let activeEffect // 🚨 核心全局变量:记住当前正在执行的 effect

export function effect(fn) {
  activeEffect = fn // 1. 📌 标记:把 fn 存入全局变量
  fn() // 2. ▶️ 执行:执行 fn (此时会触发 getter)
  activeEffect = undefined // 3. 🧹 清空:执行完毕后释放标记
}

函数作用: 只有在 effect 内部读取响应式数据时, activeEffect 才会有值, 从而实现依赖收集.

🔗 第三步:建立依赖关系

将 追踪 (activeEffect) 和 通知 (subs) 的逻辑集成到 RefImpl 中。

// ref.ts
import { activeEffect } from './effect'

class RefImpl {
  _value
  subs // 存储依赖这个 ref 的 effect 函数

  // ... constructor

  get value() {
    // 🔍 依赖收集 (Track)
    if (activeEffect) {
      this.subs = activeEffect // 简单的实现:只支持一个 effect
    }
    return this._value
  }

  set value(newValue) {
    this._value = newValue
    // 🔄 触发更新 (Trigger)
    this.subs?.() // 执行之前收集到的 effect 函数
  }
}

export function ref(value) {
  return new RefImpl(value)
}

🎉 完成!基本响应式已就绪

让我们通过一个小例子来回顾整个流程:

const count = ref(0) // 步骤 1:创建 Ref
effect(() => {
  console.log('count.value ==> ', count.value) // 步骤 2:注册 Effect
})
setTimeout(() => {
  count.value = 1 // 步骤 3:修改 Ref
}, 1000)
阶段操作触发函数关键动作结果
注册effect(() => { ... })effect标记 activeEffectfn0
收集依赖count.valueRefImpl getterTrack: 记录 activeEffectsubs
清理effect 结束effect清空 activeEffect
更新count.value=1RefImpl setterTrigger: 更新 value = 1
通知执行 ref.subs?.()setter调用被收集的 fn
重新执行 fn 再次运行effect / fn重新获取 count.value(1)1

🗣️ 用大白话解释

阶段谁在做什么目的
创建ref(0)创建一个 RefImpl, 拥有 gettersetter
依赖收集effect 执行时, getter 被触发ref 记住了当前正在执行的函数 activeEffect
触发更新setter 被触发ref 通知之前记录下的函数 subs 重新执行

💻 完整代码实现

effect.ts - 依赖追踪模块

/**
 * 💡 依赖追踪核心:全局变量 activeEffect
 * 记录当前正在读取响应式数据的副作用函数。
 */
export let activeEffect

export function effect(fn) {
  activeEffect = fn
  activeEffect()
  activeEffect = undefined
}

ref.ts - 响应式变量模块

import { activeEffect } from './effect'

/**
 * 📦 RefImpl: 响应式引用的封装类
 */
class RefImpl {
  _value
  subs // 依赖集合 (简易版只存一个函数)

  constructor(value) {
    this._value = value
  }

  get value() {
    // 🎯 依赖收集
    if (activeEffect) {
      this.subs = activeEffect // 建立连接
    }
    return this._value
  }

  set value(newValue) {
    // 🎯 触发更新
    this._value = newValue
    this.subs?.() // 执行副作用函数
  }
}

export function ref(value) {
  return new RefImpl(value)
}

🚧 下一步挑战

当前的实现已经掌握了响应式的核心原理,但还存在明显的缺陷,为下一步的优化指明了方向:

  • ❌ 单一依赖: subs 只能存储一个 effect 函数。
  • ❌ 多重依赖: 一个 effect 无法依赖多个 ref(在 effect 内部读取多个 ref 值)。
    这就是 Vue 响应式系统的最核心原理!🎉 下一步我们将引入 Dep (依赖集合) 来解决这些问题。