跳到主要内容

核心原理:Proxy 与 Object.defineProperty

Vue 的响应式系统是整个框架的基石。当你修改一个数据,页面自动更新——这背后的"魔法"正是数据代理(数据劫持)。Vue 2 使用 Object.defineProperty,Vue 3 则全面升级为 Proxy。本文将深入讲解这两种方案的原理、差异,以及 Vue 3 为什么要做这次迁移。

1. 什么是响应式?

先用一个最简单的例子感受"响应式"的含义:

// 普通 JavaScript 对象
const data = { message: 'Hello' }

// 修改数据后,页面不会自动更新
data.message = 'World'
// 你需要手动操作 DOM 来更新页面...
document.querySelector('#app').textContent = data.message

但在 Vue 中,你只需要修改数据,页面就会自动更新:

<template>
<p>{{ message }}</p>
</template>

<script setup>
import { ref } from 'vue'
const message = ref('Hello')

// 修改数据,页面自动更新!
message.value = 'World'
</script>

这就是响应式——数据变化时,自动通知所有依赖它的地方进行更新

要实现这套机制,核心在于第二步——如何拦截数据的读取和修改?这就是 Object.definePropertyProxy 要解决的问题。

2. Object.defineProperty(Vue 2 方案)

2.1 基本用法

Object.defineProperty 是 ES5 提供的 API,可以为对象的属性定义 getter 和 setter:

const data = {}
let internalValue = 'Hello'

Object.defineProperty(data, 'message', {
enumerable: true,
configurable: true,
get() {
console.log('读取了 message')
return internalValue
},
set(newVal) {
console.log('修改了 message:', newVal)
internalValue = newVal
}
})

data.message // 控制台输出: "读取了 message"
data.message = 'World' // 控制台输出: "修改了 message: World"

通过 getter/setter,我们可以在数据被读取时收集依赖,在数据被修改时触发更新

2.2 Vue 2 的响应式实现

Vue 2 的响应式系统由三个核心角色组成:

Observer:递归劫持所有属性

class Observer {
constructor(value) {
this.walk(value)
}

walk(obj) {
// 遍历对象的每个属性
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}

function defineReactive(obj, key, val) {
const dep = new Dep() // 每个属性都有一个 Dep 实例

// 如果属性值是对象,递归处理
if (typeof val === 'object' && val !== null) {
new Observer(val)
}

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖:把当前的 Watcher 加入 Dep
if (Dep.target) {
dep.depend()
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
// 新值如果是对象,也需要递归劫持
if (typeof newVal === 'object' && newVal !== null) {
new Observer(newVal)
}
// 通知所有 Watcher 更新
dep.notify()
}
})
}

Dep:依赖管理器

class Dep {
constructor() {
this.subs = [] // 存放所有订阅的 Watcher
}

depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}

notify() {
this.subs.forEach(watcher => watcher.update())
}
}

// 全局变量,指向当前正在收集依赖的 Watcher
Dep.target = null

Watcher:观察者

class Watcher {
constructor(getter, callback) {
this.getter = getter
this.callback = callback
this.value = this.get() // 首次求值,触发依赖收集
}

get() {
Dep.target = this // 将自身设为当前 Watcher
const value = this.getter() // 读取数据,触发 getter → 收集依赖
Dep.target = null // 清除
return value
}

update() {
const oldValue = this.value
this.value = this.get()
this.callback(this.value, oldValue)
}
}

2.3 完整的依赖收集流程

一句话总结

getter 里收集依赖,setter 里触发更新——这就是 Vue 2 响应式的核心。

2.4 Object.defineProperty 的局限性

虽然 Object.defineProperty 成功支撑了 Vue 2 多年,但它有几个无法克服的天然缺陷

缺陷 1:无法检测属性的新增和删除

const vm = new Vue({
data: {
user: { name: '张三' }
}
})

// ❌ 新增属性 —— 不是响应式的!
vm.user.age = 18
// 页面不会更新,因为 age 没有被 defineProperty 劫持

// ❌ 删除属性 —— 不是响应式的!
delete vm.user.name
// 页面不会更新

原因Object.defineProperty 只能劫持已存在的属性。新增的属性没有被 defineProperty 处理过,自然没有 getter/setter。

Vue 2 的应对方案

// 使用 Vue.set 添加响应式属性
Vue.set(vm.user, 'age', 18)
// 或
this.$set(this.user, 'age', 18)

// 使用 Vue.delete 删除属性
Vue.delete(vm.user, 'name')
// 或
this.$delete(this.user, 'name')

缺陷 2:无法直接监听数组索引变化和 length

const vm = new Vue({
data: {
list: ['a', 'b', 'c']
}
})

// ❌ 通过索引修改数组 —— 不是响应式的!
vm.list[0] = 'x'
// 页面不会更新

// ❌ 修改数组长度 —— 不是响应式的!
vm.list.length = 1
// 页面不会更新

Vue 2 的应对方案:重写数组的 7 个变异方法。

// Vue 2 内部重写了这些方法:
const methodsToPatch = [
'push', 'pop', 'shift', 'unshift',
'splice', 'sort', 'reverse'
]

methodsToPatch.forEach(method => {
const original = Array.prototype[method]
arrayMethods[method] = function (...args) {
const result = original.apply(this, args)
const ob = this.__ob__

// 如果是 push/unshift/splice,新插入的元素也需要响应式处理
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)

// 触发更新
ob.dep.notify()
return result
}
})
// 所以你需要用这些方法来操作数组:
vm.list.push('d') // ✅ 响应式
vm.list.splice(0, 1, 'x') // ✅ 响应式

// 或者用 Vue.set:
Vue.set(vm.list, 0, 'x') // ✅ 响应式

缺陷 3:初始化时递归遍历,性能开销大

const data = {
user: {
profile: {
address: {
city: '北京',
district: {
name: '海淀',
// ... 更深的嵌套
}
}
}
}
}

// Vue 2 在初始化时会递归遍历所有层级
// 即使某些深层数据从未被使用,也会被劫持
// 对于大型对象,这个过程可能很慢

缺陷 4:无法监听 Map、Set 等数据结构

// Vue 2 的 Object.defineProperty 无法代理 Map、Set
const data = {
myMap: new Map(),
mySet: new Set()
}

// 这些数据结构在 Vue 2 中不是响应式的

2.5 缺陷总结

缺陷描述Vue 2 的临时方案
新增/删除属性无法检测Vue.set / Vue.delete
数组索引修改无法检测重写 7 个变异方法
性能开销初始化递归遍历无法根本解决
Map/Set完全不支持
核心痛点

这些局限不是 Vue 的问题,而是 Object.defineProperty 这个 API 的天然局限。它只能针对已有属性定义 getter/setter,无法拦截对象层面的操作(新增属性、删除属性、数组操作等)。

3. Proxy(Vue 3 方案)

3.1 基本用法

Proxy 是 ES6(ES2015)引入的 API,可以创建一个对象的代理,拦截对该对象的各种操作:

const data = { message: 'Hello' }

const proxy = new Proxy(data, {
get(target, key, receiver) {
console.log(`读取了 ${key}`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`修改了 ${key}:`, value)
const result = Reflect.set(target, key, value, receiver)
return result
}
})

proxy.message // 控制台输出: "读取了 message"
proxy.message = 'World' // 控制台输出: "修改了 message: World"

3.2 Proxy vs Object.defineProperty 直观对比

两者最根本的区别在于劫持的粒度不同

维度Object.definePropertyProxy
劫持粒度属性级别 — 逐个劫持每个属性对象级别 — 代理整个对象
新增属性❌ 无法检测✅ 自动拦截
删除属性❌ 无法检测✅ 通过 deleteProperty 拦截
数组操作❌ 需要 hack✅ 原生支持
Map/Set❌ 不支持✅ 支持
初始化递归遍历所有属性懒代理,访问时才处理
浏览器支持IE9+不支持 IE

3.3 Proxy 支持的拦截操作(Trap)

Proxy 支持 13 种拦截操作,Vue 3 主要使用其中几种:

const handler = {
// 拦截属性读取
get(target, key, receiver) { },

// 拦截属性设置
set(target, key, value, receiver) { },

// 拦截 delete 操作
deleteProperty(target, key) { },

// 拦截 key in obj 操作
has(target, key) { },

// 拦截 Object.keys()、for...in 等
ownKeys(target) { },

// ... 还有 apply、construct 等
}

3.4 解决 Object.defineProperty 的所有痛点

新增属性 —— 自动响应

const data = reactive({ name: '张三' })

// ✅ 直接添加新属性,自动响应式!
data.age = 18
// 页面自动更新,不需要 Vue.set
// 原理:Proxy 的 set trap 能拦截所有属性赋值
const proxy = new Proxy(target, {
set(target, key, value, receiver) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.set(target, key, value, receiver)

if (!hadKey) {
// 新增属性 → trigger ADD
console.log('新增了属性:', key)
} else {
// 修改属性 → trigger SET
console.log('修改了属性:', key)
}
return result
}
})

删除属性 —— 自动响应

const data = reactive({ name: '张三', age: 18 })

// ✅ 直接删除属性,自动响应式!
delete data.age
// 页面自动更新,不需要 Vue.delete
// 原理:Proxy 的 deleteProperty trap
const proxy = new Proxy(target, {
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('删除了属性:', key)
// 触发更新
return result
}
})

数组操作 —— 全面支持

const list = reactive(['a', 'b', 'c'])

// ✅ 通过索引修改 —— 自动响应式!
list[0] = 'x'

// ✅ 修改 length —— 自动响应式!
list.length = 1

// ✅ 所有数组方法 —— 自动响应式!
list.push('d')
list.splice(1, 1)

Map/Set —— 原生支持

const myMap = reactive(new Map())
const mySet = reactive(new Set())

// ✅ Map 操作 —— 响应式!
myMap.set('key', 'value')
myMap.delete('key')

// ✅ Set 操作 —— 响应式!
mySet.add('item')
mySet.delete('item')

3.5 为什么要配合 Reflect?

在 Vue 3 的 Proxy handler 中,你会看到大量 Reflect 的使用:

const proxy = new Proxy(target, {
get(target, key, receiver) {
// 为什么不直接 return target[key]?
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
// 为什么不直接 target[key] = value?
return Reflect.set(target, key, value, receiver)
}
})

原因是 receiver 参数确保了 this 指向正确。看下面的例子:

const parent = {
get value() {
return this._value // this 应该指向谁?
}
}

const child = Object.create(parent)
child._value = 42

const proxy = new Proxy(parent, {
get(target, key, receiver) {
// ❌ 错误做法:target[key]
// this 会指向 parent,读不到 child._value
// return target[key]

// ✅ 正确做法:Reflect.get(target, key, receiver)
// receiver 是 child 的代理,this 指向正确
return Reflect.get(target, key, receiver)
}
})

// 当通过 child 的代理访问 value 时
// receiver 确保 getter 中的 this 指向 child(而非 parent)
Reflect 的作用

Reflect 的方法与 Proxy 的 trap 一一对应,它们配合使用可以确保:

  1. 正确的 this 绑定(通过 receiver 参数)
  2. 返回值语义一致(如 Reflect.set 返回布尔值表示成功或失败)
  3. 代码更健壮(避免手动操作时可能的边界问题)

4. Vue 3 响应式系统源码解析

4.1 reactive 函数

reactive 是 Vue 3 中创建响应式对象的核心 API:

// 简化版实现
function reactive(target) {
// 如果已经是响应式对象,直接返回
if (target.__v_isReactive) return target

return createReactiveObject(target)
}

function createReactiveObject(target) {
const proxy = new Proxy(target, {
get: createGetter(),
set: createSetter(),
deleteProperty: deleteProperty,
has: has,
ownKeys: ownKeys
})
return proxy
}

4.2 get 拦截器(依赖收集)

function createGetter() {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)

// 收集依赖
track(target, key)

// 如果值是对象,递归创建响应式(懒代理!)
if (typeof res === 'object' && res !== null) {
return reactive(res)
}

return res
}
}
懒代理 vs 递归遍历

Vue 2:初始化时递归遍历整个对象,所有属性一次性全部劫持。 Vue 3:只有当属性被实际访问时,才会对其值创建响应式代理(懒代理)。

这意味着如果一个深层嵌套对象的某些属性从未被读取,它们就不会被代理,大幅减少了初始化开销。

4.3 set 拦截器(触发更新)

function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key]
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.set(target, key, value, receiver)

// 只在值真正变化时触发更新
if (!hadKey) {
// 新增属性
trigger(target, 'add', key, value)
} else if (value !== oldValue) {
// 修改属性
trigger(target, 'set', key, value, oldValue)
}

return result
}
}

4.4 track 与 trigger:依赖收集与触发更新

Vue 3 使用 WeakMap → Map → Set 的三级结构来管理依赖关系:

// 全局依赖映射
const targetMap = new WeakMap()

// 收集依赖
function track(target, key) {
if (!activeEffect) return

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)
}

// 触发更新
function trigger(target, type, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return

const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
为什么用 WeakMap?

WeakMap 的 key 是弱引用。当响应式对象不再被引用时,它在 targetMap 中的记录会被垃圾回收器自动清除,避免内存泄漏。

4.5 effect:副作用函数

Vue 3 用 effect 替代了 Vue 2 的 Watcher

let activeEffect = null

function effect(fn) {
const effectFn = () => {
activeEffect = effectFn
fn() // 执行函数,触发 get → track
activeEffect = null
}
effectFn() // 首次执行,收集依赖
return effectFn
}
// 使用示例
const data = reactive({ count: 0 })

effect(() => {
// 这个函数中访问了 data.count
// 所以 data.count 的 dep 会收集这个 effect
console.log('count 变了:', data.count)
})

data.count = 1 // 触发 set → trigger → 重新执行 effect
// 输出: "count 变了: 1"

4.6 完整流程图

5. 深入对比:两套方案的差异

5.1 初始化性能

// 一个包含 1000 个属性的大对象
const bigData = {}
for (let i = 0; i < 1000; i++) {
bigData[`key${i}`] = { nested: { deep: i } }
}

// Vue 2:初始化时遍历 1000 个属性,
// 每个属性的嵌套对象也递归处理
// 总共调用 defineProperty 3000+ 次

// Vue 3:创建一个 Proxy 即可
// 只有实际访问的属性才会被代理

5.2 运行时性能

操作Vue 2(defineProperty)Vue 3(Proxy)
读取已有属性getter 直接返回Proxy get trap
修改已有属性setter 触发更新Proxy set trap
新增属性❌ 需要 Vue.set✅ 自动拦截
删除属性❌ 需要 Vue.delete✅ 自动拦截
数组索引修改❌ 需要 spliceVue.set✅ 自动拦截
遍历(for...in无法拦截ownKeys trap
in 操作符无法拦截has trap

5.3 代码量对比

// ========== Vue 2 的数组处理 ==========
// 需要约 80 行代码来重写数组方法

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push', 'pop', 'shift', 'unshift',
'splice', 'sort', 'reverse'
]
// ... 大量重写逻辑

// ========== Vue 3 的数组处理 ==========
// Proxy 天然支持,无需额外代码

const arr = reactive([1, 2, 3])
arr[0] = 99 // ✅ 自动响应
arr.push(4) // ✅ 自动响应
arr.length = 0 // ✅ 自动响应

5.4 浏览器兼容性

关于 IE 兼容

Proxy 是无法被 polyfill 的——因为它需要引擎级别的支持,JavaScript 代码无法模拟拦截对象操作的能力。这也是 Vue 3 彻底放弃 IE 支持的根本原因。

6. 手写简易响应式系统

为了加深理解,我们分别实现两个简易版响应式系统。

6.1 基于 Object.defineProperty(简易 Vue 2 风格)

// ====== 简易版 Vue 2 响应式 ======

// 当前正在执行的副作用函数
let activeEffect = null

// 依赖管理
class Dep {
constructor() {
this.subscribers = new Set()
}

depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}

notify() {
this.subscribers.forEach(effect => effect())
}
}

// 将对象变成响应式
function observe(obj) {
Object.keys(obj).forEach(key => {
let internalValue = obj[key]
const dep = new Dep()

// 如果值是对象,递归处理
if (typeof internalValue === 'object' && internalValue !== null) {
observe(internalValue)
}

Object.defineProperty(obj, key, {
get() {
dep.depend()
return internalValue
},
set(newVal) {
if (newVal === internalValue) return
internalValue = newVal
if (typeof newVal === 'object' && newVal !== null) {
observe(newVal)
}
dep.notify()
}
})
})
return obj
}

// 副作用函数
function watchEffect(fn) {
activeEffect = fn
fn()
activeEffect = null
}

// === 测试 ===
const state = observe({ count: 0, message: 'hello' })

watchEffect(() => {
console.log('count 变了:', state.count)
})
// 输出: "count 变了: 0"

state.count = 1 // 输出: "count 变了: 1"
state.count = 2 // 输出: "count 变了: 2"

// ❌ 新增属性不会触发更新
state.newProp = 'test' // 没有输出

6.2 基于 Proxy(简易 Vue 3 风格)

// ====== 简易版 Vue 3 响应式 ======

let activeEffect = null
const targetMap = new WeakMap()

function track(target, key) {
if (!activeEffect) return

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)
}

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(target) {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key)

// 懒代理:值是对象时递归创建代理
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) {
trigger(target, key)
}
return result
},

deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)

if (hadKey && result) {
trigger(target, key)
}
return result
}
})
}

function watchEffect(fn) {
activeEffect = fn
fn()
activeEffect = null
}

// === 测试 ===
const state = reactive({ count: 0, list: [1, 2, 3] })

watchEffect(() => {
console.log('count 变了:', state.count)
})
// 输出: "count 变了: 0"

state.count = 1 // 输出: "count 变了: 1"

// ✅ 新增属性 —— 自动响应
watchEffect(() => {
console.log('name:', state.name)
})
state.name = '张三' // 输出: "name: 张三"

// ✅ 数组操作 —— 自动响应
watchEffect(() => {
console.log('list:', state.list.join(','))
})
state.list[0] = 99 // 输出: "list: 99,2,3"
state.list.push(4) // 输出: "list: 99,2,3,4"

7. ref 与 reactive 的关系

在 Vue 3 中,除了 reactive,还有一个重要的 API:ref

7.1 为什么需要 ref?

Proxy 只能代理对象,无法代理原始值(string、number、boolean 等):

// ❌ 无法代理原始值
let count = 0
const proxy = new Proxy(count, { ... })
// TypeError: Cannot create proxy with a non-object as target

所以 Vue 3 用 ref 将原始值包装成对象:

// ref 的简化实现
function ref(value) {
return {
get value() {
track(this, 'value')
return value
},
set value(newVal) {
if (newVal !== value) {
value = newVal
trigger(this, 'value')
}
}
}
}
const count = ref(0)

// 通过 .value 访问,触发 get
console.log(count.value) // 0

// 通过 .value 赋值,触发 set → 更新视图
count.value = 1

7.2 ref vs reactive 对比

特性refreactive
适用类型任意类型(原始值 + 对象)仅对象类型
访问方式.value直接访问属性
实现方式getter/setter(类似 defineProperty)Proxy
模板中使用自动解包,不需要 .value直接使用
赋值特性可以整体替换不能整体替换(会丢失响应式)
import { ref, reactive } from 'vue'

// ref:适合单一值
const count = ref(0)
const name = ref('张三')
count.value++

// reactive:适合对象/数组
const state = reactive({
user: { name: '张三', age: 18 },
list: [1, 2, 3]
})
state.user.name = '李四'

// ⚠️ reactive 的陷阱:不能整体替换
let state2 = reactive({ count: 0 })
state2 = reactive({ count: 1 }) // ❌ 丢失响应式!
// 应该修改属性,而不是替换整个对象

8. 面试常见问题

Q1:Vue 2 和 Vue 3 的响应式原理有什么区别?

参考回答:

Vue 2 使用 Object.defineProperty 实现响应式,它通过递归遍历 data 对象的所有属性,为每个属性定义 getter 和 setter。getter 中收集依赖(Dep + Watcher),setter 中触发更新。

Vue 3 使用 Proxy 实现响应式,它在对象层面创建代理,能拦截对象的所有操作(读取、设置、删除、遍历等)。配合 Reflect 确保正确的 this 指向。

核心区别:

  1. 劫持粒度:defineProperty 是属性级别,Proxy 是对象级别
  2. 新增/删除属性:defineProperty 无法检测,Proxy 可以
  3. 数组支持:defineProperty 需要 hack(重写方法),Proxy 原生支持
  4. 初始化方式:defineProperty 递归遍历,Proxy 懒代理(访问时才处理)
  5. 兼容性:defineProperty 支持 IE9+,Proxy 不支持 IE

Q2:为什么 Vue 3 用 Proxy 替代 Object.defineProperty?

参考回答:

Object.defineProperty 有几个无法克服的天然缺陷:

  1. 无法检测属性新增和删除:需要额外的 Vue.set / Vue.delete API,增加了心智负担
  2. 数组监听有缺陷:无法直接监听索引修改和 length 变化,需要重写 7 个数组方法
  3. 初始化性能差:需要递归遍历整个对象,即使某些属性从未被使用
  4. 不支持 Map/Set 等数据结构

Proxy 在对象层面代理,天然解决了以上所有问题,代码更简洁、性能更好、开发体验更优。唯一的代价是放弃了 IE 支持,但这在现代前端开发中是可接受的。


Q3:Proxy 可以被 polyfill 吗?为什么?

参考回答:

不能完美 polyfill

Proxy 需要 JavaScript 引擎级别的支持,它的核心能力(拦截对象操作)无法通过普通 JavaScript 代码模拟。虽然 Google 有一个 proxy-polyfill 库,但它只支持 getsetapplyconstruct 四个 trap,且有很多限制(如不支持 deletePropertyhasownKeys 等)。

这也是 Vue 3 彻底放弃 IE 支持的根本原因——没有妥协方案。


Q4:Vue 3 的 reactive 和 ref 有什么区别?什么时候用哪个?

参考回答:

维度refreactive
支持类型任意类型仅对象/数组
实现原理通过 getter/setter 拦截 .value通过 Proxy 代理整个对象
访问方式.value(模板中自动解包)直接访问属性

使用建议:

  • 原始值(number、string、boolean)→ 用 ref
  • 对象/数组且不需要整体替换 → 用 reactive
  • 拿不准 → 用 ref(它能处理所有类型)

实际项目中,很多团队选择全部使用 ref,因为 reactive 有一个陷阱:不能整体赋值替换,否则会丢失响应式。


Q5:Object.defineProperty 能监听数组吗?为什么 Vue 2 不用它监听数组?

参考回答:

技术上可以——Object.defineProperty 可以通过索引劫持数组元素。但 Vue 2 选择不这么做,原因是性能问题

// 理论上可以这样做
const arr = [1, 2, 3]
arr.forEach((val, index) => {
Object.defineProperty(arr, index, {
get() { ... },
set() { ... }
})
})

但问题是:

  1. 数组可能非常大,给每个索引都定义 getter/setter 性能开销太高
  2. 数组的 push/pop 等操作会改变 length 和索引,需要频繁重新定义
  3. 用户可能随时通过索引添加元素(如 arr[100] = 1),无法预先劫持

所以 Vue 2 选择了重写数组原型方法的方案——只拦截会改变数组的 7 个方法(push、pop、shift、unshift、splice、sort、reverse),作为一种性能和功能的折中。


Q6:Vue 3 响应式中,WeakMap 有什么作用?

参考回答:

Vue 3 的依赖关系存储在 targetMap(一个 WeakMap)中:

targetMap (WeakMap) → target → depsMap (Map) → key → dep (Set of effects)

使用 WeakMap 而非 Map 的原因是避免内存泄漏

  • WeakMap 的 key 是弱引用,不会阻止垃圾回收
  • 当一个响应式对象不再被引用时,它在 targetMap 中的条目会被自动清除
  • 如果用普通 Map,即使对象不再使用,Map 仍然持有引用,对象无法被回收

Q7:什么是懒代理?Vue 3 是如何实现的?

参考回答:

懒代理(Lazy Reactive)是指只在属性被访问时才对其值进行响应式处理,而非初始化时一次性递归处理所有层级。

Vue 3 在 Proxy 的 get 拦截器中实现了懒代理:

get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)

// 只有在实际访问时,才将嵌套对象转为响应式
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
}

好处:

  1. 初始化速度快——创建一个 Proxy 比递归遍历整个对象快得多
  2. 按需处理——从未被访问的深层数据不会被代理,节省内存和 CPU
  3. 对大型数据结构特别友好

Q8:Reflect 在 Vue 3 响应式中的作用是什么?

参考回答:

Reflect 在 Vue 3 中配合 Proxy 使用,主要有两个作用:

  1. 确保正确的 this 指向:通过传递 receiver 参数,保证在涉及原型链继承时,getter/setter 中的 this 指向正确的对象

  2. 规范化的返回值Reflect.set 返回布尔值表示操作是否成功,比直接赋值更便于错误处理

如果不用 Reflect,在某些涉及继承和 getter/setter 的场景下,this 的指向会出错,导致依赖收集和触发更新不正确。


Q9:Vue 2 的 Vue.set 内部是怎么实现的?

参考回答:

Vue.set(target, key, value) 的实现逻辑如下:

  1. 如果 target 是数组且 key 是有效索引 → 使用 splice 方法(已被重写,能触发更新)
  2. 如果 key 已经存在于 target 中 → 直接赋值(已有 getter/setter,自动触发更新)
  3. 如果 key 是新属性 → 调用 defineReactive 为其定义 getter/setter,然后手动调用 ob.dep.notify() 触发更新
function set(target, key, val) {
// 数组
if (Array.isArray(target)) {
target.splice(key, 1, val)
return val
}
// 已有属性
if (key in target) {
target[key] = val
return val
}
// 新增属性
const ob = target.__ob__
defineReactive(target, key, val)
ob.dep.notify()
return val
}

本质上就是手动弥补 Object.defineProperty 无法检测新增属性的缺陷。这也是 Vue 3 使用 Proxy 后不再需要 Vue.set 的原因。

9. 总结

核心知识点回顾:

  1. Object.defineProperty 是属性级别的劫持,通过 getter/setter 拦截读写操作。Vue 2 基于它构建了 Observer-Dep-Watcher 的响应式系统
  2. Proxy 是对象级别的代理,能拦截对象上的所有操作(get、set、delete、has、ownKeys 等)。Vue 3 基于它构建了 track-trigger-effect 的响应式系统
  3. Vue 3 选择 Proxy 的根本原因是 解决 Object.defineProperty 的天然局限:无法检测新增/删除属性、数组监听有缺陷、初始化性能差
  4. Proxy 的懒代理策略比 Object.defineProperty 的递归遍历有显著的性能优势
  5. Reflect 配合 Proxy 使用,确保正确的 this 指向和规范化的返回值
  6. ref 用 getter/setter 包装原始值,reactive 用 Proxy 代理对象——两者是互补关系