VUE3响应式原理——从零解析
基本概念
在开始讲解响应式原理之前,我们需要知道两个基本概念:
什么是副作用函数?
即该函数的执行影响到其他函数的执行结果,则称该函数为副作用函数。例如:
const obj = { text: 'test' };
function effect() {
obj.text = ‘hello’;
}
effect()执行后,其他使用到obj.text的函数中,读取到的值将是hello,而不是text,产生了副作用,故称effect()为副作用函数。
什么是响应式数据?
即当某个数据发生变化时,所有使用该数据的地方都发生了变化,则称该数据为响应式数据。例如:
const obj = { text: 'test' };
function effect() {
ducoment.body.innerText = obj.text;
}
effect();
obj.text = 'hello';
当obj.text的值设置为hello后,若body显示的内容由test变为hello,则称obj是一个响应式数据。
如何实现响应式?
通过上述基本概念的举例说明可以看出,响应式数据涉及到了数据的读取(get)和设置(set)操作——副作用函数执行时,进行了读取操作;数据值改变时,进行了设置操作,同时副作用函数被执行。
那怎么样才能保证对数据进行设置操作时,副作用函数被执行呢?可以在读取操作时使用一个容器将副作用函数保存起来,在设置操作时取出副作用函数执行,就实现了最简单的响应式。
在ES2015+以后,
Proxy可以实现拦截数据的get、set操作,并进行一些特殊处理。
// 副作用函数
function effect() {
document.getElementById("result").innerHTML = obj.text;
}
const data = { text: "test" };
// 收集副作用函数的容器
const bucket = new Set();
// 响应式数据
const obj = new Proxy(data, {
get(target, key) {
// 读取时将副作用函数存入容器
bucket.add(effect);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
// 设置后将容器中的副作用函数取出逐一执行
bucket.forEach((fn) => fn());
return true;
},
});
然而,在实际应用过程中,副作用函数名称并不都是effect,可能是其他名称,也可能是一个匿名函数。因此,需要改造一下原有的effect函数,允许其接收一个真正的副作用函数,并存到一个变量中,解决副作用函数名称被硬编码的问题。
// 当前激活的副作用函数
let activeEffect;
// 改造原有的effect函数
function effect(fn){
activeEffect = fn;
fn();
}
const data = { text: "test" };
// 收集副作用函数的容器
const bucket = new Set();
// 响应式数据
const obj = new Proxy(data, {
get(target, key) {
if (activeEffect) {
bucket.add(activeEffect);
}
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
bucket.forEach((fn) => fn());
return true;
},
});
如何仅触发特定的副作用函数?
上一节中,已经实现了基本的响应式数据。但如果给obj中原本不存在的属性设置数据后,会发现副作用函数被执行了两次,例如下面这段代码:
effect(() => {
console.log('执行了副作用函数');
})
function exec() {
obj.text = 'hello';
obj.name = '张三';
}
exec();
这和预期不一致——原始数据没有name属性,且副作用函数中未读取该属性,exec()执行到最后一行时,不应触发副作用函数的执行。
通过观察可以发现,obj、text、effect呈现一种树状结构:
拓展可以得到以下情况:
即target、key、effect是一对多的关系,因此单单使用Set是不满足的,需要调整收集副作用函数的容器的数据结构。
// 当前激活的副作用函数
let activeEffect;
// 改造原有的effect函数
export function effect(fn) {
activeEffect = fn;
fn();
}
const data = { text: "test" };
// 收集副作用函数的容器
const bucket = new WeakMap();
// 响应式数据
export const obj = new Proxy(data, {
get(target, key) {
if (!activeEffect) {
return target\[key];
}
let depsMap = bucket.get(target);
if (!depsMap) {
// 如果不存在,则创建一个新的Map
bucket.set(target, (depsMap = new Map()));
}
let effectsSet = depsMap.get(key);
if (!effectsSet) {
// 如果不存在,则创建一个新的Set
depsMap.set(key, (effectsSet = new Set()));
}
effectsSet.add(activeEffect);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
const depsMap = bucket.get(target);
// 没有收集到有副作用函数的属性,直接返回
if (!depsMap) {
return;
}
// 取出与属性绑定的所有副作用函数逐一执行
const effectsSet = depsMap.get(key);
effectsSet && effectsSet.forEach((fn) => fn());
return true;
},
});