响应式原理深入
本教程共 40 篇 · 第 37 篇 · 更新于 2026-07-29 · 约 7 分钟阅读
37. 响应式原理深入
本节目标:透过 API 看本质,搞懂 Vue 响应式系统是怎么追踪依赖、触发更新的。
响应式到底是什么意思
“响应式”就是数据变化时自动更新相关联的东西。用一个简单的例子来说明:
A0 = 1
A1 = 2
A2 = A0 + A1 // 3
当你把 A0 改成 2,A2 自动变成 4。JavaScript 默认不这样:
let A0 = 1
let A1 = 2
let A2 = A0 + A1 // 3
A0 = 2
console.log(A2) // 还是 3
Note这个自动更新机制在第 1 章”Vue 简介”中首次介绍过。本节将深入讲解其底层实现原理。
怎么让 JS 实现这个效果?分三步:
- 追踪:谁读了 A0、A1?记录下来。
- 触发:A0 变了,通知所有读过它的代码。
- 执行:那些代码重新运行。
track 和 trigger
Vue 用 Proxy(或 getter/setter)拦截对象属性的读写:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key) // 记录:有人读了
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key) // 通知:值变了
}
})
}
track 做了什么?检查当前有没有正在运行的”副作用”(effect),有就把它加到该属性的订阅列表里。
let activeEffect
function track(target, key) {
if (activeEffect) {
const effects = getSubscribersForProperty(target, key)
effects.add(activeEffect)
}
}
trigger 做了什么?找到该属性的所有订阅者,逐个调用。
function trigger(target, key) {
const effects = getSubscribersForProperty(target, key)
effects.forEach((effect) => effect())
}
订阅关系用全局 WeakMap 存储:WeakMap<target, Map<key, Set<effect>>>。
Reactive Effect
把上面的串起来,就是 Vue 的响应式副作用:
function whenDepsChange(update) {
const effect = () => {
activeEffect = effect // 标记自己为当前活动 effect
update() // 执行时会触发 track
activeEffect = null // 清除标记
}
effect() // 首次执行,建立订阅关系
}
Vue 暴露了这个能力:watchEffect()。
import { ref, watchEffect } from 'vue'
const A0 = ref(0)
const A1 = ref(1)
const A2 = ref()
watchEffect(() => {
A2.value = A0.value + A1.value // 读了 A0、A1,自动追踪
})
A0.value = 2 // A2 自动重新计算
computed 内部也是用 reactive effect 管理依赖。
Vue 组件的渲染本身就是一个 reactive effect——追踪模板里用到的所有响应式数据,数据变化时重新渲染。
ref vs reactive
ref 用 getter/setter 实现,reactive 用 Proxy 实现:
// ref 的简化实现
function ref(value) {
return {
get value() {
track(this, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(this, 'value')
}
}
}
两者各有局限:
| 局限 | 说明 |
|---|---|
| 解构丢失响应性 | 把 reactive 的属性解构到局部变量,不再响应 |
| 引用不同 | reactive(obj) !== obj,Proxy 创建了新对象 |
响应式调试
响应式自动追踪很方便,但有时候你想搞清楚”到底追踪了什么”或”什么触发了更新”。
组件调试钩子
import { onRenderTracked, onRenderTriggered } from 'vue'
onRenderTracked((event) => {
debugger // 追踪到新的依赖时暂停
})
onRenderTriggered((event) => {
debugger // 某个依赖触发更新时暂停
})
事件对象包含这些信息:
| 字段 | 说明 |
|---|---|
effect | 当前的 reactive effect |
target | 触发更新的目标对象 |
type | 操作类型(get/set/add 等) |
key | 变化的属性名 |
newValue / oldValue | 新旧值 |
Note这些调试钩子只在开发模式生效,生产环境自动移除。
计算属性调试
const plusOne = computed(() => count.value + 1, {
onTrack(e) {
debugger // count.value 被追踪为依赖时暂停
},
onTrigger(e) {
debugger // count.value 变化触发重新计算时暂停
}
})
侦听器调试
watch(source, callback, {
onTrack(e) { debugger },
onTrigger(e) { debugger }
})
集成外部状态
Vue 的响应式是深度代理。但有些外部状态库(比如 Immer、XState)不需要深度代理,甚至深度代理会出问题。
通用方案:用 shallowRef 包一层。shallowRef 只追踪 .value 的变化,内部值保持原样。外部状态变化时,替换 .value 触发更新。
Immer 集成
import { produce } from 'immer'
import { shallowRef } from 'vue'
export function useImmer(baseState) {
const state = shallowRef(baseState)
const update = (updater) => {
state.value = produce(state.value, updater)
}
return [state, update]
}
XState 状态机集成
import { createMachine, interpret } from 'xstate'
import { shallowRef } from 'vue'
export function useMachine(options) {
const machine = createMachine(options)
const state = shallowRef(machine.initialState)
const service = interpret(machine)
.onTransition((newState) => (state.value = newState))
.start()
const send = (event) => service.send(event)
return [state, send]
}
与 Signals 的对比
近年来其他框架提出的 Signals(Solid、Angular、Preact)本质上跟 Vue ref 一样——值容器 + 依赖追踪 + 变化触发。
API 设计上的取舍:
| 风格 | 访问值 | 更新值 | 特点 |
|---|---|---|---|
| Vue ref | .value | .value = | 简单一致 |
| Solid Signal | count() | setCount(1) | 读写分离,更安全 |
| Angular Signal | count() | count.set(1) | 方法调用 |
Vue 也支持自定义 API 风格。用 shallowRef + triggerRef 就能模拟 Solid 的读写分离。
本节回顾
- Vue 响应式基于 Proxy(reactive)和 getter/setter(ref)
track追踪谁读了属性,trigger在属性变化时通知订阅者- 响应式副作用(effect)自动追踪依赖,依赖变化时重新执行
- 组件渲染本身就是 reactive effect
onRenderTracked/onRenderTriggered用于调试- 外部状态用
shallowRef集成 - Vue ref 跟其他框架的 Signals 本质相同,API 风格有别