响应式原理深度解析
响应式原理深度解析
Vue 3 的响应式系统是其最核心的能力,也是区别于传统 DOM 操作的关键设计。理解响应式原理,不仅能帮助我们写出更高效的代码,还能在遇到问题时快速定位根因。本文将从 Proxy 基础出发,深入解析 Vue 3 响应式系统的全貌。
从 Object.defineProperty 到 Proxy
Vue 2 使用 Object.defineProperty 实现响应式,这一方案存在几个明显局限:无法监听新增属性、无法监听数组下标、需要递归遍历所有对象。Vue 3 采用了 Proxy 替代,从根本上解决了这些问题。
Proxy 的基本用法
const raw = { name: 'Alice', age: 28 }
// target: 代理目标对象
// key: 被访问的属性名
// value: 新设定的值
const handler = {
get(target, key, receiver) {
console.log(`读取属性: ${key}`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`设置属性: ${key} = ${value}`)
return Reflect.set(target, key, value, receiver)
},
deleteProperty(target, key) {
console.log(`删除属性: ${key}`)
return Reflect.deleteProperty(target, key)
},
has(target, key) {
console.log(`in 运算符检查: ${key}`)
return Reflect.has(target, key)
}
}
const proxy = new Proxy(raw, handler)
proxy.name // 触发 get → 输出: 读取属性: name
proxy.age = 30 // 触发 set → 输出: 设置属性: age = 30
delete proxy.age // 触发 deleteProperty → 输出: 删除属性: age
'name' in proxy // 触发 has → 输出: in 运算符检查: name核心概念:Proxy 会在对象访问的各个环节(读取、赋值、删除、遍历)自动触发预设的拦截函数,这正是 Vue 3 响应式系统的基础。
Proxy 的懒代理特性
与 Vue 2 递归遍历不同,Proxy 是惰性的——只在访问时才会创建深层代理:
const handler = {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
// 仅当属性是对象时才递归代理
if (value !== null && typeof value === 'object') {
return new Proxy(value, handler)
}
return value
}
}
const state = new Proxy({ user: { profile: { name: 'Bob' } } }, handler)
// user 层级才被代理,profile 在访问时才生成代理
console.log(state.user) // Proxy 被创建
console.log(state.user.profile) // 深层 Proxy 被创建Vue 3 响应式核心:reactive 与 ref
reactive 的实现
reactive 本质上是对普通对象包一层 Proxy:
// 简化版 reactive 实现(源码思路)
function reactive(target) {
if (typeof target !== 'object' || target === null) {
console.warn('reactive 只接受对象类型')
return target
}
const handler = {
get(target, key, receiver) {
// 依赖收集:在 get 时追踪当前激活的副作用
track(target, key)
const value = Reflect.get(target, key, receiver)
// 对象类型递归代理
if (isObject(value)) {
return reactive(value)
}
return value
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 只有值真正变化时才触发更新
if (hasChanged(value, oldValue)) {
// 触发依赖的副作用重新执行
trigger(target, key, value, oldValue)
}
return result
}
}
return new Proxy(target, handler)
}
// 使用示例
const state = reactive({
count: 0,
user: {
name: 'Alice',
tags: ['vue', 'typescript']
}
})
state.count++ // 自动触发依赖更新
state.user.name = 'Bob' // 嵌套对象同样响应式ref 的实现
ref 针对基本类型做了特殊处理——用包含 .value 属性的对象来存储值:
// 简化版 ref 实现
class RefImpl {
private _value: any
public dep: Set<Function>
public __v_isRef = true
constructor(value) {
this._value = value
this.dep = new Set()
}
get value() {
// 读取时收集依赖
trackRefValue(this)
return this._value
}
set value(newVal) {
// 新值与旧值不同时触发更新
if (hasChanged(newVal, this._value)) {
this._value = newVal
// 触发所有依赖的副作用
triggerRefValue(this)
}
}
}
function ref(value) {
return new RefImpl(value)
}
// 自动解包:当 ref 作为 reactive 对象的属性时
// Vue 会自动访问其 .value,无需手动处理
const count = ref(0)
const state = reactive({ count })
console.log(state.count) // 自动解包为 0
state.count++ // 等价于 count.value++关键设计:ref 通过
.value保持了对基本类型的响应式能力,而 reactive 的属性如果是 ref,模板中会自动解包——这一设计让两者的使用体验高度统一。
依赖追踪:track 与 trigger
Vue 3 的响应式系统建立在”依赖收集 → 变化通知”两个阶段之上。
依赖收集(track)
当副作用函数读取响应式数据时,Vue 自动将这个函数记录到该数据的依赖列表中:
// 全局维护一个"当前活跃副作用"的引用
let activeEffect: Function | undefined
function track(target, key) {
if (!activeEffect) return // 没有活跃副作用时跳过
// targetMap 结构:target → key → Set<effect>
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect) // 建立 响应式数据 → 副作用 的映射
}
// effect 函数自动收集依赖
function effect(fn: () => void) {
const wrappedFn = () => {
activeEffect = wrappedFn
fn() // 执行期间访问的响应式数据会被自动追踪
activeEffect = undefined
}
wrappedFn()
}
// 使用示例
effect(() => {
// 这里读取了 count,所以此 effect 依赖 count
console.log(`计数:${state.count}`)
// 这里读取了 name,所以也依赖 name
console.log(`名称:${state.user.name}`)
})变化通知(trigger)
当响应式数据被修改时,Vue 从依赖图中查找所有依赖它的副作用并触发执行:
function trigger(target, key, value, oldValue) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (!dep) return
// 依次执行所有依赖的副作用
dep.forEach(effect => {
// scheduler 用于控制更新时机(如 nextTick)
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
})
}依赖图结构:
targetMap维护了「响应式对象 → 属性名 → 副作用集合」的三层映射,使得任意数据变化都能精准找到需要重新执行的代码。
computed 的响应式实现
computed 本质上是一个带有缓存机制的响应式数据:
class ComputedRefImpl {
private _value: any // 缓存值
private _dirty = true // 缓存是否过期
private effect: ReactiveEffect
constructor(getter, private _setter) {
// 创建一个带有 scheduler 的 effect
this.effect = new ReactiveEffect(getter, () => {
// 依赖变化时标记缓存过期,但不立即重新计算
this._dirty = true
// 通知依赖此 computed 的下游更新
triggerRefValue(this)
})
}
get value() {
trackRefValue(this) // 收集当前活跃副作用对此 computed 的依赖
if (this._dirty) {
this._dirty = false
this._value = this.effect.run() // 懒计算,结果存入缓存
}
return this._value
}
}
function computed(getter, options?) {
return new ComputedRefImpl(getter, options?.set)
}
// 使用示例
const double = computed(() => state.count * 2)
// 第一次访问:执行 getter,结果被缓存
console.log(double.value) // 计算一次
state.count = 5
// count 变化后 _dirty 变为 true
// 第二次访问:检测到 _dirty,重新计算
console.log(double.value) // 重新计算,得到 10缓存原理:
_dirty标志位确保 getter 只在依赖真正变化时才重新执行,这是computed与普通effect的本质区别。
watch 与 watchEffect
watch 和 watchEffect 都用于响应副作用,但触发时机不同:
<script setup>
import { ref, watch, watchEffect, reactive } from 'vue'
const count = ref(0)
const user = reactive({ name: 'Alice', age: 28 })
// watchEffect:立即执行,自动追踪依赖
watchEffect(() => {
// 自动追踪 count 和 user.name
console.log(`[watchEffect] ${user.name} 的计数:${count.value}`)
})
// watch:惰性执行,精确指定监听目标
watch(count, (newVal, oldVal) => {
console.log(`[watch count] ${oldVal} → ${newVal}`)
}, { immediate: false })
// 监听多个数据源
watch([count, () => user.name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`[watch multiple] count: ${oldCount}→${newCount}, name: ${oldName}→${newName}`)
})
// deep 监听:深入对象内部
watch(() => ({ ...user }), (newVal, oldVal) => {
console.log('user 对象深层变化')
}, { deep: true })
function increment() {
count.value++
}
</script>选择建议:
watchEffect适合副作用逻辑简单、依赖明确的场景;watch适合需要获取旧值、惰性触发、或精确控制触发时机的场景。
副作用清理
在组件卸载或监听器停止时,需要清理副作用防止内存泄漏:
// 在 effect 中返回清理函数
effect(() => {
const timer = setInterval(() => {
console.log(state.count)
}, 1000)
// 返回清理函数:组件卸载或重新运行时自动调用
return () => clearInterval(timer)
})
// watch 的清理
const stop = watch(count, (newVal) => {
if (newVal > 100) {
console.log('达到上限,停止监听')
stop() // 显式调用停止函数
}
})
// onInvalidate(在 watchEffect 中)
watchEffect((onInvalidate) => {
const source = axios.CancelToken.source()
axios.get('/api/data', { cancelToken: source.token })
.then(res => console.log(res))
// 请求取消时清理
onInvalidate(() => source.cancel('请求已取消'))
})常见陷阱与规避
响应式丢失:解构与赋值
const state = reactive({ count: 0, name: 'Alice' })
// ❌ 解构后失去响应式
const { count, name } = state
count++ // 原 state.count 不变!
// ✅ 使用 toRefs 保持响应式
import { toRefs } from 'vue'
const { count, name } = toRefs(state)
count.value++ // 响应式正常工作
// ✅ 或使用 toRef 单独处理
import { toRef } from 'vue'
const count = toRef(state, 'count')
// ❌ 将响应式对象赋值给普通变量
const another = state
another.count = 99 // ❌ 丢失代理能力
// ✅ 使用 shallowReactive 创建浅层响应式
import { shallowReactive } from 'vue'
const shallow = shallowReactive({ deep: { value: 1 } })
shallow.deep.value = 2 // 不触发深层更新!数组响应式的特殊处理
const list = reactive([1, 2, 3])
// ❌ 直接替换数组(丢失响应式)
list = [4, 5, 6] // 禁止!reactive 不允许重新赋值整个对象
// ✅ 使用数组方法(响应式)
list.push(4) // 自动触发更新
list.splice(1, 1) // 删除第二个元素
list.length = 0 // 清空数组(触发更新)
// ✅ 替换整个数组的正确方式
Object.assign(list, [4, 5, 6]) // 原地替换,保留代理
// ✅ 使用 readonly 防止修改
import { readonly } from 'vue'
const safe = readonly(state) // 任何修改操作抛出警告总结
Vue 3 的响应式系统以 Proxy 为基石,构建了”精准追踪 → 批量更新 → 自动解包”的完整链路:
- Proxy 替代
Object.defineProperty,支持新增属性、数组下标、懒代理 - reactive 包装普通对象为 Proxy,
ref用.value包装基本类型 - 依赖追踪:通过
targetMap建立「数据 → 属性 → 副作用」的三层映射 - computed 以
_dirty标志实现惰性求值与缓存 - 常见陷阱:解构丢失响应式、赋值替换对象、数组方法误用
下一篇文章我们将学习 条件渲染与列表渲染,探讨 v-if、v-show、v-for 的使用场景与性能差异。
评论
Written by
AI-Writer