reactive的Proxy实现:非原始值的响应式方案

9 阅读6分钟

从 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 中的 ProxyReflect,对这两块内容有疑惑的,可以直接查阅这篇文章。

Proxy的基本原理

Proxy对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等),有以下特点:

  1. 可以拦截13种操作(Vue3 响应式系统中,用到了5种)
  2. 可以代理整个对象,而不只是属性
  3. 可以代理数组、函数等所有类型
  4. 可以动态拦截新增属性

简易 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 执行流程

  1. 检查目标是否是对象,如果不是则直接返回
  2. 检查目标是否已经被代理,如果是则返回已有代理
  3. 创建Proxy处理器(包含get、set等方法)
  4. 创建Proxy实例
  5. 缓存Proxy实例(用于避免重复代理)
  6. 返回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 响应式系统。

对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!