作者:陈大鱼头 github: https://github.com/KRISACHAN
在 Vue 3 中,watch
、watchEffect
和 onWatcherCleanup
是三个非常重要的 API,用于响应式数据的监听和副作用处理。本文将详细解析它们的实现原理、使用方式以及最佳实践。
概述
watch
和 watchEffect
是 Vue 3 中用于监听响应式数据变化并执行副作用的两个主要 API,而 onWatcherCleanup
则用于在 watcher 被清理时执行清理逻辑。它们在处理异步操作、数据变化响应等场景中非常有用。
源码解析
文件位置
core/packages/runtime-core/src/apiWatch.ts
关键代码示例与注释
apiWatch.ts
在 apiWatch.ts
文件中,定义了 watch
和 watchEffect
的实现逻辑。
import { ReactiveEffect, track, trigger } from '@vue/reactivity';
import { queuePreFlushCb } from './scheduler';
import { EMPTY_OBJ, isFunction, isObject } from '@vue/shared';
// 定义 watch 函数
export function watch(source, cb, options?) {
return doWatch(source, cb, options);
}
// 定义 watchEffect 函数
export function watchEffect(effect, options?) {
return doWatch(effect, null, options);
}
// 核心的 doWatch 函数
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
let getter;
if (isFunction(source)) {
getter = source; // 如果 source 是函数,直接作为 getter
} else {
getter = () => source; // 否则创建一个返回 source 的函数
}
let cleanup;
const onCleanup = (fn) => {
cleanup = effect.onStop = () => {
fn(); // 注册清理函数
};
};
const job = () => {
if (cleanup) {
cleanup(); // 执行清理函数
}
if (cb) {
cb(); // 执行回调函数
} else {
effect.run(); // 运行副作用
}
};
const effect = new ReactiveEffect(getter, job);
if (cb) {
if (immediate) {
job(); // 立即执行
} else {
effect.run(); // 否则运行副作用
}
} else {
effect.run(); // 运行副作用
}
return () => {
effect.stop(); // 停止副作用
};
}
函数内部关键流程
**定义
getter
**:
如果 source
是函数,则直接作为getter
。否则,创建一个返回 source
的函数作为getter
。
定义清理函数:
使用 onCleanup
注册清理函数,在副作用停止时执行。
定义 job
函数:
在 job
函数中,先执行清理函数,然后执行回调函数或运行副作用。
创建 ReactiveEffect
实例:
使用 getter
和job
创建ReactiveEffect
实例。
执行副作用或回调:
如果有回调函数,根据 immediate
选项决定是否立即执行job
。否则,运行副作用。
返回停止函数:
返回一个函数,用于停止副作用。
API 使用方式与参数
watch
watch
用于监听响应式数据的变化,并在变化时执行回调函数。
参数
source
:要监听的响应式数据或 getter 函数。cb
:数据变化时执行的回调函数。options
:可选参数对象,包括immediate
、deep
、flush
、onTrack
和onTrigger
。
示例代码
<template>
<div>
<input v-model="question" placeholder="Ask a question" />
<p>{{ answer }}</p>
</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
const question = ref('');
const answer = ref('Questions usually contain a question mark. ;-)');
// 监听 question 的变化
watch(question, (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
answer.value = 'Thinking...';
// 模拟异步操作
setTimeout(() => {
answer.value = 'Yes';
}, 1000);
}
});
return {
question,
answer,
};
},
};
</script>
watchEffect
watchEffect
用于自动追踪其回调函数中使用的所有响应式数据,并在这些数据变化时重新执行回调函数。
参数
effect
:要执行的副作用函数。options
:可选参数对象,包括flush
。
示例代码
<template>
<div>
<input v-model="question" placeholder="Ask a question" />
<p>{{ answer }}</p>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const question = ref('');
const answer = ref('Questions usually contain a question mark. ;-)');
// 自动追踪 question 的变化
watchEffect(() => {
if (question.value.includes('?')) {
answer.value = 'Thinking...';
// 模拟异步操作
setTimeout(() => {
answer.value = 'Yes';
}, 1000);
}
});
return {
question,
answer,
};
},
};
</script>
onWatcherCleanup
onWatcherCleanup
用于在 watcher 被清理时执行清理逻辑。注意,onWatcherCleanup
仅在 Vue 3.5+ 中支持,且必须在 watchEffect
的副作用函数或 watch
的回调函数的同步执行期间调用:不能在异步函数中的 await
语句之后调用。
参数
cleanupFn
:要执行的清理函数。
示例代码
<template>
<div>
<input v-model="question" placeholder="Ask a question" />
<p>{{ answer }}</p>
</div>
</template>
<script>
import { ref, watch, onWatcherCleanup } from 'vue';
export default {
setup() {
const question = ref('');
const answer = ref('Questions usually contain a question mark. ;-)');
// 监听 question 的变化
watch(question, (newQuestion, oldQuestion, onCleanup) => {
if (newQuestion.includes('?')) {
answer.value = 'Thinking...';
const timeout = setTimeout(() => {
answer.value = 'Yes';
}, 1000);
// 注册清理函数
onCleanup(() => {
clearTimeout(timeout);
});
}
});
return {
question,
answer,
};
},
};
</script>
最佳实践示例
深度监听
使用 deep
选项来监听对象的嵌套属性变化。
<template>
<div>
<input v-model="user.name" placeholder="Enter your name" />
<p>{{ user.name }}</p>
</div>
</template>
<script>
import { reactive, watch } from 'vue';
export default {
setup() {
const user = reactive({
name: '',
});
// 深度监听 user 对象的变化
watch(user, (newUser, oldUser) => {
console.log('User changed:', newUser);
}, { deep: true });
return {
user,
};
},
};
</script>
深度监听(指定层级)
在 Vue 3.5+ 中,deep
选项可以是一个数字,表示最大遍历深度。
<template>
<div>
<input v-model="user.name" placeholder="Enter your name" />
<p>{{ user.name }}</p>
</div>
</template>
<script>
import { reactive, watch } from 'vue';
export default {
setup() {
const user = reactive({
name: '',
address: {
city: '',
country: ''
}
});
// 深度监听 user 对象的变化,最大遍历深度为 2
watch(user, (newUser, oldUser) => {
console.log('User changed:', newUser);
}, { deep: 2 });
return {
user,
};
},
};
</script>
立即执行
使用 immediate
选项在 watcher 创建时立即执行回调。
<template>
<div>
<input v-model="question" placeholder="Ask a question" />
<p>{{ answer }}</p>
</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
const question = ref('');
const answer = ref('Questions usually contain a question mark. ;-)');
// 立即执行回调
watch(question, (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
answer.value = 'Thinking...';
setTimeout(() => {
answer.value = 'Yes';
}, 1000);
}
}, { immediate: true });
return {
question,
answer,
};
},
};
</script>
清理副作用
使用 onWatcherCleanup
清理异步操作。
<template>
<div>
<input v-model="question" placeholder="Ask a question" />
<p>{{ answer }}</p>
</div>
</template>
<script>
import { ref, watch, onWatcherCleanup } from 'vue';
export default {
setup() {
const question = ref('');
const answer = ref('Questions usually contain a question mark. ;-)');
// 监听 question 的变化
watch(question, (newQuestion, oldQuestion, onCleanup) => {
if (newQuestion.includes('?')) {
answer.value = 'Thinking...';
const timeout = setTimeout(() => {
answer.value = 'Yes';
}, 1000);
// 注册清理函数
onCleanup(() => {
clearTimeout(timeout);
});
}
});
return {
question,
answer,
};
},
};
</script>
总结
在 Vue 3 中,watch
、watchEffect
和 onWatcherCleanup
是处理响应式数据变化和副作用的重要工具。通过理解它们的实现原理和使用方式,可以更好地管理应用中的数据变化和副作用处理,提升开发效率和代码质量。
参考资料
Vue.js 官方文档 - Watchers Vue.js 源码 - apiWatch.ts