核心原理: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.defineProperty 和 Proxy 要解决的问题。
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.defineProperty | Proxy |
|---|---|---|
| 劫持粒度 | 属性级别 — 逐个劫持每个属性 | 对象级别 — 代理整个对象 |
| 新增属性 | ❌ 无法检测 | ✅ 自动拦截 |
| 删除属性 | ❌ 无法检测 | ✅ 通过 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 的方法与 Proxy 的 trap 一一对应,它们配合使用可以确保:
- 正确的
this绑定(通过receiver参数) - 返回值语义一致(如
Reflect.set返回布尔值表示成功或失败) - 代码更健壮(避免手动操作时可能的边界问题)
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
}
}
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 的 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 | ✅ 自动拦截 |
| 数组索引修改 | ❌ 需要 splice 或 Vue.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 浏览器兼容性
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 对比
| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 任意类型(原始值 + 对象) | 仅对象类型 |
| 访问方式 | .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 指向。
核心区别:
- 劫持粒度:defineProperty 是属性级别,Proxy 是对象级别
- 新增/删除属性:defineProperty 无法检测,Proxy 可以
- 数组支持:defineProperty 需要 hack(重写方法),Proxy 原生支持
- 初始化方式:defineProperty 递归遍历,Proxy 懒代理(访问时才处理)
- 兼容性:defineProperty 支持 IE9+,Proxy 不支持 IE
Q2:为什么 Vue 3 用 Proxy 替代 Object.defineProperty?
参考回答:
Object.defineProperty 有几个无法克服的天然缺陷:
- 无法检测属性新增和删除:需要额外的
Vue.set/Vue.deleteAPI,增加了心智负担 - 数组监听有缺陷:无法直接监听索引修改和 length 变化,需要重写 7 个数组方法
- 初始化性能差:需要递归遍历整个对象,即使某些属性从未被使用
- 不支持 Map/Set 等数据结构
Proxy 在对象层面代理,天然解决了以上所有问题,代码更简洁、性能更好、开发体验更优。唯一的代价是放弃了 IE 支持,但这在现代前端开发中是可接受的。
Q3:Proxy 可以被 polyfill 吗?为什么?
参考回答:
不能完美 polyfill。
Proxy 需要 JavaScript 引擎级别的支持,它的核心能力(拦截对象操作)无法通过普通 JavaScript 代码模拟。虽然 Google 有一个 proxy-polyfill 库,但它只支持 get、set、apply、construct 四个 trap,且有很多限制(如不支持 deleteProperty、has、ownKeys 等)。
这也是 Vue 3 彻底放弃 IE 支持的根本原因——没有妥协方案。
Q4:Vue 3 的 reactive 和 ref 有什么区别?什么时候用哪个?
参考回答:
| 维度 | ref | reactive |
|---|---|---|
| 支持类型 | 任意类型 | 仅对象/数组 |
| 实现原理 | 通过 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() { ... }
})
})
但问题是:
- 数组可能非常大,给每个索引都定义 getter/setter 性能开销太高
- 数组的
push/pop等操作会改变 length 和索引,需要频繁重新定义 - 用户可能随时通过索引添加元素(如
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
}
好处:
- 初始化速度快——创建一个 Proxy 比递归遍历整个对象快得多
- 按需处理——从未被访问的深层数据不会被代理,节省内存和 CPU
- 对大型数据结构特别友好
Q8:Reflect 在 Vue 3 响应式中的作用是什么?
参考回答:
Reflect 在 Vue 3 中配合 Proxy 使用,主要有两个作用:
-
确保正确的
this指向:通过传递receiver参数,保证在涉及原型链继承时,getter/setter 中的this指向正确的对象 -
规范化的返回值:
Reflect.set返回布尔值表示操作是否成功,比直接赋值更便于错误处理
如果不用 Reflect,在某些涉及继承和 getter/setter 的场景下,this 的指向会出错,导致依赖收集和触发更新不正确。
Q9:Vue 2 的 Vue.set 内部是怎么实现的?
参考回答:
Vue.set(target, key, value) 的实现逻辑如下:
- 如果 target 是数组且 key 是有效索引 → 使用
splice方法(已被重写,能触发更新) - 如果 key 已经存在于 target 中 → 直接赋值(已有 getter/setter,自动触发更新)
- 如果 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. 总结
核心知识点回顾:
- Object.defineProperty 是属性级别的劫持,通过 getter/setter 拦截读写操作。Vue 2 基于它构建了 Observer-Dep-Watcher 的响应式系统
- Proxy 是对象级别的代理,能拦截对象上的所有操作(get、set、delete、has、ownKeys 等)。Vue 3 基于它构建了 track-trigger-effect 的响应式系统
- Vue 3 选择 Proxy 的根本原因是 解决 Object.defineProperty 的天然局限:无法检测新增/删除属性、数组监听有缺陷、初始化性能差
- Proxy 的懒代理策略比 Object.defineProperty 的递归遍历有显著的性能优势
- Reflect 配合 Proxy 使用,确保正确的
this指向和规范化的返回值 ref用 getter/setter 包装原始值,reactive用 Proxy 代理对象——两者是互补关系