在上一篇文章中,我们对Proxy有了一些认识后,似乎有些明白了Vue3为什么在实现响应式系统使用Proxy来代替了Object.defineProperty,下面我们来详细看下Proxy在功能、性能和开发体验上有哪些改进?
1. Vue3的Proxy实现原理
Vue3通过Proxy对象实现响应式数据。采用代理的方式对目标对象进行拦截,进行一些列自定义操作,从而达到读取和修改目标对象,并触发对应钩子函数的目的。Proxy对象涉及2个参数:target和handler。target是被劫持的目标对象。handler是用于声明代理target的指定行为对象,它支持的拦截操作一共有13种,包括getPrototypeOf、setPrototypeOf、isExtensible、preventExtensions、getOwnPropertyDescriptor、defineProperty、has、get、set、deleteProperty、ownKeys、apply和construct。其中,get方法用于拦截对象属性的读取。set方法用于拦截对象属性的设置,它返回一个布尔值,用于判断设置成功或失败。
与Vue2相比,除了代理方式不同以外,核心逻辑基本一致,读取属性时收集依赖,设置属性时更新派发,通过发布订阅者模式实现数据的响应式。
(1)响应式对象的创建
通过 reactive() 函数将普通对象转换为响应式对象:
import { reactive } from 'vue';
const obj = reactive({ count: 0 });
由上可知,reactive方法传入一个对象,通过Proxy对象进行属性的拦截,因此reactive()函数的代码实现如下:
function reactive(obj){
//{count: 0}
return new Proxy(obj,reactiveHandlers)
}
obj传入的data对象,此时需代理的对象为{count: 0} ,拦截方法为reactiveHandlers(),下面对该对象进行set和get拦截。
(2)依赖收集与派发更新
依赖收集和更新派发前,不能匿名收集对应的effect副作用函数,因为对象在派发更新的时候无法知道与effect副作用函数的映射关系,所以在收集前需要通过一个队列将对象、属性和effect副作用函数进行映射。这样才能保证对象的属性更新时,能够准群定位effect副作用函数并执行。在这种情况下,reactiveHandlers()函数内部无论是get还是set均存在映射的逻辑。整个get和set的方法实现逻辑如下:
对于get方法:
(1)进行依赖收集。
(2)收集完成后返回需要的值
对于set方法:
(1)派发更新
(2)继续赋值操作。
针对get的实现,如果有读取操作,则需要进行依赖收集。在Vue3中,依赖收集比较复杂,通过obj->key->effect的顺序建立依赖收集关系链,进而实现对象和effect副作用函数的关联关系。通过该关系既可以收集依赖,也可以对已收集的依赖进行缓存。
针对set的实现,如果有修改操作,则需要触发effect副作用函数的执行。根据缓存的映射关系,调用effect副作用函数后再完成修改操作。
根据上述逻辑编写拦截函数,代码实现如下:
const reactiveHandlers = {
get(target,key){
const value = getDep(target,key).value
//判断当前得到的数据是对象还是其他类型,如果是对象,则需要继续拆解
if(value && typeof value === 'object'){
return reactive(value)
}else{
//完成递归后,返回对应的值
return value
}
},
set(target,key,value){
getDep(target,key).value = value
}
}
上述代码使用getDep()函数保存映射关系。通过getDep()函数获取对象的value值,在对象值读取完成后,进一步判断对象值是否为对象,若为对象则需要继续调用reactive递归遍历进行依赖收集,保证所有的嵌套对象都能受到代理。
getDep()函数处理依赖收集和派发更新,该函数内部实现逻辑如下:
//通过WeakMap结构保存映射关系
const targetToHashMap = new WeakMap()
function getDep(target,key){
//确认是否有该对象的代理
let depMap = targetToHashMap.get(target)
//如果没有进行依赖收集
if(!depMap){
depMap = new Map();
targetToHashMap.set(target,depMap)
}
//确认对应的key是否有依赖收集
let dep = depMap.get(key)
if(!dep){
//如果没有,则进行依赖收集,存储结构如下:{new WeakMap(key1: new Dep(val),key2: new Dep(val))}
dep = new Dep(target[key])
depMap.set(key,dep)
}
return dep
}
上述代码通过全局变量targetToHashMap来保存这个映射关系,此处targetToHashMap的类型是WeakMap,该类型是ES6新增的原生数据结构,关于WeakMap的具体特性不展开介绍,这里简单介绍一个WeakMap的用法。WeakMap的key必须为Object,并且提供delete,get,has和set方法。该类型有一个特性,对应的key销毁后可直接释放内存,因此采用该类型作为缓存对象,缓存内容删除后立即释放内存。根据WeakMap的特点,选择WeakMap类型缓存映射关系,无需关系内存释放等问题。
前面已经介绍过,为了实现target->key->effect的映射关系,全局缓存空间已声明,再通过get方法判断target是都已经缓存完成。若已完成,则直接跳过缓存target的步骤;若没有完成,则再定义一个target作为键,Map类型对象作为值,用于缓存dep变量,截止该步骤WeapMap结构为{ target : new Map()}。
注:Map 对象也是一键值对的格式进行存储,与Object最大的不同是,Map的key可以是任何值。
此处选择Map结构作为键值对存储,也是考虑到key的变化,正好符合Map结构的特性,截止该步骤,Map结构为{key : effect}。
此时需要建立target和key之间的关系,此处先判断 target 的 new Map() 内是否有key的缓存,若没有则保存key,对应的值为effect副作用函数,进行该步操作后,结构为 { target1: { key1: effect1}} ,通过两层键值对映射,建立起 target->key->effect之间的关联关系。
effect副作用函数的内容在该函数内未体现,而是通过 dep 实例实现,该实例通过Dep类定义get 和set 方法,涉及get依赖收集 和 set 派发更新方法,effect副作用函数的映射也在该Dep类内完成。下面查看该类实现逻辑:
let activeEffect
class Dep{
subscribers = new Set();
constructor(value){
this._value = value
}
get value(){
//进行依赖收集
this.depend()
//完成依赖收集后再返回对应的值
return this._value
}
set value(value){
this._value = value
//派发更新
this.notify()
}
depend(){
//依赖收集,将全局的activeEffect方法进行保存,以便后续执行
if(activeEffect){
this.subscribers.add(activeEffect)
}
}
notify(){
//派发更新,循环缓存的subscribers数组,执行effect副作用函数
this.subscribers.forEach((effect) => {
effect()
})
}
}
上述Dep类实现如下:定义get和set拦截方法,get方法内通过depend方法依赖收集,完成依赖收集后返回对应的值,set方法内将值更新,再通过notify方法进行派发更新。
2. Proxy 与 Object.defineProperty 的区别
特性 | proxy (Vue 3) | Object.defineProperty (Vue 2) |
---|---|---|
拦截能力 | 支持 13 种操作(如 get、set、deleteProperty 等) | 仅支持 get 和 set,无法拦截 delete、in 等操作 |
新增/删除属性 | 自动检测,无需特殊 API | 需通过 Vue.set/Vue.delete 手动触发更新 |
数组监听 | 直接监听数组索引修改、push/pop 等方法 | 需重写数组方法(如 push、pop)进行拦截 |
性能 | 初始化时无需递归遍历所有属性,按需转换 | 初始化时递归遍历对象属性,性能较差 |
嵌套对象处理 | 惰性响应式,访问时递归转换 | 初始化时递归转换所有嵌套对象 |
浏览器兼容性 | 支持现代浏览器,不兼容 IE11 | 兼容 IE9+ |
3. Proxy 的核心优势
(1) 更全面的拦截能力
支持动态新增属性:无需手动调用 Vue.set,直接赋值 obj.newProp = 1 即可触发更新。
支持删除属性:delete obj.prop 会触发 deleteProperty 拦截。
支持数组索引修改:arr[0] = 1 直接触发更新,无需重写数组方法。
(2) 性能优化
按需响应式:仅在访问对象属性时递归转换为响应式,减少初始化开销。
批量更新:通过调度器合并多次数据变更,避免重复渲染。
(3) 简化代码逻辑
统一拦截入口:所有操作通过 Proxy 的 Handler 处理,无需像 Vue 2 那样为对象和数组分别实现响应式。