外观
vue3响应式原理
约 1589 字大约 5 分钟
2025-10-06
在vue2 中主要使用的是Object.defineProperty
的方式遍历对象实现的响应式,有以下的问题
- 需要依次遍历对象中的每一个属性,添加劫持,如果对象特别大,性能消耗也大
- 无法监听对象中新增的属性,需要手动调用vue.set来实现
而在vue3中主要使用的是proxy来实现响应式更新,并提供ref
和reactive
方法来设置响应式数据
一、proxy
proxy相比较于Object.defineProperty
有以下的优势:
- 无需遍历对象所有属性
Proxy
可以直接代理整个对象,不需要对每个属性逐个defineProperty
。- 对性能提升很大,尤其是大对象。
- 可拦截新增和删除属性
Proxy
可以直接监听set
、deleteProperty
,因此不需要再额外用Vue.set
和Vue.delete
。
- 可以代理数组的索引变化
Proxy
可以拦截arr[0] = xxx
这样的操作,解决了 Vue2 监听数组索引的缺陷。
const handler = {
get(target, key, receiver) {
console.log('get:', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set:', key, '=', value)
return Reflect.set(target, key, value, receiver)
},
deleteProperty(target, key) {
console.log('delete:', key)
return Reflect.deleteProperty(target, key)
}
}
const obj = { a: 1 }
const proxyObj = new Proxy(obj, handler)
proxyObj.a // get: a
proxyObj.b = 2 // set: b = 2
delete proxyObj.a // delete: a
二、 ref 和 reactive
基本原理:
- 基于proxy对数据的进行监听劫持,当调用改数据的时候会触发响应式数据的get,使用track进行依赖收集
- 当修改数据的时候会触发响应式数据的set,使用trigger进行数据更新
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)
}
})
}
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
由上面的ref可以看出,他对输入的数据使用value来进行封装的,这也是为什么使用ref定义的响应式数据需要使用
.value
来进行的原因
2.1 effect 副作用函数
effect 是 Vue3 响应式系统的核心,它用于创建响应式副作用 - 当依赖的响应式数据变化时自动重新执行的函数。
effect 基本原理
- 包装函数:将用户传入的函数包装成 effect 函数
- 设置活跃 effect:在执行前将当前 effect 设置为活跃状态
- 执行原始函数:执行过程中会访问响应式数据,触发依赖收集
- 清理活跃状态:执行完成后清理活跃 effect
let activeEffect = null
function effect(fn) {
// 创建一个包装函数
const effectFn = () => {
try {
// 设置当前活跃的effect
activeEffect = effectFn
// 执行原始函数,触发依赖收集
return fn()
} finally {
// 执行完成后清空
activeEffect = null
}
}
// 设置effect的依赖数组
effectFn.deps = []
// 立即执行一次
effectFn()
return effectFn
}
2.2 track 依赖收集
依赖收集是响应式系统的关键环节,它建立响应式数据与 effect 之间的关联关系。
依赖收集流程:
- 检查活跃 effect:只有存在活跃的 effect 时才进行收集
- 建立映射关系:
- 从
targetMap
中获取 target 对应的depsMap
- 从
depsMap
中获取 key 对应的dep Set
- 从
- 双向记录:
- 将 effect 添加到 dep 中
- 将 dep 添加到 effect 的 deps 数组中(用于后续清理)
track 实现
// 全局依赖存储结构
const targetMap = new WeakMap()
/**
* targetMap 结构:
* {
* [target]: {
* [key]: Set([effect1, effect2, ...])
* }
* }
*/
function track(target, key) {
// 如果没有活跃的effect,直接返回
if (!activeEffect) return
// 获取target对应的depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 获取key对应的dep Set
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
// 如果当前effect还没有被收集,则进行收集
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
// 同时effect也记录自己被哪些dep收集了(用于清理)
activeEffect.deps.push(dep)
}
}
2.3 trigger 数据更新
当响应式数据发生变化时,trigger 负责找到所有依赖该数据的 effect 并执行它们。
更新流程
- 查找依赖:根据 target 和 key 找到所有相关的 effect
- 避免循环:排除当前正在执行的 effect 避免无限循环
- 批量执行:执行所有需要更新的 effect
- 重新收集:effect 执行时会重新收集依赖
trigger 实现
function trigger(target, key) {
// 获取target对应的depsMap
const depsMap = targetMap.get(target)
if (!depsMap) return
// 获取key对应的所有effect
const effects = depsMap.get(key)
if (effects) {
// 创建一个副本避免无限循环
const effectsToRun = new Set(effects)
// 执行所有相关的effect
effectsToRun.forEach(effect => {
// 如果effect不是当前正在执行的effect,则执行
if (effect !== activeEffect) {
effect()
}
})
}
}
三、watch computed watchEffect
这些高级 API 都是基于基础的 effect/track/trigger 机制构建的,通过不同的配置选项和包装逻辑实现了各自的特有行为。
3.1 computed 计算属性
computed 是基于 effect 的懒执行和缓存机制实现的,具有惰性求值和缓存特性。
- 懒执行:只有访问
.value
时才执行计算 - 缓存机制:依赖未变化时直接返回缓存值
- 惰性更新:依赖变化时标记为脏数据,但不立即重新计算
3.2 watch 监听器
watch 是基于 effect 的调度器机制实现的,可以控制副作用的执行时机。
- 灵活的数据源:支持响应式对象、getter 函数、ref 等
- 深度监听:通过递归遍历实现深度监听
- 执行时机控制:支持 sync、pre、post 等执行时机
- 清理机制:提供 onInvalidate 函数用于清理副作用
3.3 watchEffect 即时监听器
watchEffect 是基于 effect 的自动依赖收集机制实现的。
- 自动依赖收集:自动追踪回调中使用的响应式数据
- 立即执行:创建后立即执行一次
- 简化语法:不需要指定监听源
more
由上面的副作用函数-effect就可以实现数据响应式的精准更新,在vue3 中vue vaper就推出了不适用虚拟dom,而是直接操作真实的dom节点的方式来进行页面更新
- effect也是异步更新的
- 可以实现精准的dom更新,不需要使用diff算法来进行虚拟节点的对比