vue3解读—reactivity响应式实现

485 阅读5分钟

reactivity响应式实现

前言

前言:Vue3 中引入了proxy进行数据劫持,effect是响应式系统的核心,而响应式系统又是 vue3 中的核心,所以vue3的解读要从 effect 开始讲起。

reactivity和effect的使用

目前vue3的各个模块都可以单独安装,首先我们需要安装npm i @vue/reactivity,从中引入我们需要的方法,下面是我简单写一段测试代码。

const { reactive, effect } = require("@vue/reactivity");
let a = reactive({
    value: 1,
});
let b;
effect(() => {
    b = a.value + 10;
    console.log(b);
});
a.value = 10;

先别急着看我的输出,想想自己心中的答案是什么?

下面公布控制台的实际输出:

image.png

总结:我们会发现effect函数执行了两次,一次是我们往effect函数传入匿名函数时,它立即执行了一次;接下来是我们对响应式变量a.value进行重新赋值时(或者说effect内函数所依赖的响应式变量发生变化时)它又执行了一次。

接下来我们就好好思考下,如何设计一个reactive方法,将传入的值变成响应式,以及实现一个effect函数,其内函数所依赖的响应式数据变化时,该函数会再次执行。

Dep和effect的实现

reactive会将传入的变量变成响应式数据,包括对象等数据;地基需要从下往上一层一层的搭建,我们这里先实现将单一变量编译成响应式数据,并用effct对其监听。废话不多说,上代码:

第一步:

class Dep {
    constructor(val) {
        this._val = val;
    }
    get value() {
        return this._val;
    }
    set value(newVal) {
        this._val = newVal
    }
}
const dep = new Dep(10);
console.log('取值:', dep.value);
dep.value = 20;
console.log('赋值:', dep.value);

我们通过class类去创建一个简单的变量,用其构造器constructor初始化对象属性,使用settergetter存取器拦截该属性的存取行为。此时dep就是响应式数据吗?想啥呢!但我们会很熟悉后面的取存值行为,这不就是我们在vue3中使用ref声明变量时,取值和赋值的写法吗!

image.png

有了这一步的铺垫,我们接下来的思路是不是更清晰了,只需要在初始化一个Dep时,收集依赖该dep的函数,并在dep值发生变化时,再次执行这个依赖函数,是不是就能实现前面reactiveeffect所达到的效果。ready go

第二步:

let currentEffect;
class Dep {
    constructor(val) {
        this.effects = new Set(); // 储存依赖当前变量的函数,并去重
        this._val = val;
    }
    get value() {
        this.depend();
        return this._val;
    }
    set value(newVal) {
        this._val = newVal
        this.notice(); // 赋值时触发依赖
    }
    // 收集依赖
    depend() {
        if(currentEffect) { // 要记得判空
            this.effects.add(currentEffect);
        }
    }
    // 触发依赖
    notice() {
        this.effects.forEach(effect => {
            effect();
        });
    }
}
const effectWatch = (effect) => {
    currentEffect = effect;
    effect(); // 别忘了首先就会执行一次
    currentEffect = null;
}

const dep = new Dep(10);
let b;
effectWatch(() => {
     b = dep.value + 10;
     console.log('effectWatch', b);
});
dep.value = 20;

为了讲解时方便区分,所以我在实现effect时,重新取名叫effectWatch;不墨迹了,赶紧看代码,相对于第一步,Dep类新增了两个方法,分别是储存依赖的函数和触发依赖的函数。首先effectWatch在调用时就会执行一次依赖函数,并且是在effectWatch的参数中取dep.value值时,就会将当前依赖函数储存,当我们对dep.value赋值时,会再次触发依赖的函数。

到此为止我们只是实现了一个简化单一的‘reactive’effect,真正的reacive可不仅仅只是传一个普通字符的功能,一起想想下一步该怎么做呢?

reactive实现

前面我们已经实现了简单地Dep,它只是一个单一的变量,远远不能满足我们的开发需求。如果我们通过嵌套调用Dep,是不是就能实现reactive的功能了?而且开局我们已经了解到需要使用到proxy对数据进行劫持,那么是不是就好实现多了。

第一步

const reactive = (raw) => {
    return new Proxy(raw, {
        get(target, key) {
            console.log('get----', target[key]);
            return Reflect.get(target, key);
        },
        set(target, key, value) {
            console.log('set----', key, value);
            return Reflect.set(target, key, value);
        }
    });
}
const user = reactive({
    name: '春赏百花冬观雪',
});
user.name;
user.age = 24;
user.age;

我们先科普下proxy,就作者而言,很早就学习过该方法,但对于它的应用确实少的可怜(唯唯诺诺的小菜鸡)。proxy就是在我们访问对象前添加了一层拦截,从而实现基本操作的拦截和自定义(我理解为过滤),而且proxy常常与Reflect成对出现,Reflect也就是反射,它的出现简化了我们调用Object对象的代码,保持JS的简单,它们之间的基情还需要读者自行去了解哦。

对于user我们可以理解为一个较为复杂的对象,此时的它的每一个属性key所对应的值是不是相当于我前面的dep呢,那么我们只需要再访问该key值时将它通过Dep初始化,从而使整个user的任意属性值发生变化,那么所依赖的函数也能再次执行了,整个user也就成了响应式对象了。从代码来看,第一步依旧是 利用存取器对该user对象进行监听。

image.png

第二步

const targetMap = new Map(); // 用于搜集经过reactive初始化的变量
const getDep = (target, key) => {
    let depsMap = targetMap.get(target); // 从targetMap取,如果有的话
    if (!depsMap) { // 没有就先储存
        depsMap = new Map();
        targetMap.set(target, depsMap);
    }
    let dep = depsMap.get(key); // 并将dep与target的key建立连接
    if (!dep) {
        dep = new Dep();
        depsMap.set(key, dep);
    }
    return dep;
};
const reactive = (raw) => {
    return new Proxy(raw, {
        get(target, key) {
            const dep = getDep(target, key);
            dep.depend(); // 当我们访问对象每个属性时,都会收集依赖
            return Reflect.get(target, key);
        },
        set(target, key, value) {
            const dep = getDep(target, key);
            const result = Reflect.set(target, key, value); // 重新设置值之后
            dep.notice(); // 触发依赖
            return result; // 再return
        }
    });
};
const user = reactive({
    name: '春赏百花冬观雪',
});
effectWatch(() => {
    user.name;
    console.log('effect---', user.name);
});
user.name = '晓看天色暮观云';

这里要注意为什么接收Reflect.set后再return抛出,因为我们需要将user.name的值更新后,紧接着触发我们收集到的依赖,最后才能抛出以完成setimage.png

作者也是前端小白一枚,只是通过自己的学习并记录以方便自己的回顾,也希望读者大佬们能提出宝贵意见,共同进步。