响应式
如果只能用一句话概括 Vue 的响应式,可以这样说:
你改数据,Vue 能知道;Vue 知道后,会把依赖这个数据的地方重新运行。
这句话看起来简单,但背后其实是一整套机制:
- 谁在读取数据
- 谁在修改数据
- 哪些逻辑依赖了这份数据
- 数据变化后应该通知谁重新执行
如果你先把这张图看懂,后面再看 track、trigger、effect,就不会觉得响应式系统特别抽象。
一、为什么响应式这么重要
在没有响应式系统时,你通常要手动做很多事:
state.count++
updateView()
而在 Vue 里,我们更希望写成:
state.count++
剩下的更新工作交给框架处理。
这就是响应式的价值:让状态变化自动驱动视图和副作用更新。
二、先不要急着看源码,先建立直觉
可以把响应式系统理解成一个“订阅通知系统”:
- 某段逻辑运行时读取了数据
- Vue 记住“这段逻辑依赖了这个数据”
- 当数据变化时,Vue 去通知对应逻辑重新运行
这三个动作分别对应三个关键词:
track:收集依赖trigger:触发更新effect:被收集、会重新运行的副作用函数
三、一个最小响应式例子
先别看 Vue 源码,先看最小模型:
let activeEffect: Function | null = null
const bucket = new Set<Function>()
const data = { text: 'hello' }
const obj = new Proxy(data, {
get(target, key) {
if (activeEffect) {
bucket.add(activeEffect)
}
return target[key]
},
set(target, key, value) {
target[key] = value
bucket.forEach(fn => fn())
return true
},
})
function effect(fn: Function) {
activeEffect = fn
fn()
activeEffect = null
}
effect(() => {
document.body.innerText = obj.text
})
setTimeout(() => {
obj.text = 'world'
}, 1000)
这个版本已经能体现最基础的思路:
- 读取时收集依赖
- 修改时重新执行依赖
但它还远远不够。
四、为什么这个最小版本不够用
因为真实项目里的数据不会只有一个字段,也不会只有一个副作用函数。
比如下面这些问题,最小版本都处理不好:
- 不同属性应该分别追踪
- 不同对象不能混在一起
- 分支切换后旧依赖要清理
- 副作用不能无限递归触发
- 更新不能每次都立刻同步执行
所以真正可用的响应式系统,必须把数据结构设计得更精细。
五、Vue 3 的核心数据结构
Vue 3 里最关键的一层结构,可以简化理解成:
WeakMap<object, Map<PropertyKey, Set<ReactiveEffect>>>
它表示:
- 第一层
WeakMap:按对象区分 - 第二层
Map:按属性区分 - 第三层
Set:记录依赖这个属性的 effect
你可以把它想成一本三层通讯录:
- 哪个对象
- 哪个字段
- 对应哪些订阅者
六、track 在做什么
当响应式对象的某个属性被读取时,会触发依赖收集。
简化后可以理解成:
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
}
这段逻辑的本质非常朴素:
只要当前存在正在运行的 effect,就把它记到当前属性对应的依赖集合里。
七、trigger 在做什么
当属性被修改时,Vue 会找到依赖它的 effect,然后重新执行。
简化理解:
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(effect => effect())
}
这就是“通知订阅者”的过程。
八、effect 为什么是整个系统的中心
很多人学响应式时只盯着 reactive,其实真正连接“数据”和“更新逻辑”的桥梁是 effect。
它的职责是:
- 先执行一遍函数
- 在执行过程中触发依赖收集
- 后续数据变化时可以再次执行
也就是说,没有 effect,响应式系统就只是一个会记账的壳子。
九、为什么需要依赖清理
看一个分支例子:
effect(() => {
document.body.innerText = state.ok ? state.text : 'not'
})
第一次执行时,如果 state.ok 为 true,那这段 effect 会依赖:
state.okstate.text
但如果下一次 state.ok 变成 false,理论上这段逻辑就不应该继续依赖 state.text 了。
如果不清理旧依赖,会发生:
state.text改了- effect 还是会重新执行
- 但实际上这次执行根本不需要它
这就是为什么 Vue 在每次 effect 重新执行前,都会做 cleanup。
十、为什么需要 effect 栈
真实项目里,副作用函数可能嵌套执行。
例如:
effect(() => {
console.log(state.foo)
effect(() => {
console.log(state.bar)
})
})
如果只用一个全局 activeEffect,就很容易乱掉。
因此 Vue 会用一个栈来保存 effect 执行上下文:
- 入栈当前 effect
- 执行函数
- 执行结束后出栈
- 恢复上一层 activeEffect
这可以避免嵌套时依赖收集错位。
十一、为什么更新不能每次都同步执行
如果一改数据就立刻执行 effect,会有几个问题:
- 重复执行太多
- 多次修改不能合并
- 组件更新会变得低效
因此 Vue 不只是“收集依赖再触发”,它还会配合调度器 scheduler 去控制执行时机。
这也是为什么响应式系统会自然连接到:
- 组件更新队列
nextTick- 批量刷新机制
十二、reactive、ref、computed、watch 的关系
很多人容易把这些 API 拆开记,其实它们底层关系很近。
reactive
让对象变成响应式对象。
ref
让一个值拥有响应式能力,本质上可以理解成:
{ value: xxx }
只是 Vue 帮你做了包装。
computed
本质上是“带缓存的 effect”。
它不会每次都重新计算,而是:
- 依赖没变时直接返回缓存
- 依赖变了才重新计算
watch
本质上是“观察某个响应式来源变化后执行回调”。
它和 effect 的关系非常近,只是语义更偏“监听变化”。
十三、源码阅读时最值得抓的主线
如果你准备读 Vue 3 响应式源码,建议抓这条主线:
reactive()如何创建代理对象get里如何trackset里如何triggereffect()如何记录 activeEffect- cleanup、effect 栈、scheduler 如何把系统补完整
只要主线清楚,后面的各种优化和边界处理就不容易把你绕晕。
十四、一个简化版源码脉络
下面这段伪代码,基本能串起整个响应式主流程:
const targetMap = new WeakMap()
let activeEffect
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn
fn()
activeEffect = undefined
}
effectFn()
return effectFn
}
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, (deps = new Set()))
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(effect => effect())
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
},
})
}
真实源码当然会复杂得多,但核心理解不会离这条主线太远。
十五、你真正应该理解什么
学响应式,不是为了背 API,而是要真正理解下面这件事:
Vue 不是“神奇地自动更新”,而是把“依赖收集”和“变更通知”这两件事做成了稳定的数据结构和执行流程。
总结
响应式系统最核心的三件事是:
- 读取时收集依赖
- 修改时触发更新
- 用 effect 把状态和逻辑连接起来
当你理解了这条主线,再去看 computed、watch、组件更新、nextTick,会顺畅很多。
