构建一个自定义的Vue3响应式监听器:watchEffect(附源码)
1.watchEffect
watchEffect 是Vue3新增的一个api,其功能在侦听到数据发生变化时执行回调。作为一名 Vue.js 开发者,你肯定会熟悉 watchEffect
这个常用API,与watch不同点在与
- 1 不需要指定监听的数据, 回调函数中使用到哪些数据就监视哪些数据(类似computed计算属性)。
- 2 初始时就会执行一次, 收集依赖。
- 3 默认深度侦听。
2.watchEffect使用方式
ini
<script setup>
import { reactive, ref, watchEffect } from "vue";
let firstName = ref("张");
let lastName = ref("三");
let fullName = "";
watchEffect(() => {
console.log(firstName.value + lastName.value);
});
firstName.value = "李";
lastName.value = "四";
</script>
3.思路
-
在构建一个Vue3风格的响应式监听器时,我们首先要处理的是如何让数据成为"响应式"的。这意味着数据的变化应该能够被系统检测到,并触发相应的操作。为了实现这一点,我们采用观察者模式的原则,通过
Proxy
这一ES6特性来拦截对数据的读(get)和写(set)操作。在数据被读取时,我们进行所谓的依赖收集,即将访问的属性与watchEffect
中定义的回调函数相关联;而当数据被修改时,相应的回调函数将被触发执行。具体到
watchEffect
的实现上,我们需要关注以下几个关键步骤:- 全局回调挂载:这是实现响应式系统的关键一环。我们需要一个全局的变量来暂存当前被处理的依赖项所对应的回调函数。在依赖收集阶段,我们将此回调函数暂存至全局变量中,以便在触发依赖收集时能够顺利访问到这个回调。
- 执行回调函数 :
watchEffect
的设计使其在首次运行时就执行一次回调函数。这个执行过程不仅仅是简单的函数调用,更重要的是它触发了依赖收集的过程。由于回调函数必然会访问到响应式数据,这样就自然而然地激发了对应数据的依赖收集。 - 从全局移除回调:一旦依赖收集完成,当前的回调函数就完成了其任务。此时,我们需要将其从全局变量中移除,为下一个依赖项的回调函数的收集腾出空间。
通过以上步骤,我们就能构建一个有效的
watchEffect
函数,它能够在数据变化时自动执行相关的回调,为Vue应用带来动态的响应式特性。
4.数据响应化
4.1 Proxy
下面使用Proxy来完成数据的响应化
javascript
// 数据响应化
function createReactive(obj) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
return new Proxy(obj, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key);
return typeof result === "object" ? createReactive(result) : result;
},
set(target, key, value, receiver) {
const outcome = Reflect.set(target, key, value, receiver);
trigger(target, key);
return outcome;
},
});
}
4.2 实现依赖收集
当我们构建一个类似于Vue的响应式系统时,一个关键的问题是如何将依赖(即数据属性)与相应的回调函数有效地联系起来。理想情况下,我们想要用一个映射结构(如Map)来存储这种关系:依赖项作为键,对应的回调函数作为值。然而,这种方法面临一个问题:当处理多个响应式对象时,若这些对象有同名属性,则会发生冲突。
为了解决这个问题,我们需要为每个响应式对象独立地存储依赖关系。解决方案是将每个对象与一个专门的映射(Map)关联,这个映射维护着对象属性(依赖项)与回调函数之间的关系。而所有这些对象及其关联的映射被存储在一个全局映射中。但是,标准的Map结构不能有效地处理对象作为键的情况,因为它们不支持垃圾回收机制。
这就是我们选择使用WeakMap
的原因。WeakMap
是ES6中引入的一种特殊类型的Map,它允许对象作为键名,同时支持垃圾回收。这意味着,如果没有其他引用指向一个对象,该对象就可以被垃圾回收,从而避免内存泄漏。此外,考虑到一个属性可能在多个地方被侦听(即可能有多个回调函数关联到同一属性),我们将每个属性映射到一个Set
结构,这个Set
中存储了所有相关的回调函数。
实现这一逻辑的步骤如下:
- 创建一个全局的
WeakMap
,用于存储每个响应式对象及其对应的依赖关系映射(即属性和回调函数的映射)。 - 设置一个全局变量,用于临时存储当前正在处理的回调函数。
通过这种方式,我们可以高效地管理依赖和回调函数,确保即使在复杂的应用中也能保持良好的性能和内存管理。
javascript
// 存储回调全局变量
let currentEffect;
/*
存储依赖关系的数据结构。它的整体结构是,以需要响应化的对象作为键名,键值是一个map结构。
该Map以对象的每个属性名为键名,键值为set结构。
该Set存储了该属性变化时需要触发的所有回调。
*/
const dependencyMap = new WeakMap();
接着实现依赖收集。过程是为响应式对象的某个属性所对应的集合添加一个回调函数。
ini
// 依赖跟踪
function track(target, key) {
if (currentEffect) {
let depsMap = dependencyMap.get(target);
if (!depsMap) {
depsMap = new Map();
dependencyMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(currentEffect);
}
}
4.3 实现侦听执行
接下来实现监听执行。从全局WeakMap中取出当前变更属性所对应的所有回调函数,依次执行 ,这一步比较简单。
ini
// 触发更新
function trigger(target, key) {
const depsMap = dependencyMap.get(target);
if (!depsMap) {
return;
}
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect());
}
}
至此,数据相应化的工作已经完成,接下来实现watchEffect。
5.实现watchEffect
前面已经总结过,watchEffect只有三点。全局挂载回调,执行回调,踢出回调。
ini
// 自定义watchEffect实现
function customWatchEffect(effect) {
currentEffect = effect;
effect(); // 依赖收集
currentEffect = null;
}
至此所有工作都已完成,我们来测试一下。
ini
let firstName = createReactive({ value: "张" });
let lastName = createReactive({ value: "三三" });
customWatchEffect(() => {
console.log(firstName.value + lastName.value);
});
firstName.value = "李";
lastName.value = "四四";
可以看到其能够实现首次执行,依赖收集,侦听->执行,深度侦听。动作与Vue3中的watchEffect一致。
最后附上完整代码
ini
let currentEffect;
const dependencyMap = new WeakMap();
// 数据响应化
function createReactive(obj) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
return new Proxy(obj, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key);
return typeof result === "object" ? createReactive(result) : result;
},
set(target, key, value, receiver) {
const outcome = Reflect.set(target, key, value, receiver);
trigger(target, key);
return outcome;
},
});
}
// 依赖跟踪
function track(target, key) {
if (currentEffect) {
let depsMap = dependencyMap.get(target);
if (!depsMap) {
depsMap = new Map();
dependencyMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(currentEffect);
}
}
// 触发更新
function trigger(target, key) {
const depsMap = dependencyMap.get(target);
if (!depsMap) {
return;
}
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect());
}
}
// 自定义watchEffect实现
function customWatchEffect(effect) {
currentEffect = effect;
effect();
currentEffect = null;
}