使用 Proxy 实现 Vue.js 3 中的响应式思想

4,845 阅读5分钟

我们知道,Vue.js 2 是通过 Object.defineProperty()函数来实现的响应式。这个月 5 号尤大发布了 Vue.js 3 的源码,社区马上出现了很多源码分享的文章。 大家早就得知新版的响应式是用 Proxy 实现的,现在我们来利用 Proxy 实现一个基本的响应式骨架。

基础

关于 Proxy 的基础知识,可以去MDN学习直达链接

响应式核心

准备工作

本文将实现响应式的核心函数命名为 reactive, 这个函数返回一个被代理之后的结果,可以通过操作这个返回结果来触发响应式。

首先,给出测试数据:一个有 name 属性的对象 obj,我们希望经过 reactive 函数处理之后返回一个新对象 proxyObj,我们像操作 obj 一样操作 proxyObj 对象。

let obj = {
    name: 'test name'
}

let proxyObj = reactive(obj);

例如:当修改 proxyObj.name 时会触发响应式。

其次:设置一个函数用来模拟响应式过程,这里没有必要真的去更新DOM。我们设置一个可以简单地输出一个字符串提示当前需要进行视图更新的函数就可以了。

// 提示视图需要更新
function trigger() {
    console.log('视图需要更新');
}

实现 reactive 函数

一个辅助函数

首先明确 reactive 函数接收一个参数,需要对这个参数进行代理,并返回代理后的结果。如果参数是对象才需要代理,否则直接返回。

这里需要建立一个辅助函数用来判断一个变量是否是对象:

function isObject(param) {
    return typeof param === 'object' && param !== null;
}

主体函数

使用 Proxy 时需要定义一个代理对象 handler 来对目标进行代理操作,这个对象主要有两个方法,即 get 和 set, 分别为 获取和设置属性值的时候触发。同时在内部的实现需要利用到 Reflect对象, 详情见下面的代码。

/* 返回一个被代理后的结果,通过操作这个结果可以来实现响应式, 例如视图更新 */
function reactive(target) {
    // 如果是个对象,则返回被代理后的结果,如果不是则直接返回
    if(!isObject(target)) {
        return target;
    }
    
    // 需要定义一个代理对象来对 target 进行代理操作
    // 这个对象主要有两个方法,即 get 和 set
    const handler = {
        get(target, key, receiver) {
            return Reflect.get(target, key, receiver); // 相当于 return target[key]
        },
        set(target, key, value, receiver) {
            trigger();
            return Reflect.set(target, key, value, receiver); // 相当于 target[key] = value
        }
    };

    // 利用 Proxy 来代理这个对象属性
    let observed = new Proxy(target, handler);

    return observed;
}

现在我们修改 proxyObj 的name属性,发现会触发了最基本的响应式:

let obj = {
    name: 'jjjs'
}

let proxyObj = reactive(obj);

// 修改 name 属性,发现可以监控到
proxyObj.name = 'new Name';
console.log(proxyObj.name);

结果是输出:

而且可以对本来不存在的属性进行监控:

proxyObj.age = 6;
console.log(proxyObj.age);

结果:

但是当我们想处理一个数组时,却发现会触发两次视图更新提示:

let array = [1,2,3];
let obArray = reactive(array);
obArray.push(4)

reactive代码进行,修改,输出set 动作中每次的 key, 观察是谁触发了两次:

function reactive(target) {
    // ...
        set(target, key, value, receiver) {
            trigger();
            console.log(key); // 输出变动的 key
            return Reflect.set(target, key, value, receiver); // 相当于 target[key] = value
        }
    };
    // ...
}

通过上图可以看出当监视数组时,数组下标的更新会触发一次,而数组length的更新也会进行触发,这就是二次触发的原因。

但我们是不需要在 length 更新时对视图进行更新的,所以需要对这里的逻辑进行修改:只对私有属性的修改动作触发视图更新。

function reactive(target) {
    const handler = {
        // ...
        set(target, key, value, receiver) {
            // 只对私有属性的修改动作触发视图更新
            if(!target.hasOwnProperty(key)) {
                trigger();
                console.log(key);
            }
            return Reflect.set(target, key, value, receiver); // 相当于 target[key] = value
        }
    };
    // ...
}

当需要对嵌套的对象进行获取时,例如:

// 对于嵌套的对象
var obj = {
    name: 'jjjs',
    array: [1,2,3]
}

var proxyObj = reactive(obj);
proxyObj.array.push(4);

此时会发现,并不会触发视图需要更新的提示,这是需要对对象进行递归处理:

function reactive(target) {
// ...
    const handler = {
        get(target, key, receiver) {
            const proxyTarget =  Reflect.get(target, key, receiver); // 相当于获取 target[key]
            if(isObject(target[key])) { // 对于对象进行递归
                return reactive(proxyTarget); // 递归
            }

            return proxyTarget;
        },
        // ...
    };
// ...
}

此时发现可以正常进行监听:

然而,当多次获取代理结果时,会出现多次触发代理的情况:

function reactive(target) {
// ...
    console.log('走代理');
    
    // 利用 Proxy 来代理这个对象属性
    let observed = new Proxy(target, handler);
    return observed;
}
// 多次获取代理结果
var proxyObj = reactive(obj);
var proxyObj = reactive(obj);
var proxyObj = reactive(obj);
var proxyObj = reactive(obj);

结果:

走代理
走代理
走代理
走代理

这种情况是我们不希望有的,我们希望对同一个对象仅做一次代理。这个时候,我们需要对已经代理过的对象进行缓存,一次在进行代理之前查询缓存判断是否已经经过了代理,只有没有经过代理的对象才走一次代理。

最适合当做缓存容器的对象是 WeakMap, 这是由于它对于对象的弱引用特性。对WeakMap不熟悉的同学可以点击这里查看其特性。

现在对代码进行修改,增加一个缓存对象:

const toProxy = new WeakMap(); // 用来保存代理后的对象

function reactive(target) {
    // ...
    if(toProxy.get(target)) { // 判断对象是否已经被代理了
        return toProxy.get(target);
    }
    // ...
    console.log('走代理');
    // 利用 Proxy 来代理这个对象属性
    let observed = new Proxy(target, handler);

    toProxy.set(target, observed); // 保存已经代理了的对象

    return observed;
}

现在观察上面代码的结果:

走代理

发现对同一个对象只走了一次代理,这正是我们期望的结果。

总结

上面仅用了几十行代码做出了一个极简的 Proxy 对于响应式的实现,虽然简陋,但是对于理解思想足够了。

源代码地址: Here

如有错误,感谢指出~