本文在Vue3响应式原理的简单实现的基础上,进一步探讨如何得到Vue3源码中的最基本的trace和trigger函数。
背景
为了说清楚Vue3的响应式系统原理,首先得搭建一个最简单的Vue3响应式原理模型,其中必须得有如下几个基本的元素:
- 原始对象
- 存放副作用函数的集合容器
- 响应式对象
- 副作用函数
简单实现
根据上一节提到的几个基本要素,我们有如下简单实现。
js
// 原始对象
const data = { text: "hello world" };
// 存放副作用函数的集合容器。之所以是用Set数据结构,是为了防止相同的副作用函数重复收集
const bucket = new Set();
// 响应式对象。响应式对象为原始对象的Proxy代理
const obj = new Proxy(data, {
get(target, key) {
// 将2个副作用函数添加到容器中
bucket.add(effect1);
bucket.add(effect2);
return target[key];
},
set(target, key, val) {
target[key] = val;
// 将容器中的副作用函数逐一执行
bucket.forEach((fn) => fn());
return true;
},
});
// 定义第一个副作用函数
function effect1() {
console.log("effect 1", obj.text);
}
// 定义第二个副作用函数
function effect2() {
console.log("effect 2", obj.text);
}
代码的关键在于响应式对象obj的get和set的处理:
- 在对象属性的读操作中(get),将两个响应式函数effect1和effect2存放到容器bucket中;
- 在对象属性的写操作中(set),将容器bucket中所有的响应式函数逐一执行。
我们再添加一些初始化逻辑,我们就能运行上述demo了。
- 首先初始化运行所有的副作用函数,触发响应式对象的get方法;
- 其次模拟2s之后响应式对象属性值的更新,观察否会自动触发副作用函数的运行。
js
// 省略之前的代码
// 初始化依次执行副作用函数,触发 get
effect1();
effect2();
// 模拟2s后修改数据
setTimeout(() => {
obj.text = 'HELLO WORLD'
}, 2000)
// 初始化输出如下:
// effect 1 hello world
// effect 2 hello world
// 2s后输出如下:
// effect 1 HELLO WORLD
// effect 2 HELLO WORLD
可以看到2s之后,响应式对象属性赋值,能够自动触发相关的副作用函数执行,实现了响应式的基本效果。
移除硬编码
上面的代码中,写了两个副作用函数:effect1和effect2。但实际上的副作用函数个数是不确定的。因此有必要优化下上面的代码,避免出现obj的get方法中的硬编码。
- 首先将effect改造为一个用来执行副作用函数的函数(即函数运行的容器),具体的副作用函数作为一个参数传给effect。effect内部会具体执行传入的副作用函数。
js
// fn为具体的副作用函数
function effect(fn) {
fn()
}
- 其次,用一个全局变量activeEffect用来表示当前正在执行的副作用函数。
js
let activeEffect = null
function effect(fn) {
activeEffect = fn
fn()
}
- 最后,当执行到具体副作用函数的时候,因为一定会进入到obj的get函数,因此可以在obj的get方法中,通过判断有无activeEffect来收集当前正在执行的副作用函数。
js
// 省略其他代码
const obj = new Proxy(data, {
get(target, key) {
if(activeEffect) {
bucket.set(activeEffect)
}
return target[key]
},
// 省略其他代码
})
// 省略其他代码
完整的代码如下:
js
// 原始对象
const data = { text: "hello world" };
// 存放副作用函数的集合容器。之所以是用Set数据结构,是为了防止相同的副作用函数重复收集
const bucket = new Set();
// 表示当前正在运行的副作用函数
let activeEffect = null;
// 用于执行副作用函数的函数
function effect(fn) {
activeEffect = fn
fn() // 执行副作用函数
}
// 响应式对象。响应式对象为原始对象的Proxy代理
const obj = new Proxy(data, {
get(target, key) {
if(activeEffect) {
bucket.add(activeEffect)
}
return target[key];
},
set(target, key, val) {
target[key] = val;
// 将容器中的副作用函数逐一执行
bucket.forEach((fn) => fn());
return true;
},
});
////////////////////////////////////
// 定义第一个副作用函数
function effect1() {
console.log("effect 1", obj.text);
}
// 定义第二个副作用函数
function effect2() {
console.log("effect 2", obj.text);
}
// 初始化依次执行副作用函数,触发 get
effect(effect1);
effect(effect2);
// 模拟2s后修改数据
setTimeout(() => {
obj.text = 'HELLO WORLD'
}, 2000)
通过上面的改造,我们引入了一个全局变量activeEffect,从而移除了之前obj的get方法中对于副作用函数的硬编码,降低了耦合性。
副作用函数的隔离
上一节的代码中,原始对象data只有一个属性text。如果现在原始对象data有两个属性text1和text2,同时也有两个副作用函数,情况是否有不同呢?
js
// 原始对象,包含两个属性
const data = {
text1: "hello world",
text2: "你好世界",
};
// 其他代码
// 副作用函数1
function effectText1 {
console.log("effect text 1", obj.text1)
}
// 副作用函数2
function effectText2 {
console.log("effect text 2", obj.text2)
}
我们希望的是,当修改obj.text1,触发effectText1的执行,不触发effectText2;当修改obj.text2,触发effectText2的执行,不触发effecctText1。即对应属性的修改,只会触发对应包含该属性的副作用函数,而不会相互干扰。完整代码如下:
js
// 原始对象,包含两个属性
const data = {
text1: "hello world 1",
text2: "hello world 2",
};
// 存放副作用函数的集合容器。之所以是用Set数据结构,是为了防止相同的副作用函数重复收集
const bucket = new Set();
// 表示当前正在运行的副作用函数
let activeEffect = null;
// 用于执行副作用函数的函数
function effect(fn) {
activeEffect = fn;
fn(); // 执行副作用函数
}
// 响应式对象。响应式对象为原始对象的Proxy代理
const obj = new Proxy(data, {
get(target, key) {
if (activeEffect) {
bucket.add(activeEffect);
}
return target[key];
},
set(target, key, val) {
target[key] = val;
// 将容器中的副作用函数逐一执行
bucket.forEach((fn) => fn());
return true;
},
});
////////////////////////////////////
// 定义第一个副作用函数
function effectText1() {
console.log("effect text 1", obj.text1);
}
// 定义第二个副作用函数
function effectText2() {
console.log("effect text 2", obj.text2);
}
// 初始化依次执行副作用函数,触发 get
effect(effectText1);
effect(effectText2);
// 模拟2s后修改数据
setTimeout(() => {
obj.text2 = "HELLO WORLD 2";
}, 2000);
// 初始化输出如下:
// effect text 1 hello world 1
// effect text 2 hello world 2
// 2s后输出如下:
// effect text 1 hello world 1
// effect text 2 HELLO WORLD 2
可以看到,2s之后修改了text2属性,不但触发了effectText2的执行,也触发了effectText1的执行。原因在于我们每次触发属性的更新,都会将bucket的所有副作用函数都会执行一遍,且在执行所有的副作用函数的过程中,没有区分哪些副作用函数该执行,哪些不该执行。因此我们需要改造下容纳副作用函数的容器bucket。 改造的思路如下:
将bucket用Map对象实现,其中Map的key表示对象的属性,Map的value为一个Set对象,Set用于存储对应key相关联的副作用函数。
根据上面的思路,我们可以跟进一步思考。上面只考虑了一个响应式对象的情况,实际情况中可能存在多个响应式对象存在,因此可以进一步升级上面的思路。
将bucket用WeakMap表示,WeakMap中的每个key表示一个响应式对象,WeakMap的每个value用一个Map表示。其中Map的每个key表示该对象的每个属性,Map的每个value用Set表示,用于存储属性相关联的响应式函数。
js
// 原始对象,包含两个属性
const data = {
text1: "hello world 1",
text2: "hello world 2",
};
// 存放副作用函数的集合容器
const bucket = new WeakMap();
// 表示当前正在运行的副作用函数
let activeEffect = null;
// 用于执行副作用函数的函数
function effect(fn) {
activeEffect = fn;
fn(); // 执行副作用函数
}
// 响应式对象。响应式对象为原始对象的Proxy代理
const obj = new Proxy(data, {
get(target, key) {
if(!activeEffect) return target[key]
let depsMap = bucket.get(target)
if(!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
let deps = depsMap.get(key)
if(!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
return target[key];
},
set(target, key, val) {
target[key] = val;
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
return true;
},
});
////////////////////////////////////
// 定义第一个副作用函数
function effectText1() {
console.log("effect text 1", obj.text1);
}
function effectText2() {
console.log("effect text 2", obj.text2)
}
// 初始化依次执行副作用函数,触发 get
effect(effectText1);
effect(effectText2);
// 模拟2s后修改数据
setTimeout(() => {
obj.text2 = "HELLO WORLD 2";
}, 2000);
// 初始化输出如下:
// effect text 1 hello world 1
// effect text 2 hello world 2
// 2s后输出如下:
// effect text 2 HELLO WORLD 2
提取trace和trigger函数
我们将上一节中get和set方法的逻辑提取出来,就变成了triace和trigger函数。
js
// 省略其他代码
let activeEffect = null
const bucket = new WeakMap()
function track(target, key){
if(!activeEffect) return
let depsMap = bucket.get(target)
if(!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
let deps = depsMap.get(key)
if(!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
其中trace表示副作用函数的收集,trigger表示副作用函数的触发执行。同理,响应式对象的定义可以简化为如下形式:
js
// 响应式对象。响应式对象为原始对象的Proxy代理
const obj = new Proxy(data, {
get(target, key) {
if(!activeEffect) return target[key]
track(target, key);
return target[key];
},
set(target, key, val) {
target[key] = val;
trigger(target, key);
return true;
},
});
结论
本文我们通过直观的方式实现了一个最简单的响应式系统。然后通过逐步优化和抽象,提取出了trace和trigger函数。