Vue 3 响应式原理
大白话解释: Vue 3 的响应式就像给对象"换了一个门卫"(Proxy)。所有对对象的操作(读取、修改、删除、检查)都要经过这个门卫,门卫会记录"谁来过",然后在数据变化时通知他们。
Proxy vs defineProperty 的区别:
- defineProperty:只能在门口装监控,只能监控已有的属性,新增的看不到
- Proxy:整个对象都被监控,任何操作都能拦截
为什么 Vue 3 要换用 Proxy?
- 自动检测新增属性:不用再写
this.$set了 - 自动检测数组变化:
arr[0] = 'new'能触发更新了 - 性能更好:惰性代理,按需访问,不会一开始就递归所有属性
Vue 3 使用 Proxy 代理对象的读写操作,在读取时收集依赖(track),在修改时触发更新(trigger)。相比 Vue 2 的 Object.defineProperty,解决了新增属性、数组索引等检测问题。
Proxy vs Object.defineProperty
| 特性 | Vue 2 (defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 新增属性 | 需要 $set | 自动检测 |
| 删除属性 | 需要 $delete | 自动检测 |
| 数组索引 | 无法检测 | 自动检测 |
| 数组 length | 无法检测 | 自动检测 |
| 性能 | 初始化递归全部属性 | 惰性代理,按需访问 |
| 兼容性 | IE9+ | 不支持 IE |
| 实现方式 | 劫持属性的 getter/setter | 代理整个对象 |
Proxy 基础
ts
// 创建原始对象
const raw = { name: '张三', age: 25 }
// 创建 Proxy 代理
const proxy = new Proxy(raw, {
// 拦截属性读取
get(target, key, receiver) {
console.log(`读取 ${String(key)}`) // 记录读取操作
return Reflect.get(target, key, receiver) // 返回原始值
},
// 拦截属性设置
set(target, key, value, receiver) {
console.log(`设置 ${String(key)} = ${value}`) // 记录设置操作
const result = Reflect.set(target, key, value, receiver) // 设置新值
return result
},
// 拦截属性删除
deleteProperty(target, key) {
console.log(`删除 ${String(key)}`) // 记录删除操作
return Reflect.deleteProperty(target, key) // 删除属性
},
// 拦截 in 操作符
has(target, key) {
console.log(`检查 ${String(key)} 是否存在`) // 记录检查操作
return Reflect.has(target, key) // 返回检查结果
},
})
// 测试代理
proxy.name // 读取 name
proxy.age = 26 // 设置 age
delete proxy.age // 删除 age
'name' in proxy // 检查 name手写响应式系统
完整实现(简化版)
注意:以下实现是简化版,用于理解核心原理。Vue 3 实际实现更复杂,包含依赖清理、嵌套 effect、调度器等功能。
ts
// ===== 依赖管理 =====
type EffectFn = () => void // 副作用函数类型
// 当前正在执行的副作用函数,用于依赖收集
let activeEffect: EffectFn | null = null
// 依赖收集桶:使用 WeakMap 存储对象到其属性依赖的映射
// 结构:target → key → effects(副作用函数集合)
const targetMap = new WeakMap<object, Map<string | symbol, Set<EffectFn>>>()
// 收集依赖:当读取属性时,将当前副作用函数收集到依赖中
function track(target: object, key: string | symbol) {
if (!activeEffect) return // 没有正在执行的副作用函数,无需收集
// 获取或创建 target 的依赖 Map
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)
}
// 触发更新:当属性变化时,执行所有相关的副作用函数
function trigger(target: object, key: string | symbol) {
const depsMap = targetMap.get(target) // 获取 target 的依赖 Map
if (!depsMap) return // 没有依赖,直接返回
const deps = depsMap.get(key) // 获取属性的依赖集合
if (deps) {
deps.forEach((effect) => {
// 避免无限循环:如果副作用函数是当前正在执行的,跳过
if (effect !== activeEffect) {
effect() // 执行副作用函数
}
})
}
}
// ===== reactive:将对象转换为响应式 =====
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
// 拦截属性读取
get(target, key, receiver) {
track(target, key) // 收集依赖
const result = Reflect.get(target, key, receiver) // 获取原始值
// 惰性代理:读取到的嵌套对象也转为 reactive
if (typeof result === 'object' && result !== null) {
return reactive(result) // 递归代理嵌套对象
}
return result
},
// 拦截属性设置
set(target, key, value, receiver) {
const oldValue = (target as any)[key] // 获取旧值
const result = Reflect.set(target, key, value, receiver) // 设置新值
// 值变化时才触发更新
if (oldValue !== value) {
trigger(target, key) // 触发更新
}
return result
},
// 拦截属性删除
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key) // 删除属性
trigger(target, key) // 触发更新
return result
},
})
}
// ===== ref:将值包装为响应式引用 =====
// 注意:这是简化版实现,Vue 3 实际使用 class 实现,支持更多特性
function ref<T>(value: T) {
// 用闭包保存当前值,支持 .value 整体替换
let inner = typeof value === 'object' && value !== null
? reactive(value as object) // 对象类型自动转为 reactive
: value // 基本类型直接保存
const refObj = {} // 创建 ref 对象
// 使用 defineProperty 定义 .value 属性
Object.defineProperty(refObj, 'value', {
get() {
track(refObj, 'value') // 收集依赖
return inner // 返回当前值
},
set(newValue) {
if (newValue === inner) return // 值未变化,跳过
// 对象类型自动 reactive
inner = typeof newValue === 'object' && newValue !== null
? reactive(newValue as object) // 新对象转为 reactive
: newValue // 基本类型直接保存
trigger(refObj, 'value') // 触发更新
},
})
return refObj // 返回 ref 对象
}
// ===== effect(副作用函数)=====
// 执行副作用函数,并自动收集依赖
function effect(fn: EffectFn) {
activeEffect = fn // 设置当前正在执行的副作用函数
fn() // 执行函数,触发 getter 收集依赖
activeEffect = null // 清空当前副作用函数
}
// ===== 使用示例 =====
// 创建响应式对象
const state = reactive({ count: 0, name: '张三' })
// 注册副作用函数,自动收集依赖
effect(() => {
console.log(`count is: ${state.count}`) // 读取 state.count,触发依赖收集
})
// 修改响应式数据,触发更新
state.count = 1 // 输出: count is: 1
state.count = 2 // 输出: count is: 2惰性代理(Lazy Proxy)
Vue 3 的 Proxy 是惰性的:只有在读取嵌套对象时才会将其转为 Proxy。
ts
// 创建嵌套对象
const state = reactive({
user: {
profile: {
address: {
city: '北京',
},
},
},
})
// 此时只有 state 本身是 Proxy
// state.user 不是 Proxy,直到你读取它
console.log(state.user) // 读取时,state.user 被转为 Proxy
// 现在 state.user 也是 Proxy 了
console.log(state.user.profile) // 读取时,state.user.profile 被转为 Proxy
// 现在 state.user.profile 也是 Proxy 了💡 这就是 Vue 3 性能更好的原因:不需要初始化时就递归代理所有属性。
注意:上面的简化实现中
get每次读取都会递归调用reactive,实际 Vue 3 源码会缓存已代理的对象,避免重复创建 Proxy。
shallowReactive / shallowRef
只代理第一层,不深度递归。
ts
import { shallowReactive, shallowRef } from 'vue' // 导入浅层响应式 API
// shallowReactive —— 只有第一层是响应式的
const state = shallowReactive({
name: '张三', // ✅ 响应式:第一层属性
nested: {
count: 0, // ❌ 不是响应式的:嵌套对象
},
})
state.name = '李四' // ✅ 触发更新:修改第一层属性
state.nested.count = 1 // ❌ 不触发更新:修改嵌套属性
// shallowRef —— 只有 .value 是响应式的
const data = shallowRef({ count: 0 }) // 创建浅层 ref
data.value = { count: 1 } // ✅ 整体替换触发更新
data.value.count = 2 // ❌ 不触发更新:修改内部属性💡 适合大型数据结构只关心引用变化的场景,如从接口返回的大列表。
readonly / shallowReadonly
ts
import { reactive, readonly, shallowReadonly } from 'vue' // 导入只读 API
// 创建响应式对象
const original = reactive({ count: 0, user: { name: '张三' } })
// 深度只读 —— 所有层级都不能修改
const readonlyState = readonly(original) // 创建深度只读代理
readonlyState.count = 1 // 警告!修改失败,控制台输出警告
// 浅层只读 —— 只有第一层不能修改
const shallowReadonlyState = shallowReadonly(original) // 创建浅层只读代理
shallowReadonlyState.count = 1 // 警告!修改失败,第一层只读
shallowReadonlyState.user.name = '李四' // ✅ 可以修改,嵌套层可写💡
provide/inject中常用readonly防止后代篡改数据。
toRef / toRefs
toRef —— 将 reactive 对象的某个属性转为 ref
ts
import { reactive, toRef } from 'vue' // 导入 reactive 和 toRef
// 创建响应式对象
const state = reactive({ count: 0, name: '张三' })
// toRef 创建一个与原始属性同步的 ref
const countRef = toRef(state, 'count') // 将 state.count 转为 ref
countRef.value++ // state.count 也变为 1
state.count++ // countRef.value 也变为 2toRefs —— 将 reactive 对象的所有属性转为 ref
ts
import { reactive, toRefs } from 'vue' // 导入 reactive 和 toRefs
// 创建响应式对象
const state = reactive({ count: 0, name: '张三' })
// 解构时保持响应性:将每个属性转为 ref
const { count, name } = toRefs(state)
// count 是 Ref<number>,name 是 Ref<string>
count.value++ // state.count 也变了,保持同步💡 Composables 中返回 reactive 对象时,常用
toRefs让调用方可以安全解构。
toRaw / markRaw
ts
import { reactive, toRaw, markRaw } from 'vue' // 导入 toRaw 和 markRaw
// toRaw —— 获取代理背后的原始对象
const state = reactive({ count: 0 }) // 创建响应式对象
const raw = toRaw(state) // 获取原始对象,不是 Proxy
raw.count = 1 // 不触发响应式更新
// markRaw —— 标记对象永远不被代理
const plain = markRaw({ nested: {} }) // 标记为非代理
const state2 = reactive({ data: plain }) // 包含非代理对象
console.log(state2.data === plain) // true,不是 Proxy💡
markRaw适合不需要响应式的大对象,如第三方库实例、大型配置对象。
响应式丢失的常见场景
1. 解构 reactive 对象
ts
const state = reactive({ count: 0, name: '张三' }) // 创建响应式对象
// ❌ 解构后丢失响应性
const { count, name } = state // count 和 name 是普通值,不是响应式
// ✅ 用 toRefs:保持响应性
const { count: countRef, name: nameRef } = toRefs(state) // countRef 和 nameRef 是 Ref 类型2. 替换 reactive 对象
ts
let state = reactive({ count: 0 }) // 创建响应式对象
state = reactive({ count: 1 }) // ❌ 旧引用(如模板绑定)仍指向原对象,不会更新
// ✅ 修改属性:直接修改属性值
state.count = 1
// ✅ 或用 ref:ref 可以整体替换 .value
const stateRef = ref({ count: 0 }) // 用 ref 包装对象
stateRef.value = { count: 1 } // ✅ ref 可以替换 .value,保持响应性3. 传递 reactive 属性给函数
ts
const state = reactive({ count: 0 }) // 创建响应式对象
// ❌ 传递的是值,不是引用
function increment(count: number) {
count++ // 不会影响 state.count,只是局部变量
}
// ✅ 传递整个 reactive 对象
function increment(state: { count: number }) {
state.count++ // 修改原始对象
}
// ✅ 或传递 ref:使用 toRef 创建 ref
const count = toRef(state, 'count') // 将 state.count 转为 ref
function increment(count: Ref<number>) {
count.value++ // 修改 ref,同步更新 state.count
}4. 数组中的对象替换
ts
const list = reactive([{ id: 1, name: 'a' }]) // 创建响应式数组
// ✅ 直接修改属性:触发更新
list[0].name = 'b' // 修改对象属性
// ✅ 整体替换数组项:触发更新
list[0] = { id: 1, name: 'b' } // 替换整个对象
// ✅ push / splice:触发更新
list.push({ id: 2, name: 'c' }) // 添加新元素调试响应式
ts
import { onRenderTracked, onRenderTriggered, ref, reactive } from 'vue' // 导入调试钩子
// 追踪依赖收集:当组件渲染时收集依赖
// 注意:以下钩子仅在开发模式下有效,生产环境会被移除
onRenderTracked((event) => {
console.log('跟踪:', event) // 输出依赖收集事件
})
// 追踪触发更新:当响应式数据变化触发重新渲染时
onRenderTriggered((event) => {
console.log('触发:', event) // 输出触发更新事件
// event 包含:target(原始对象)、key(变化的属性)、oldValue、newValue
console.log('详细信息:', event.target, event.key, event.oldValue, event.newValue)
})