各位观众,晚上好!今天咱们聊聊 Vue 3 源码里两位“观察员”——watch 和 watchEffect。它们都是用来响应数据变化的,但干活方式却大相径庭。你可以把 watch 想象成一位“侦察兵”,需要你明确告诉他要观察哪个目标,而 watchEffect 则像一位“情报员”,自己去搜集情报,看看哪些信息对他有用。
准备好了吗?咱们这就开始深入剖析这两位“观察员”的内心世界!
第一幕:角色介绍
首先,让我们简单回顾一下 watch 和 watchEffect 的基本用法。
watch
watch 允许你监听一个或多个响应式数据源,并在数据变化时执行回调函数。你需要明确指定要监听的数据源,以及当数据变化时要执行的回调函数。
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
watch(
() => count.value, // 监听的数据源
(newValue, oldValue) => { // 回调函数
console.log(`Count changed from ${oldValue} to ${newValue}`);
}
);
return {
count,
increment
};
}
};
</script>
watchEffect
watchEffect 则更加“主动”。它会自动追踪在回调函数中使用的所有响应式依赖项,并在这些依赖项发生变化时重新执行回调函数。你不需要明确指定要监听的数据源,watchEffect 会自己“嗅探”。
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { ref, computed, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
const increment = () => {
count.value++;
};
watchEffect(() => {
console.log(`Count is: ${count.value}, Double Count is: ${doubleCount.value}`);
});
return {
count,
doubleCount,
increment
};
}
};
</script>
第二幕:源码剖析 – 依赖收集
现在,让我们深入 Vue 3 源码,看看 watch 和 watchEffect 是如何实现依赖收集的。 这也是它们最核心的差异所在。
watch 的依赖收集
watch 的依赖收集是“手动”的。你需要明确告诉 watch 要监听哪个响应式数据源。这意味着 watch 只会追踪你明确指定的依赖项。
在 Vue 3 源码中,watch 的实现大致如下 (简化版):
// packages/runtime-core/src/apiWatch.ts
function watch(source, cb, options) {
const getter = isFunction(source)
? source
: () => traverse(source) // 如果 source 不是函数,则递归访问 source 的所有属性以触发依赖收集
let oldValue = undefined
let newValue = undefined
const job = () => {
newValue = effect.run() // 重新运行 effect,触发依赖项的 getter
if (deep || hasChanged(newValue, oldValue)) {
cb(newValue, oldValue)
oldValue = newValue
}
}
const effect = new ReactiveEffect(getter, scheduler) // 创建 ReactiveEffect 实例,并传入 getter 函数
const scheduler = () => queueJob(job)
if (options?.immediate) {
job() // 立即执行一次
} else {
oldValue = effect.run() // 首次运行 effect,触发依赖项的 getter
}
return () => {
effect.stop() // 停止监听
}
}
function traverse(value) {
if (!isObject(value)) {
return value
}
for (const key in value) {
traverse(value[key])
}
return value
}
关键点在于 ReactiveEffect 和 getter 函数。
ReactiveEffect: 这是 Vue 3 中用于追踪响应式依赖项的核心类。每个watch都会创建一个ReactiveEffect实例。getter函数: 这个函数负责读取你要监听的数据源。当getter函数被执行时,它会触发数据源的get拦截器,从而将当前的ReactiveEffect实例添加到数据源的依赖项列表中。这就是依赖收集的过程。
如果 source 是一个对象,traverse 函数会递归访问该对象的所有属性,从而触发所有属性的 get 拦截器,实现深度监听。
watchEffect 的依赖收集
watchEffect 的依赖收集则是“自动”的。它会在回调函数执行期间,自动追踪所有被访问的响应式依赖项。
在 Vue 3 源码中,watchEffect 的实现大致如下 (简化版):
// packages/runtime-core/src/apiWatch.ts
function watchEffect(effect, options) {
const job = () => {
effect.run() // 重新运行 effect,触发依赖项的 getter
}
const instance = getCurrentScope()
const runner = effect as any
runner.effect = new ReactiveEffect(
effect,
() => {
if (runner.active) {
if (queue === LifecycleHooks.PRE_FLUSH) {
if (!instance || instance.isMounted) {
queueJob(job)
}
} else {
queueJob(job)
}
}
}
) // 创建 ReactiveEffect 实例,并传入 effect 函数
if (options?.immediate) {
job() // 立即执行一次
} else {
runner.effect.run() // 首次运行 effect,触发依赖项的 getter
}
return () => {
runner.effect.stop() // 停止监听
}
}
与 watch 类似,watchEffect 也使用 ReactiveEffect 来追踪依赖项。不同之处在于:
watchEffect直接将回调函数作为ReactiveEffect的getter函数传入。- 当回调函数执行时,所有被访问的响应式数据都会自动收集到
ReactiveEffect的依赖项列表中。
依赖收集策略对比
为了更清晰地对比 watch 和 watchEffect 的依赖收集策略,我们用表格来总结一下:
| 特性 | watch |
watchEffect |
|---|---|---|
| 依赖收集方式 | 手动指定 | 自动追踪 |
| 数据源指定 | 需要明确指定要监听的数据源 | 无需指定,自动追踪回调函数中使用的依赖项 |
| 灵活性 | 更灵活,可以精确控制监听哪些数据源 | 更加方便,无需手动指定依赖项 |
| 适用场景 | 需要精确控制监听范围的场景 | 适用于依赖关系比较明确,且需要自动追踪的场景 |
第三幕:源码剖析 – 副作用执行
依赖收集完成之后,下一步就是副作用的执行。当依赖项发生变化时,watch 和 watchEffect 都会触发回调函数的执行。
watch 的副作用执行
watch 的副作用执行是基于比较的。它会比较新值和旧值,只有当新值和旧值不同时,才会执行回调函数。
回到 watch 的源码 (简化版):
function watch(source, cb, options) {
// ... (依赖收集部分)
const job = () => {
newValue = effect.run() // 重新运行 effect,触发依赖项的 getter
if (deep || hasChanged(newValue, oldValue)) { // 比较新值和旧值
cb(newValue, oldValue) // 执行回调函数
oldValue = newValue
}
}
// ...
}
关键点在于 hasChanged 函数。这个函数负责比较新值和旧值,判断是否需要执行回调函数。默认情况下,hasChanged 函数会使用 Object.is 来比较新值和旧值。你也可以通过 deep 选项来开启深度比较。
watchEffect 的副作用执行
watchEffect 的副作用执行则更加简单粗暴。只要依赖项发生变化,它就会无条件地执行回调函数。
回到 watchEffect 的源码 (简化版):
function watchEffect(effect, options) {
// ... (依赖收集部分)
const job = () => {
effect.run() // 重新运行 effect,触发依赖项的 getter
}
// ...
}
可以看到,watchEffect 并没有进行新旧值比较。只要依赖项发生变化,job 函数就会被添加到微任务队列中,等待执行。
副作用执行策略对比
同样,我们用表格来总结一下 watch 和 watchEffect 的副作用执行策略:
| 特性 | watch |
watchEffect |
|---|---|---|
| 执行时机 | 依赖项变化且新值与旧值不同时 | 依赖项变化时 |
| 新旧值比较 | 需要比较新值和旧值 | 无需比较 |
| 性能 | 理论上性能更好,避免不必要的回调执行 | 可能存在不必要的回调执行 |
| 适用场景 | 需要避免不必要的回调执行的场景 | 适用于对性能要求不高,且需要及时响应变化的场景 |
第四幕:实战演练
为了更好地理解 watch 和 watchEffect 的差异,我们来看几个实际的例子。
例子 1:监听单个响应式数据
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { ref, watch, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
// 使用 watch
watch(
() => count.value,
(newValue, oldValue) => {
console.log(`watch: Count changed from ${oldValue} to ${newValue}`);
}
);
// 使用 watchEffect
watchEffect(() => {
console.log(`watchEffect: Count is: ${count.value}`);
});
return {
count,
increment
};
}
};
</script>
在这个例子中,watch 和 watchEffect 都可以监听 count 的变化。但是,watch 需要你明确指定要监听 count.value,而 watchEffect 会自动追踪 count.value。
例子 2:监听多个响应式数据
<template>
<div>
<p>Count: {{ count }}</p>
<p>Message: {{ message }}</p>
<button @click="increment">Increment</button>
<input v-model="message" />
</div>
</template>
<script>
import { ref, watch, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
const message = ref('');
const increment = () => {
count.value++;
};
// 使用 watch 监听多个数据
watch(
[() => count.value, () => message.value],
([newCount, newMessage], [oldCount, oldMessage]) => {
console.log(`watch: Count changed from ${oldCount} to ${newCount}, Message changed from ${oldMessage} to ${newMessage}`);
}
);
// 使用 watchEffect 监听多个数据
watchEffect(() => {
console.log(`watchEffect: Count is: ${count.value}, Message is: ${message.value}`);
});
return {
count,
message,
increment
};
}
};
</script>
在这个例子中,watch 可以同时监听 count 和 message 的变化。你需要将要监听的数据源放在一个数组中。watchEffect 同样可以自动追踪 count 和 message 的变化。
例子 3:避免不必要的副作用执行
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { ref, watch, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
const increment = () => {
count.value = 0; // 直接设置为0,避免中间值
};
// 使用 watch 避免不必要的副作用执行
watch(
() => count.value,
(newValue, oldValue) => {
if (newValue !== oldValue) { // 只有当新值和旧值不同时才执行
console.log(`watch: Count changed from ${oldValue} to ${newValue}`);
}
}
);
// 使用 watchEffect 可能导致不必要的副作用执行
watchEffect(() => {
console.log(`watchEffect: Count is: ${count.value}`);
});
return {
count,
increment
};
}
};
</script>
在这个例子中,increment 函数直接将 count 设置为 0。如果使用 watchEffect,可能会因为中间值的变化而导致不必要的副作用执行。而使用 watch,可以通过比较新值和旧值来避免这种情况。
第五幕:总结与建议
通过以上的分析,我们可以总结出 watch 和 watchEffect 的主要差异:
| 特性 | watch |
watchEffect |
|---|---|---|
| 依赖收集方式 | 手动指定 | 自动追踪 |
| 数据源指定 | 需要 | 不需要 |
| 副作用执行 | 基于新旧值比较 | 无条件执行 |
| 灵活性 | 更灵活,可以精确控制监听范围和回调执行 | 更加方便,但灵活性较低 |
| 性能 | 理论上更好,避免不必要的回调 | 可能存在不必要的回调 |
| 适用场景 | 需要精确控制监听范围和回调执行的场景 | 依赖关系明确,且需要及时响应变化的场景 |
那么,在实际开发中,我们应该如何选择 watch 和 watchEffect 呢?
- 如果你需要精确控制监听范围,并且希望避免不必要的回调执行,那么应该选择
watch。 例如,你需要监听一个对象的特定属性,或者只有当数据发生特定变化时才执行回调函数。 - 如果你的依赖关系比较明确,并且需要及时响应数据的变化,那么可以选择
watchEffect。 例如,你需要根据多个响应式数据的变化来更新 UI,或者执行一些副作用操作。 - 在性能敏感的场景中,应该优先考虑
watch。 因为watch可以避免不必要的回调执行,从而提高性能。 - 在代码简洁性要求较高的场景中,可以选择
watchEffect。 因为watchEffect不需要手动指定依赖项,可以减少代码量。
总而言之,watch 和 watchEffect 各有优缺点,选择哪个取决于你的具体需求。
希望今天的讲座能够帮助大家更好地理解 Vue 3 源码中 watch 和 watchEffect 的实现差异,以及它们在依赖收集和副作用执行上的不同策略。 记住,理解它们的原理,才能更好地运用它们!
感谢大家的观看! 我们下次再见!