watch〡watchEffect 原理
一、watch
侦听指定数据源,当数据源更新时执行回调函数。
1、侦听单个来源
- .vue
- 类型
const count = ref(0)
watch(count, (newCount, oldCount) => {
/* ... */
})
function watch<T>(
source: WatchSource<T>,
callback: WatchCallback<T>,
options?: WatchOptions
): StopHandle
2、侦听多个来源
- .vue
- 类型
const count1 = ref(0)
const count2 = ref(0)
watch([count1, count2], ([newCount1, newCount2], [oldCount1, oldCount2]) => {
/* ... */
})
function watch<T>(
sources: WatchSource<T>[],
callback: WatchCallback<T[]>,
options?: WatchOptions
): StopHandle
3、侦听配置 options
watch 的第三个参数可以进行侦听配置,支持以下选项:
interface WatchOptions extends WatchEffectOptions {
// 是否在侦听器创建时立即触发回调,第一次调用时旧值是 undefined,默认为 false
immediate?: boolean
// 如果源是对象,强制深度遍历,以便在深层级变更时触发回调,默认为 false
deep?: boolean
// 调整回调函数的刷新时机,默认为 pre
// pre: 使侦听器在组件渲染之前执行
// post: 使侦听器延迟到组件渲染之后再执行
// sync: 在响应式依赖发生改变时立即触发侦听器
flush?: 'pre' | 'post' | 'sync'
// 调试侦听器的依赖,当源被追踪为依赖时触发,仅会在开发模式下工作
onTrack?: (event: DebuggerEvent) => void
// 调试侦听器的依赖,当源被更改时触发,仅会在开发模式下工作
onTrigger?: (event: DebuggerEvent) => void
}
二、watchEffect
立即运行一个函数,同时自动收集依赖的数据源,当数据源更新时重新执行。
1、基本用法
- .vue
- 类型
const count = ref(0)
watchEffect(() => console.log(count.value))
function watchEffect(
effect: (onCleanup: OnCleanup) => void,
options?: WatchEffectOptions
): StopHandle
2、watchEffect 配置项
watchEffect 的第二个参数也可以进行侦听配置,支持以下选项:
interface WatchEffectOptions {
flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
3、停止侦听
watchEffect 的返回值可以用来停止侦听,举个例子:
<template>
<h1>{{ txt }}</h1>
<input v-model="msg" />
<button @click="stop">stop watchEffect</button>
</template>
<script setup>
import { ref, watchEffect } from 'vue'
const msg = ref('')
const txt = ref('123')
const stop = watchEffect(() => {
txt.value = msg.value + 'xx'
})
</script>
实现效果:
三、二者的区别
- watchEffect 相当于将 watch 依赖的数据源与回调函数合并,当数据源更新时该回调函数会重新执行;
- watchEffect 相当于 watch 设置了
immediate: true
,在组件更新前会执行一次; - watch 可以访问监听数据源变化前的值,而 watchEffect 只能访问改变后的值;
- watch 不可以停止监听,而 watchEffect 可以。
四、原理解析
1、watch〡watchEffect 原理
- watch 函数
- watchEffect 函数
apiWatch.ts
// https://github.com/vuejs/vue/blob/main/src/v3/apiWatch.ts#L139
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
// 侦听的数据源
source: T | WatchSource<T>,
// 侦听回调函数
cb: any,
// 侦听的配置项
options?: WatchOptions<Immediate>
): WatchStopHandle {
if (__DEV__ && typeof cb !== 'function') {
warn(
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
`Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
`supports \`watch(source, cb, options?) signature.`
)
}
return doWatch(source as any, cb, options)
}
apiWatch.ts
// https://github.com/vuejs/vue/blob/main/src/v3/apiWatch.ts#L61
export type WatchEffect = (onCleanup: OnCleanup) => void
export function watchEffect(
// 接收函数类型的变量,在这个函数中会传入 onCleanup 参数用以清除副作用
effect: WatchEffect,
// 侦听的配置项
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
可以看到,watch 和 watchEffect 都会调用 doWatch
函数来完成侦听。
2、doWatch
function doWatch(
// 侦听的数据源
source: WatchSource | WatchSource[] | WatchEffect | object,
// 侦听回调函数
cb: WatchCallback | null,
// 侦听的配置项
{
immediate,
deep,
flush = 'pre',
onTrack,
onTrigger
}: WatchOptions = emptyObject
): WatchStopHandle {
if (__DEV__ && !cb) {
if (immediate !== undefined) {
warn(
`watch() "immediate" option is only respected when using the ` +
`watch(source, callback, options?) signature.`
)
}
if (deep !== undefined) {
warn(
`watch() "deep" option is only respected when using the ` +
`watch(source, callback, options?) signature.`
)
}
}
const warnInvalidSource = (s: unknown) => {
warn(
`Invalid watch source: ${s}. A watch source can only be a getter/effect ` +
`function, a ref, a reactive object, or an array of these types.`
)
}
// 设置当前的组件实例,方便侦听器找到自己对应的组件
const instance = currentInstance
const call = (fn: Function, type: string, args: any[] | null = null) =>
invokeWithErrorHandling(fn, null, args, instance, type)
// getter 最终会当做副作用的函数参数传入
let getter: () => any
// forceTrigger 表示是否需要强制更新
let forceTrigger = false
// isMultiSource 标记传入的是单个还是以数组形式传入的多个数据源
let isMultiSource = false
// 根据不同的 source 类型重置上面三个参数的值
if (isRef(source)) {
// 为 ref 时 getter 直接返回 source.value
getter = () => source.value
// 根据 source 是否为 shallowRef 来设置强制更新
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// reactive 的值不需要解包获取,因此直接返回 source
getter = () => {
;(source as any).__ob__.dep.depend()
return source
}
// reactive 中往往有多个属性,所以 deep 设为 true
deep = true
} else if (isArray(source)) {
isMultiSource = true
// 根据数组中是否存在 reactive 响应式对象来判断要不要强制更新
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
// getter 为 source 中各个元素 getter 的结果组成的数组
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return traverse(s)
} else if (isFunction(s)) {
return call(s, WATCHER_GETTER)
} else {
__DEV__ && warnInvalidSource(s)
}
})
} else if (isFunction(source)) {
if (cb) {
// 有回调函数时 cb 时,getter 为 source 函数执行的结果
// 这种情况一般是 watch 中的数据源以函数的形式传入
getter = () => call(source, WATCHER_GETTER)
} else {
// 没有回调函数 cb 时,即 watchEffect 使用场景
getter = () => {
// 如果组件已卸载则不执行
if (instance && instance._isDestroyed) {
return
}
// 否则执行 cleanup 清除依赖
if (cleanup) {
cleanup()
}
// 然后执行 source 函数
return call(source, WATCHER, [onCleanup])
}
}
} else {
// 如果 source 类型均不是以上情况,则将其设为空函数并警告
getter = noop
__DEV__ && warnInvalidSource(source)
}
// 接着处理 watch 使用场景
if (cb && deep) {
// 有回调且 deep 为 true 时
const baseGetter = getter
// 使用 traverse 来包裹 getter 函数,对数据源的每个属性递归遍历进行监听
getter = () => traverse(baseGetter())
}
// 然后声明 cleanup 和 onCleanup 函数
let cleanup: () => void
// 在 onCleanup 的执行过程中给 cleanup 赋值
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = watcher.onStop = () => {
call(fn, WATCHER_CLEANUP)
}
}
// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's eager
if (isServerRendering()) {
// we will also not call the invalidate callback (+ runner is not set up)
onCleanup = noop
if (!cb) {
getter()
} else if (immediate) {
call(cb, WATCHER_CB, [
getter(),
isMultiSource ? [] : undefined,
onCleanup
])
}
return noop
}
const watcher = new Watcher(currentInstance, getter, noop, {
lazy: true
})
watcher.noRecurse = !cb
// 初始化 oldValue 并赋值
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
// overwrite default run
watcher.run = () => {
if (!watcher.active) {
return
}
if (cb) {
// watch(source, cb)
const newValue = watcher.get()
if (
// deep 为 true
deep ||
// 或 forceTrigger 需要强制更新
forceTrigger ||
// 或新旧值发生了改变
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue))
) {
// 满足三种情况之一都需要触发 cb 回调,通知侦听器发生了变化
if (cleanup) {
// 调用侦听器前先通过 cleanup 清除副作用
cleanup()
}
// 接着触发 cb 回调
call(cb, WATCHER_CB, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
])
// 在回调触发后更新 oldValue 的值
oldValue = newValue
}
} else {
// watchEffect
watcher.get()
}
}
// 对侦听配置项中的 flush 进行处理
// flush 用于调整回调函数的刷新时机,默认为 pre
if (flush === 'sync') {
// 为 sync 时在响应式依赖发生改变时立即触发侦听器
watcher.update = watcher.run
} else if (flush === 'post') {
// 为 post 时使侦听器延迟到组件渲染之后再执行
watcher.post = true
// 调度器将任务推入一个延迟执行的队列中
watcher.update = () => queueWatcher(watcher)
} else {
// 为默认 pre 时,使侦听器在组件渲染之前执行
watcher.update = () => {
if (instance && instance === currentInstance && !instance._isMounted) {
// pre-watcher triggered before
const buffer = instance._preWatchers || (instance._preWatchers = [])
if (buffer.indexOf(watcher) < 0) buffer.push(watcher)
} else {
queueWatcher(watcher)
}
}
}
if (__DEV__) {
watcher.onTrack = onTrack
watcher.onTrigger = onTrigger
}
// 初始化调用副作用
if (cb) {
if (immediate) {
// 有回调函数且 immediate 为 true 时立即执行调度器任务
watcher.run()
} else {
oldValue = watcher.get()
}
} else if (flush === 'post' && instance) {
// 如果调用时机为 post,则推入 mounted hook
instance.$once('hook:mounted', () => watcher.get())
} else {
watcher.get()
}
return () => {
watcher.teardown()
}
}
五、总结
watch 用于侦听指定数据源,当数据源更新时执行回调函数。watchEffect 则立即运行一个函数,同时自动收集依赖的数据源,当数据源更新时重新执行。
二者的区别在于 watchEffect 相当于 watch 设置了 immediate: true
,并且合并了依赖的数据源跟回调函数,在组件更新前会执行一次;另外 watch 可以访问数据源改变前的值,而 watchEffect 只能访问改变后的值;但 watch 不可以停止监听,而 watchEffect 可以。
watch 和 watchEffect 的原理都是执行 doWatch
函数,doWatch
会根据侦听源的类型构造 getter 函数,根据侦听配置项构造调度器,然后通过构造的 getter 函数和调度器完成侦听,最后返回一个停止侦听的函数。