Vue3 响应式原理

前言

Vue3 的响应式系统是整个框架的核心,与 Vue2 基于 Object.defineProperty 的方案不同,Vue3 全面采用 Proxy 实现数据劫持。本文将带你深入理解 reactive、ref 和 effect 三大核心 API 的实现原理。

Vue2 响应式的局限性

先回顾一下 Vue2 的响应式实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Vue2:Object.defineProperty
function defineReactive(obj, key) {
let val = obj[key]
Object.defineProperty(obj, key, {
get() {
console.log(`读取了 ${key}`)
return val
},
set(newVal) {
console.log(`设置了 ${key}`)
val = newVal
}
})
}

这种方式存在几个明显的问题:

  1. 无法检测对象属性的添加和删除 — 必须使用 Vue.set / Vue.delete
  2. 无法直接监听数组索引和 length 变化 — 需要重写数组的 7 个方法(push、pop、shift 等)
  3. 初始化时需要递归遍历所有属性 — 嵌套深对象性能开销大

Vue3 核心:Proxy 劫持

Proxy 可以拦截对象几乎所有的操作,弥补了 Object.defineProperty 的短板。

reactive 的实现

reactive 是 Vue3 响应式的基础 API,将一个普通对象转换为响应式代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 存储依赖的 WeakMap
const targetMap = new WeakMap()
let activeEffect = null

function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
console.log(`读取了属性: ${String(key)}`)
// 依赖收集
track(target, key)
const result = Reflect.get(target, key, receiver)
// 如果是对象,深度代理
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
console.log(`设置了属性: ${String(key)}`)
// 触发更新
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey) {
console.log(`删除了属性: ${String(key)}`)
trigger(target, key)
}
return result
}
})
}

Vue3 使用 Reflect API 来确保 this 指向的正确性,这是与手写 Proxy 的一个重要区别。

依赖收集与触发:track & trigger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function track(target, key) {
if (!activeEffect) return
// 获取 target 对应的依赖表
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取 key 对应的副作用集合
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
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())
}
}

数据结构关系如下:

层级 存储结构 键 值
第一层 WeakMap target 对象 depsMap
第二层 Map 属性名 key dep (Set)
第三层 Set — effect 副作用函数

effect:调度副作用

1
2
3
4
5
6
7
8
9
function effect(fn) {
const _effect = () => {
activeEffect = _effect
fn()
activeEffect = null
}
_effect()
return _effect
}

effect 会立即执行一次传入的函数,在函数执行过程中读取到的响应式数据会通过 track 把当前 effect 收集为依赖。数据变化时 trigger 会重新执行这些 effect。

ref 的实现

ref 本质是对 reactive 的一层包装,用于处理基本类型值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ref(value) {
return {
get value() {
track(this, 'value')
return value
},
set value(newValue) {
if (newValue !== value) {
value = newValue
trigger(this, 'value')
}
}
}
}

在 Vue3 模板中使用 ref 时,会自动解包 .value,在 setup 函数中则需要手动 .value 访问。

computed 的简化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function computed(getter) {
let cached
let dirty = true

const _effect = effect(() => {
cached = getter()
dirty = false
})

return {
get value() {
if (dirty) {
cached = getter()
dirty = false
}
return cached
}
}
}

computed 的核心是缓存:依赖不变时直接返回缓存值,避免重复计算。

总结

Vue3 响应式系统相对于 Vue2 的关键改进:

  1. Proxy 替代 Object.defineProperty — 能检测属性增删、数组变化,无需额外 API
  2. 懒代理 — 只在访问嵌套对象时才递归代理,初始化性能更好
  3. 清晰的依赖收集模型 — track / trigger 分离,WeakMap → Map → Set 三级结构一目了然
  4. Reflect 保证 this 正确 — 避免了 Proxy 中 this 指代混乱的问题

理解响应式原理对于日常开发中排查渲染问题、优化性能以及面试都至关重要。


内容由 AI 生成,仅供参考。本文发布于 码上学习,转载请注明出处。