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;
先别急着看我的输出,想想自己心中的答案是什么?
下面公布控制台的实际输出:
总结:我们会发现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
初始化对象属性,使用setter
和getter
存取器拦截该属性的存取行为。此时dep
就是响应式数据吗?想啥呢!但我们会很熟悉后面的取存值行为,这不就是我们在vue3中使用ref
声明变量时,取值和赋值的写法吗!
有了这一步的铺垫,我们接下来的思路是不是更清晰了,只需要在初始化一个Dep
时,收集依赖该dep
的函数,并在dep
值发生变化时,再次执行这个依赖函数,是不是就能实现前面reactive
和effect
所达到的效果。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
对象进行监听。
第二步
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
的值更新后,紧接着触发我们收集到的依赖,最后才能抛出以完成set
。
作者也是前端小白一枚,只是通过自己的学习并记录以方便自己的回顾,也希望读者大佬们能提出宝贵意见,共同进步。