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
🤔 核心问题
要实现响应式,需要解决两个关键问题:
- 谁在使用这个变量?—— 依赖收集(
Track): 必须在数据读取时, 能够识别并记录哪些副作用函数(effect)依赖了它。 - 变量修改后通知谁?——触发更新(
Trigger): 当数据变化时, 能够通知所有依赖它的副作用函数(effect)重新执行。
🔧 第一步:添加 getter 和 setter
通过 class 封装, 利用 getter 和 setter 来拦截对变量的读取和修改:
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 | 标记 activeEffect 为 fn。 | 0 |
| 收集依赖 | count.value | RefImpl getter | Track: 记录 activeEffect 到 subs。 | |
| 清理 | effect 结束 | effect | 清空 activeEffect。 | |
| 更新 | count.value=1 | RefImpl setter | Trigger: 更新 value = 1。 | |
| 通知 | 执行 ref.subs?.() | setter | 调用被收集的 fn | |
| 重新执行 | fn 再次运行 | effect / fn | 重新获取 count.value(1) | 1 |
🗣️ 用大白话解释
| 阶段 | 谁在做什么 | 目的 |
|---|---|---|
| 创建 | ref(0) | 创建一个 RefImpl, 拥有 getter 和 setter |
| 依赖收集 | 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 (依赖集合) 来解决这些问题。