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

咱们知道,Vue.js 2 是经过 Object.defineProperty()函数来实现的响应式。这个月 5 号尤大发布了 Vue.js 3 的源码,社区立刻出现了不少源码分享的文章。 你们早就得知新版的响应式是用 Proxy 实现的,如今咱们来利用 Proxy 实现一个基本的响应式骨架。vue

基础

关于 Proxy 的基础知识,能够去MDN学习直达连接react

响应式核心

准备工做

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

首先,给出测试数据:一个有 name 属性的对象 obj,咱们但愿通过 reactive 函数处理以后返回一个新对象 proxyObj,咱们像操做 obj 同样操做 proxyObj 对象。数组

let obj = {
    name: 'test name'
}

let proxyObj = reactive(obj);
复制代码

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

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

// 提示视图须要更新
function trigger() {
    console.log('视图须要更新');
}
复制代码

实现 reactive 函数

一个辅助函数

首先明确 reactive 函数接收一个参数,须要对这个参数进行代理,并返回代理后的结果。若是参数是对象才须要代理,不然直接返回。post

这里须要创建一个辅助函数用来判断一个变量是不是对象:学习

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属性,发现会触发了最基本的响应式:ui

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

若有错误,感谢指出~

相关文章
相关标签/搜索