初探 Vue 3响应式系统(四):Watch
无论Vue2还是 Vue3 ,
watch
无论在使用还是面试中都是当之无愧的高频,它让我们能够监听响应式数据的变化,并在变化时执行相应的回调函数,今天和大家一起学习下watch的内部实现。
1. watch
的基本用法
在开始解析源码之前,我们先回顾一下 watch
的基本用法。
watch
可以监听一个或多个响应式数据,并在它们发生变化时执行回调函数。
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`从 ${oldValue} 到 ${newValue}`);
});
count.value++; // 输出: 从 0 到 1
2. watch
的核心实现
2.1 watch
函数的定义
watch
函数的定义如下:
function watch(source, cb, options) {
// 处理 source 和 cb
// 创建 effect
// 返回停止监听的函数
}
watch
函数接收三个参数:
-
source
:要监听的数据源,可以是一个响应式对象、ref、或者一个 getter 函数。 -
cb
:回调函数,当source
发生变化时执行。 -
options
:可选的配置项,比如immediate
、deep
等。
2.2 处理 source
watch
函数首先需要处理 source
,将其转换为一个 getter 函数。这是因为 Vue3 的响应式系统是基于 effect
的,而 effect
需要一个 getter 函数来追踪依赖。
function watch(source, cb, options) {
let getter;
if (typeof source === 'function') {
getter = source;
} else if (isRef(source)) {
getter = () => source.value;
} else if (isReactive(source)) {
getter = () => source;
options.deep = true; // 默认深度监听
} else {
getter = () => {};
}
// 其他逻辑
}
在这个代码片段中,我们根据 source
的类型来决定如何生成 getter
函数:
- 如果
source
是一个函数,直接将其作为getter
。 - 如果
source
是一个ref
,则getter
返回ref.value
。 - 如果
source
是一个reactive
对象,则getter
返回该对象,并默认启用深度监听。
2.3 初始化环境
首先,watch
需要为后续的操作做好准备。这包括定义一个用于存储清理函数的变量和创建一个job
函数,这个函数将在数据变化时被调用。
接下来,watch
会创建一个ReactiveEffect
实例。这个实例是Vue3响应式系统的核心,它负责追踪source
的依赖,并在依赖变化时执行job
函数。
function watch(source, cb, options) {
// ...上面的getter逻辑
let cleanup; // 用于存储清理函数
// 定义一个函数,用于注册清理函数
const onInvalidate = (fn) => {
cleanup = fn;
};
// 定义 job 函数,它会在依赖变化时执行
const job = () => {
if (cleanup) {
cleanup(); // 如果存在清理函数,则先执行清理函数
}
const newValue = effect.run(); // 执行 effect 来获取新的值
cb(newValue, oldValue, onInvalidate); // 调用回调函数
oldValue = newValue; // 更新旧值
};
// 创建一个 ReactiveEffect 实例,当getter中依赖的数据变化,就会执行job ⭐️
const effect = new ReactiveEffect(getter, job);
// 首次执行 effect 来获取初始值
let oldValue = effect.run();
}
2.4 处理 immediate
选项
watch
还支持 immediate
选项,当 immediate
为 true
时,回调函数会在 watch
创建时立即执行一次。
最后,watch
会根据immediate
选项决定是否立即执行job
函数。同时,它返回一个函数,允许我们手动停止对source
的监听。
if (options.immediate) {
job();
} else {
oldValue = effect.run();
}
在这个代码片段中,我们根据 immediate
选项来决定是否立即执行 job
函数。
2.5 返回停止监听的函数
最后,watch
函数返回一个停止监听的函数,调用这个函数可以停止对 source
的监听。
return () => {
effect.stop();
};
这个函数通过调用 effect.stop()
来停止 effect
的运行,从而停止对 source
的监听。
2.6 完整代码
function watch(source, cb, options) {
let getter;
if (typeof source === 'function') {
getter = source;
} else if (isRef(source)) {
getter = () => source.value;
} else if (isReactive(source)) {
getter = () => source;
options.deep = true; // 默认深度监听
} else {
getter = () => {};
}
let cleanup; // 用于存储清理函数
// 定义一个函数,用于注册清理函数
const onInvalidate = (fn) => {
cleanup = fn;
};
// 定义 job 函数,它会在依赖变化时执行
const job = () => {
if (cleanup) {
cleanup(); // 如果存在清理函数,则先执行清理函数
}
const newValue = effect.run(); // 执行 effect 来获取新的值
cb(newValue, oldValue, onInvalidate); // 调用回调函数
oldValue = newValue; // 更新旧值
};
// 创建一个 ReactiveEffect 实例
const effect = new ReactiveEffect(getter, job);
// 首次执行 effect 来获取初始值
let oldValue = effect.run();
// 如果选项中设置了 immediate 为 true,则立即执行 job ⭐️新增
if (options.immediate) {
job();
} else {
// 否则,再次执行 effect 来确保 oldValue 是响应式数据的当前值
oldValue = effect.run();
}
// 返回一个函数,调用它可以停止 effect,从而停止对 source 的监听 ⭐️新增
return () => {
effect.stop();
};
}
3. 代码测试
watch部分就自动中就用我们文章最下方的附录代码
3.1 测试普通 ref
import { ref, watch } from 'vue';
function watch(){
// 此处为我们的watch代码
// ...
}
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`从 ${oldValue} 到 ${newValue}`);
});
count.value++; // 输出: 从 0 到 1
在这个例子中,watch
监听了 count
的变化,并在 count
的值发生变化时打印出新旧值。
3.2 测试 reactive 对象
import { reactive, watch } from 'vue';
function watch(){
// 此处为我们的watch代码
// ...
}
const state = reactive({ count: 0 });
watch(() => state.count, (newValue, oldValue) => {
console.log(`从 ${oldValue} 到 ${newValue}`);
});
state.count++; // 输出: 从 0 到 1
在这个例子中,watch
监听了 state.count
的变化,并在 state.count
的值发生变化时打印出新旧值。
3.3 使用 immediate
选项
import { ref, watch } from 'vue';
function watch(){
// 此处为我们的watch代码
// ...
}
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`从 ${oldValue} 到 ${newValue}`);
}, { immediate: true });
// 输出: 从 undefined 到 0
count.value++; // 输出: 从 0 到 1
在这个例子中,watch
在创建时立即执行了一次回调函数,输出了 count
的初始值。
3.4 使用 onInvalidate
清理副作用
import { ref, watch } from 'vue';
function watch(){
// 此处为我们的watch代码
// ...
}
const count = ref(0);
watch(count, (newValue, oldValue, onInvalidate) => {
let expired = false;
onInvalidate(() => {
expired = true;
});
setTimeout(() => {
if (!expired) {
console.log(`从 ${oldValue} 到 ${newValue}`);
}
}, 1000);
});
count.value++; // 1秒后输出: 从 0 到 1
count.value++; // 不会输出,因为上一次的副作用被清理了
在这个例子中,我们使用 onInvalidate
来清理上一次的副作用,确保只有最新的回调函数会执行。
3. 总结
对于watch的解析就到这里咯,有什么问题的话,感谢指正!