Vue.js: 响应式原理深挖
最后更新:2026-08-26
理解 Vue 3 响应式原理能让你写出更高效的代码、调试棘手的响应式问题。本课深入 Vue 3 响应式系统的底层实现:Proxy 拦截、依赖收集、触发更新,以及 ref / computed / watch 的内部机制。
Vue 3 用 ES6 Proxy 替代 Vue 2 的 Object.defineProperty,原生支持数组、Map、Set,性能提升 2x。理解原理后,你能避免 5 个常见响应式失效陷阱。
1. 你将学到
- Proxy 拦截 get/set 操作的原理
- Reflect API 在响应式中的作用
- track(依赖收集)+ trigger(触发更新)核心流程
- ref / reactive / computed 的内部实现
- watch vs watchEffect 的内部差异
- 性能优化:shallowRef / markRaw
- 5 个常见响应式失效的根因
2. 一个 10 万行表格的"卡死"事件
(1) 痛点:100,000 项的 list,ref 包装后页面卡死
Alice 在后台加载了一个 10,000 行的商品表格:
JS
// ❌ 翻车版:deep reactive 处理 10 万行
const { ref } = Vue
const products = ref([
{ id: 1, name: 'Product 1', price: 99, ... },
// ... 10,000 项 ...
])
// Vue 给每个属性、嵌套对象都创建 Proxy
// 内存占用:100MB+
// 首次渲染:3 秒
// 滚动:10 FPS(卡顿)
性能监控显示:
"10,000 行 × 4 个嵌套属性 × Proxy 开销 = 40,000 个 Proxy。每次属性访问触发 track。内存:100MB。FPS:10。"
(2) Vue shallowRef + markRaw 解法
JS
const { shallowRef, markRaw } = Vue
// ✅ shallowRef:只跟踪 .value 整体替换
const products = shallowRef([
{ id: 1, name: 'Product 1' },
// ... 10,000 项(不会被 Proxy 包装)...
])
// ✅ markRaw:标记对象永远不响应(最快)
const staticConfig = markRaw({
apiUrl: 'https://api.example.com',
version: '1.0'
})
// 整体替换
products.value = await fetchProducts() // 只触发 1 次更新
(3) 收益
优化后:
- Proxy 数量:40,000 → 0(shallowRef 不创建 Proxy)
- 内存占用:100MB → 5MB
- 首次渲染:3s → 200ms
- FPS:10 → 60(流畅)
3. Proxy 拦截:Vue 3 响应式基石
(1) 什么是 Proxy?
Proxy 是 ES6 引入的对象包装器——可以拦截对象的 13 种操作(get / set / has / deleteProperty 等)。
JS
// 基础 Proxy 示例
const target = { name: 'Alice', age: 25 }
const proxy = new Proxy(target, {
get(target, key) {
console.log(`Read ${key}`)
return Reflect.get(target, key)
},
set(target, key, value) {
console.log(`Write ${key} = ${value}`)
return Reflect.set(target, key, value)
}
})
proxy.name // 输出 "Read name",返回 'Alice'
proxy.age = 26 // 输出 "Write age = 26"
(2) Vue 3 的 reactive 实现(简化版)
JS
// Vue 3 内部 reactivity 包的简化实现
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 1. 依赖收集:记录谁在用这个属性
track(target, key)
// 2. 递归包装:嵌套对象也变响应式
const value = Reflect.get(target, key, receiver)
if (isObject(value)) {
return reactive(value)
}
return value
},
set(target, key, value, receiver) {
// 1. 设置值
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 2. 触发更新:通知所有用这个属性的地方
if (oldValue !== value) {
trigger(target, key)
}
return result
}
})
}
(3) Proxy vs Object.defineProperty
| 维度 | Object.defineProperty (Vue 2) | Proxy (Vue 3) |
|---|---|---|
| 数组支持 | 需特殊处理 | 原生支持 |
| Map/Set | ❌ | ✅ |
| 添加新属性 | 需 Vue.set | 自动响应 |
| 性能 | 一般 | 更好(懒代理) |
| 浏览器支持 | IE9+ | IE 不支持(需 polyfill) |
4. track + trigger:依赖收集与触发更新
(1) 完整流程图
sequenceDiagram
participant T as Template
participant C as Component
participant R as Reactive<br/>(Proxy)
participant W as WeakMap<br/>(target → Map)
participant D as Data
T->>C: 1. Render (读 count)
C->>R: 2. get count
R->>W: 3. track(target, 'count')
W->>W: 4. 记录当前 activeEffect
R->>D: 5. Read 0
R-->>C: 6. Return 0
C-->>T: 7. Display "Stock: 0"
Note over D,T: 用户点击按钮
D->>R: 8. set count = 1
R->>W: 9. trigger(target, 'count')
W->>W: 10. 找到所有依赖
R->>C: 11. 调度更新
C-->>T: 12. Display "Stock: 1"
(2) 全局依赖存储结构
JS
// Vue 3 内部数据结构
const targetMap = new WeakMap()
// targetMap = { obj1: Map1, obj2: Map2, ... }
// Map1 = { key1: Set1, key2: Set2, ... }
// Set1 = Set<activeEffect1, activeEffect2, ...>
function track(target, key) {
// 1. 获取 target 对应的 Map
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 2. 获取 key 对应的 Set
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
// 3. 记录当前激活的 effect
if (activeEffect) {
dep.add(activeEffect)
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
// 通知所有依赖重新执行
dep.forEach(effect => effect.run())
}
}
(3) 5 大核心要点
| 要点 | 说明 |
|---|---|
| track 在 get 时 | 读属性 → 记录"谁在用" |
| trigger 在 set 时 | 改属性 → 通知"所有用的人" |
| WeakMap 存储 | 自动垃圾回收(target 销毁时自动清理) |
| activeEffect | 当前正在执行的 effect(render / watch / computed) |
| 批处理 | 多次修改只触发 1 次更新(nextTick) |
5. ref 的内部实现
(1) ref 的源码(简化版)
JS
// Vue 3 ref 内部实现
class RefImpl {
constructor(value) {
this._value = isObject(value) ? reactive(value) : value
}
get value() {
// 1. track
track(this, 'value')
return this._value
}
set value(newValue) {
// 1. 比较新旧值
if (hasChanged(this._value, newValue)) {
this._value = isObject(newValue) ? reactive(newValue) : newValue
// 2. trigger
trigger(this, 'value')
}
}
}
function ref(value) {
return new RefImpl(value)
}
(2) 自动解包原理
VUE
<template>
<!-- 模板中自动解包:{{ count }} 而不是 {{ count.value }} -->
<p>{{ count }}</p>
</template>
<script setup>
const { ref } = Vue
const count = ref(0)
console.log(count.value) // 0(JS 中需 .value)
console.log(count) // 0(devtools 中显示 0)
</script>
原理:模板编译时,编译器自动给 ref 加 .value(无需你手写)。
6. computed 的内部实现
(1) lazy + dirty 标志
JS
// Vue 3 computed 简化实现
function computed(getter) {
const ref = {
// 1. 缓存值
_value: undefined,
// 2. 是否过期(需要重算)
_dirty: true,
// 3. getter
get value() {
// 懒计算:只在第一次访问时算
if (this._dirty) {
this._value = effect(getter) // 收集依赖
this._dirty = false
}
// 4. track(自己被谁用)
track(this, 'value')
return this._value
}
}
// 5. 当依赖变化时,标记 dirty
effect(() => getter()).scheduled = () => {
ref._dirty = true
trigger(ref, 'value') // 触发用 computed 的组件更新
}
return ref
}
(2) 缓存机制
JS
const count = ref(0)
const double = computed(() => count.value * 2)
console.log(double.value) // 0(第一次计算)
console.log(double.value) // 0(缓存,不重算)
count.value = 5
console.log(double.value) // 10(重算)
7. 性能优化:shallowRef / shallowReactive / markRaw
(1) shallowRef:浅层响应
JS
const { shallowRef, triggerRef } = Vue
const products = shallowRef([
{ id: 1, name: 'iPhone', price: 999 },
{ id: 2, name: 'MacBook', price: 2499 }
])
// ❌ 不响应(修改 .value 内部属性)
products.value[0].name = 'iPad'
// 不会触发更新(shallowRef 只跟踪 .value 整体替换)
// ✅ 触发更新(整体替换)
products.value = [...products.value, { id: 3, name: 'iPad' }]
// ✅ 强制触发(修改内部后手动 trigger)
products.value[0].name = 'iPad'
triggerRef(products)
(2) shallowReactive:浅层 reactive
JS
const { shallowReactive } = Vue
const state = shallowReactive({
user: { name: 'Alice' },
settings: { theme: 'dark' }
})
// ✅ 响应:顶层属性变化
state.user = { name: 'Bob' }
// ❌ 不响应:嵌套属性
state.user.name = 'Charlie'
(3) markRaw:永久不响应
JS
const { markRaw, reactive } = Vue
// 第三方库实例(如 ECharts、Mapbox)不需要响应式
const map = markRaw(new Map())
const chart = markRaw(echarts.init(element))
const state = reactive({
map, // ✅ 不会被 Proxy 包装
chart // ✅ 性能提升 30%
})
(4) 5 大使用场景
| 场景 | 方案 | 性能提升 |
|---|---|---|
| 大数据列表(10万+) | shallowRef | 50x |
| 第三方库实例 | markRaw | 30% |
| 频繁创建/销毁对象 | shallowReactive | 20% |
| 只读配置 | markRaw | 10% |
| 不可变数据 | shallowRef | 5% |
8. 完整示例:响应式原理应用
▶ 示例:响应式工具函数(isRef/isReactive/toRaw/triggerRef)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
.panel { padding: 1rem; border: 1px solid #ddd; margin: 0.5rem 0; border-radius: 4px; }
.tag { display: inline-block; padding: 2px 8px; background: #42b883; color: white; border-radius: 4px; font-size: 0.85rem; margin-right: 4px; }
</style>
<div id="app">
<div class="panel">
<h4>1. isRef / isReactive 判断</h4>
<p>count 是 ref? <span class="tag">{{ String(isRef(count)) }}</span></p>
<p>state 是 reactive? <span class="tag">{{ String(isReactive(state)) }}</span></p>
<p>plain 是 ref? <span class="tag">{{ String(isRef(plain)) }}</span></p>
</div>
<div class="panel">
<h4>2. toRaw 取原始对象(去除 Proxy)</h4>
<p>state.raw === state? <span class="tag">{{ String(state.raw === state) }}</span></p>
<p>state.count = {{ state.count }}</p>
<button @click="state.count++">+1 改 state.count</button>
</div>
<div class="panel">
<h4>3. shallowRef 浅响应 + triggerRef 手动触发</h4>
<p>users 第 0 项: {{ users[0].name }}</p>
<button @click="mutateFirst">改 users[0].name(不触发更新)</button>
<button @click="forceUpdate">triggerRef 手动触发</button>
</div>
</div>
<script>
const { createApp, ref, reactive, shallowRef, triggerRef,
isRef, isReactive, toRaw } = Vue
const App = {
setup() {
const count = ref(0)
const state = reactive({ count: 0 })
const plain = { count: 0 }
const users = shallowRef([{ id: 1, name: 'Alice' }])
function mutateFirst() {
// ❌ 修改 .value 内部属性 → 不触发更新
users.value[0].name = 'Bob'
console.log('❌ 改完不触发更新')
}
function forceUpdate() {
// ✅ 手动触发
triggerRef(users)
console.log('✅ triggerRef 已触发')
}
return { count, state, plain, users,
isRef, isReactive,
mutateFirst, forceUpdate }
},
template: `
<div>
<div class="panel">
<h4>1. isRef / isReactive 判断</h4>
<p>count 是 ref? <span class="tag">{{ String(isRef(count)) }}</span></p>
<p>state 是 reactive? <span class="tag">{{ String(isReactive(state)) }}</span></p>
<p>plain 是 ref? <span class="tag">{{ String(isRef(plain)) }}</span></p>
</div>
<div class="panel">
<h4>2. toRaw 取原始对象</h4>
<p>state.count = {{ state.count }}</p>
<button @click="state.count++">+1</button>
</div>
<div class="panel">
<h4>3. shallowRef + triggerRef</h4>
<p>users 第 0 项: {{ users[0].name }}</p>
<button @click="mutateFirst">改内部属性(不触发)</button>
<button @click="forceUpdate">triggerRef 手动触发</button>
</div>
</div>
`
}
createApp(App).mount('#app')
</script>
▶ 示例:track + trigger 简化版(手写 mini Vue)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<h3>手写 mini 响应式系统</h3>
<p>state.count = {{ display }}</p>
<button @click="state.count++">+1</button>
</div>
<script>
// mini Vue 响应式系统实现
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
if (activeEffect) dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) dep.forEach(effect => effect())
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return Reflect.get(target, key)
},
set(target, key, value) {
const result = Reflect.set(target, key, value)
trigger(target, key)
return result
}
})
}
// 演示
const state = reactive({ count: 0 })
let display = 0
activeEffect = () => { display = state.count; console.log('Render:', display) }
activeEffect() // 初次收集依赖
// 绑定到页面
const App = {
setup() { return { state, display } },
template: `
<div>
<p>state.count = {{ display }}</p>
<button @click="state.count++">+1(触发 track/trigger)</button>
</div>
`
}
createApp(App).mount('#app')
</script>
▶ 示例:5 大性能对比
| 模式 | 10万行性能 | 内存 | 适用 |
|---|---|---|---|
ref([]) |
100ms | 100MB | 小数据 |
reactive({...}) |
100ms | 100MB | 嵌套对象 |
shallowRef([]) |
5ms | 5MB | 大列表 |
shallowReactive({...}) |
30ms | 30MB | 浅对象 |
markRaw({...}) |
2ms | 2MB | 第三方实例 |
▶ 示例:ref + reactive 内部结构
JS
const { ref, reactive, shallowRef, markRaw, isRef, isReactive, toRaw } = Vue
// ref:包装任何值
const count = ref(0)
console.log(isRef(count)) // true
// reactive:包装对象
const state = reactive({ count: 0 })
console.log(isReactive(state)) // true
// shallowRef:浅层
const list = shallowRef([1, 2, 3])
// markRaw:永久不响应
const map = markRaw(new Map())
// toRaw:去除 Proxy
const raw = toRaw(state)
console.log(raw === state) // false
▶ 示例:5 个常见失效根因
| 失效场景 | 根因 | 解决 |
|---|---|---|
| 整体替换 reactive 对象 | reactive 引用变了 | 用 ref 包装或修改属性 |
| 解构丢失响应 | 解构出原始值 | 用 toRefs |
| 数组直接赋值 index | 内部属性被 Proxy 拦截 | 改用 splice 或整体替换 |
| 异步赋值不响应 | 异步函数脱离 track | 用 ref/reactive 包装 |
| 嵌套对象属性不响应 | Vue 3 已修复(Vue 2 需 Vue.set) | 自动响应 |
▶ 示例:5 大调试技巧(汇总)
JS
const { triggerRef, isRef, isReactive, isReadonly, isProxy, toRaw, markRaw } = Vue
// 1. watchEffect 打印依赖
watchEffect(() => console.log('依赖:', { a: state.a, b: state.b }))
// 2. 手动触发更新
triggerRef(stateRef)
// 3. 检查响应式状态
isReactive(obj) // true/false
isRef(refObj) // true/false
// 4. 原始对象(去除 Proxy)
const raw = toRaw(reactiveObj)
// 5. 性能标记
const fast = markRaw(expensiveObj)
❓ 常见问题
Q Vue 3 为什么用 Proxy 而不是 Object.defineProperty?
A Proxy 原生支持数组、Map、Set,能拦截更多操作(13 种 vs 2 种),添加新属性自动响应,性能更好(懒代理)。唯一缺点是不支持 IE11。
Q track 和 trigger 在哪里调用?
A track 在 reactive 对象的 get 方法中调用(get 时记录依赖)。trigger 在 set 方法中调用(set 时通知所有依赖)。
Q shallowRef 和 ref 的区别?
A
ref 是深度响应(嵌套对象也是 Proxy)。shallowRef 只跟踪 .value 整体替换,不处理内部属性。适合大列表(10万+ 行)。Q computed 怎么知道依赖变了?
A computed 内部用 effect 包装 getter。当 getter 中用到的响应式数据变化时,effect 触发,重置 dirty 标志,下次访问时重算。
Q watch 和 watchEffect 内部实现差异?
A watch 显式指定 source(依赖),回调拿到 newVal/oldVal。watchEffect 不指定 source,自动追踪函数体内的依赖,回调只拿新值。
Q shallowRef 修改内部属性如何触发更新?
A 用
triggerRef(shallowRef) 手动触发。场景:批量修改内部属性后强制刷新视图。Q reactive 包装的 Proxy 在控制台显示什么?
A 显示
Proxy { ... } 包装对象,看不到原对象的属性。用 toRaw(proxyObj) 获取原始对象。📖 小节
- Vue 3 响应式核心:Proxy 拦截 + track(依赖收集)+ trigger(触发更新)
- Proxy 比 Object.defineProperty 强大:原生支持数组/Map/Set、自动响应新属性
- ref 内部:RefImpl 类,get/set 时 track/trigger
- computed 内部:dirty 标志 + lazy 计算 + 缓存
- 5 大性能优化:shallowRef / shallowReactive / markRaw
- 5 个常见响应式失效根因:替换 / 解构 / 数组 / 异步 / 嵌套
📝 作业
-
基础题(难度⭐) 实现一个简化版的响应式系统(基于 Proxy):
- 5 行 reactive 函数
- 5 行 track + trigger
- 演示:set 100 次 → 触发 effect 1 次(批处理)
-
进阶题(难度⭐⭐) 用 shallowRef + markRaw 优化大列表:
- 1000 项的 products 列表(用 shallowRef)
- 包含第三方库实例(用 markRaw)
- 测量内存和 FPS 提升
-
挑战题(难度⭐⭐⭐) 实现一个完整的"依赖追踪调试器":
- watchEffect 打印所有依赖
- triggerRef 手动触发更新
- toRaw / isReactive / isRef 工具函数演示
- 5 种失效场景的根因分析(ref 替换 / 解构 / 数组 / 异步 / 嵌套)
- 性能基准:ref vs shallowRef vs markRaw 内存对比