从 Vue2 到 Vue3 ,响应式系统最大的变化就是从
Object.defineProperty换成了Proxy。这个改变解决了 Vue2 中数组检测、属性新增等痛点,也为 Vue3 带来了更好的性能和更灵活的响应式能力。
前言:Vue2响应式的局限性
开篇之前,我们先来谈谈 Vue2 响应式的局限性:
- 无法检测属性的添加
- 无法检测数组索引赋值
- 无法检测数组长度变化
const vm = new Vue({
data: {
user: { name: '张三' },
items: ['a', 'b', 'c']
}
});
// 1. 无法检测属性的添加
vm.user.age = 25; // 不是响应式的
// 2. 无法检测数组索引赋值
vm.items[0] = 'x'; // 不会触发更新
// 3. 无法检测数组长度变化
vm.items.length = 0; // 不会触发更新
如果想解决以上问题,可以借助 Vue.set() 或 vm.$set() 等特殊方式处理:
Vue.set(vm.user, 'age', 25);
vm.$set(vm.user, 'age', 25);
这些限制的根本原因在于 Object.defineProperty 只能拦截对象的属性访问,无法拦截整个对象。而 Proxy 的出现彻底改变了这一切。
Proxy的基本原理和优势
在我的 JavaScript核心机制探秘 专栏中,有一篇文章 JavaScript Proxy与Reflect 专门讲解了 JS 中的 Proxy 和 Reflect,对这两块内容有疑惑的,可以直接查阅这篇文章。
Proxy的基本原理
Proxy对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等),有以下特点:
- 可以拦截13种操作(Vue3 响应式系统中,用到了5种)
- 可以代理整个对象,而不只是属性
- 可以代理数组、函数等所有类型
- 可以动态拦截新增属性
简易 Proxy 示例
const obj = {foo: 1};
const p = new Proxy(obj, {
get(target, key){
track(target, key); // 依赖收集
return target[key]; // 没有使用Reflect.get完成读取
},
set(target, key, newVal){
target[key] = newVal; // 没有使用Reflect.set完成设置
trigger(target, key); // 触发更新
}
})
上述代码是一个实现响应式的最基本的代码,在 get 和 set 拦截中,都是使用原始对象 target 来完成对属性的操作的,这样做会有什么问题吗?
Reflect在响应式中的应用
为什么需要Reflect?
对于上一章的问题,本节可以给出答案:直接使用原始对象 target 来完成对属性的操作,存在以下问题:
- Proxy拦截器中需要调用原始操作,但直接操作target可能绕过拦截
- 对于某些操作(如 in、delete等),没有对应的函数形式
因此,我们需要统一的API处理对象操作,而 Reflect 正好提供了与 Proxy 拦截器一一对应的静态方法。
完整的 set/get 拦截方法
const obj = {foo: 1};
const p = new Proxy(obj, {
// 接收第三个参数 receiver,即代理后的对象 p
get(target, key, receiver){
track(target, key); // 依赖收集
return Reflect.get(target, key, receiver);
},
set(target, key, newVal, receiver){
// 先获取旧值
const oldVal = target[key];
const res = Reflect.set(target, key, newVal, receiver);
if(oldVal !== newVal){
// 新旧值不同时,才触发更新
trigger(target, key);
}
return res;
}
})
其他方法
has 拦截
当我们需要判断对象或原型上是否存在给定的属性 key 时:key in obj 需要对该操作进行拦截,Proxy 中提供的对应的拦截方法为 has :
const obj = {foo: 1};
const p = new Proxy(obj, {
has(target, key){
track(target, key); // 依赖收集
return Reflect.has(target, key);
},
})
owenKeys 拦截
当我们需要对对象进行遍历 for...in 时,需要对该操作进行拦截,Proxy 中提供的对应的拦截方法为 owenKeys :
const obj = {foo: 1};
const ITERATE_KEY = Symbol();
const p = new Proxy(obj, {
owenKeys(target){
// 将副作用函数与 ITERATE_KEY 关联
track(target, ITERATE_KEY); // 依赖收集
return Reflect.owenKeys(target);
},
})
deleteProperty 拦截
当我们需要拦截删除属性的操作时,可以使用 deleteProperty 拦截:
const obj = {foo: 1};
const p = new Proxy(obj, {
deleteProperty(target, key){
return Reflect.deleteProperty(target, key);
},
})
Proxy vs Object.defineProperty
Object.defineProperty 的优缺点
- 优点:
- 浏览器兼容性好(IE9+)
- 性能开销较小
- 缺点:
- 只能拦截属性,不能拦截整个对象
- 无法检测属性添加和删除
- 数组的变异方法需要 hack
- 需要递归遍历所有属性
Proxy 的优缺点
- 优点:
- 可以拦截整个对象的操作、
- 支持动态添加属性
- 数组无需特殊处理
- 13种拦截方法,功能强大
- 性能更好(现代引擎优化)
- 缺点:
- 兼容性稍差(不支持IE)
- 无法被polyfill
reactive 实现
reactive 执行流程
- 检查目标是否是对象,如果不是则直接返回
- 检查目标是否已经被代理,如果是则返回已有代理
- 创建Proxy处理器(包含get、set等方法)
- 创建Proxy实例
- 缓存Proxy实例(用于避免重复代理)
- 返回Proxy
代理对象的身份标识
const ReactiveFlags = {
IS_REACTIVE: '__v_isReactive',
IS_READONLY: '__v_isReadonly',
IS_SHALLOW: '__v_isShallow',
RAW: '__v_raw'
};
IS_READONLY、IS_SHALLOW 等标识用来标记可读对象和浅代理,在后面的文章中会详细讲解。
深层对象代理的实现
Vue3 的响应性系统中,采用懒代理的方式,实现深层对象的代理,即:只在访问时才代理嵌套对象:
const p = new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 只在需要时进行代理(懒代理)
if (typeof result === 'object' && result !== null && !result.__isReactive) {
return reactive(result); // 注:这里需要完整的reactive函数
}
return result;
},
});
reactive 完整实现
// 定义常量
const ReactiveFlags = {
IS_REACTIVE: '__v_isReactive',
IS_READONLY: '__v_isReadonly',
IS_SHALLOW: '__v_isShallow',
RAW: '__v_raw'
};
// 工具函数
function isObject(val) {
return val !== null && typeof val === 'object';
}
function isReactive(value) {
return !!(value && value[ReactiveFlags.IS_REACTIVE]);
}
function toRaw(observed) {
return observed && observed[ReactiveFlags.RAW] || observed;
}
// 缓存Map
const reactiveMap = new WeakMap();
// 创建reactive处理器
const mutableHandlers = {
get(target, key, receiver) {
// 处理特殊标记
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
if (key === ReactiveFlags.RAW) {
return target;
}
const result = Reflect.get(target, key, receiver);
track(target, key); // 依赖收集
// 懒代理嵌套对象
if (isObject(result)) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key); // 触发更新
return result;
},
has(target, key) {
return Reflect.has(target, key);
},
deleteProperty(target, key) {
return Reflect.deleteProperty(target, key);
},
ownKeys(target) {
return Reflect.ownKeys(target);
}
};
// 主reactive函数
function reactive(target) {
// 1. 只处理对象
if (!isObject(target)) {
return target;
}
// 2. 如果已经是响应式对象,直接返回
if (isReactive(target)) {
return target;
}
// 3. 检查缓存
const existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 4. 创建代理
const proxy = new Proxy(target, mutableHandlers);
// 5. 存入缓存
reactiveMap.set(target, proxy);
// 6. 返回Proxy代理对象
return proxy;
}
结语
本篇文章实现了一个相对较为完整的 reactive 响应式系统,讲解了了 Vue2 响应式的局限性和 Proxy 的优势,以及 Reflect 在响应式中的重要作用。下篇文章,我们将讲解 reactive 相关的工具函数集,实现和 Vue3 源码完全对标的 reactive 响应式系统。
对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!