在 Vue 3 中,watch
是一个非常实用的 API,用于响应式地监听数据的变化并执行相应的回调。下面我们来详细解读 watch
的源码。
前置知识
在深入源码前,需要了解一些 Vue 3 中的核心概念:
- 响应式系统:Vue 3 采用了
@vue/reactivity
包实现响应式,核心是Proxy
对象来劫持数据的读写操作。 - 副作用函数(Effect):
effect
函数用于创建一个响应式的副作用,当依赖的数据发生变化时,副作用函数会重新执行。
源码位置与调用流程
watch
的源码位于 @vue/runtime-core
包中,核心函数是 doWatch
,watch
函数只是对 doWatch
的简单封装:
import { effect, stop, isRef, isReactive, isFunction, isArray } from '@vue/reactivity'
export function watch(
source: WatchSource | WatchSource[],
cb: WatchCallback,
options?: WatchOptions
): WatchStopHandle {
return doWatch(source, cb, options)
}
doWatch
函数详细解读
1. 解析 source
参数
source
可以是多种类型,如 ref
、reactive
对象、函数或数组,根据不同类型生成对应的 getter
函数:
let getter: () => any
let forceTrigger = false
let isMultiSource = false
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
getter = () => source
deep = true
} else if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(isShallow)
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return traverse(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
}
})
} else if (isFunction(source)) {
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// no cb -> simple effect
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onInvalidate]
)
}
}
} else {
getter = NOOP
}
isRef
:如果source
是ref
,getter
直接返回source.value
。isReactive
:如果source
是响应式对象,getter
返回该对象,并且将deep
选项设为true
,表示深度监听。isArray
:如果source
是数组,对数组中的每个元素进行处理,根据元素类型生成对应的返回值。isFunction
:如果source
是函数,根据是否有回调函数cb
进行不同处理。
2. 处理 deep
选项
如果 cb
存在且 deep
选项为 true
,则对 getter
进行包装,使用 traverse
函数进行深度遍历:
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
3. 处理 onInvalidate
onInvalidate
用于注册一个清理函数,在副作用函数重新执行或停止时调用:
let cleanup: () => void
let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
cleanup = runner.options.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
4. 初始化旧值
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
5. 定义 job
函数
job
函数是数据变化时执行的任务,根据是否有回调函数 cb
进行不同处理:
const job = () => {
if (!runner.active) {
return
}
if (cb) {
// watch(source, cb)
const newValue = runner()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue))
) {
if (cleanup) {
cleanup()
}
callWithErrorHandling(
cb,
instance,
ErrorCodes.WATCH_CALLBACK,
[newValue, oldValue, onInvalidate]
)
oldValue = newValue
}
} else {
// watchEffect
runner()
}
}
6. 定义调度器 scheduler
根据 flush
选项决定调度方式:
let scheduler: (job: () => void) => void
if (flush === 'sync') {
scheduler = job => job()
} else if (flush === 'post') {
scheduler = job => queuePostRenderEffect(job, instance)
} else {
// default: 'pre'
scheduler = job => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job)
} else {
// with 'pre' option, the first call must happen before
// the component is mounted so it is called synchronously.
job()
}
}
}
7. 创建副作用函数 runner
使用 effect
函数创建副作用函数,并传入 getter
和调度器 scheduler
:
const runner = effect(getter, {
lazy: true,
scheduler,
onTrack,
onTrigger
})
8. 初始调用
根据是否有回调函数 cb
和 immediate
选项决定是否立即执行 job
函数:
if (cb) {
if (immediate) {
job()
} else {
oldValue = runner()
}
} else {
// watchEffect
runner()
}
9. 返回停止监听的函数
const stop = () => {
stop(runner)
if (cleanup) {
cleanup()
}
}
return stop
总结
Vue 3 的 watch
源码通过对不同类型的 source
进行解析,结合 effect
函数和调度器实现了数据的监听和回调执行。主要步骤包括:
- 解析
source
生成getter
函数。 - 处理
deep
选项。 - 定义
job
函数和调度器。 - 创建副作用函数
runner
。 - 根据选项决定是否立即执行。
- 返回停止监听的函数。
通过这种方式,watch
可以灵活地监听不同类型的数据变化,并在合适的时机执行回调函数。