💡 [本系列Vue3源码解读文章基于3.3.4版本](https://github.com/vuejs/core/tree/v3.3.4) 欢迎关注公众号:《前端 Talkking》
1、前言
所谓 watch,其本质就是观测一个响应式数据,当数据发生变化的时通知并执行相应的回调函数。如下示例:
javascript
watch(obj, () => {
console.log("数据变化了")
}))
假设 watch
是一个响应数据,使用 watch
函数观测它,并传递一个回调函数,当修改响应式数据 obj 的值的时候,会触发该回调函数执行。
实际上,watch
的实质本质上就是利用了 effect
以及 options.scheduler
选项,如下代码所示:
javascript
effect(() => {
console.log(obj.foo)
}, {
scheduler() {
// 当 obj.foo 的值变化时,就会执行scheduler调度函数
}
})
如果一个副作用函数存在 scheduler
选项,当响应式数据发生变化时,会触发 scheduler
调度函数执行,而非直接触发副作用函数执行。从这个角度来看,其实 scheduler
调度函数就相当于一个回调函数,而 watch
的实现就是利用了这个特性。以下是最简单的 watch
函数实现:
javascript
function watch(source, cb) {
// 触发读取操作,从而建立联系
effect(() => source.foo,
{
scheduler() {
// 当数据变化时,调用回调函数cb
cb()
}
})
}
2、watch 源码实现
watch
函数定义在:packages/runtime-core/src/apiWatch.ts
文件下。
2.1 函数签名
javascript
export function watch<
T extends MultiWatchSources,
Immediate extends Readonly<boolean> = false
>(
sources: [...T],
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// overload: multiple sources w/ `as const`
// watch([foo, bar] as const, () => {})
// somehow [...T] breaks when the type is readonly
export function watch<
T extends Readonly<MultiWatchSources>,
Immediate extends Readonly<boolean> = false
>(
source: T,
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// overload: single source + cb
export function watch<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// overload: watching reactive object w/ cb
export function watch<
T extends object,
Immediate extends Readonly<boolean> = false
>(
source: T,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
从以上源码中,我们可以发现,watch函数可以监听的数据源有:
- 侦听的数据源是一个
ref
类型的数据 或者是一个具有返回值的getter
函数; - 侦听的数据源是一个数组;
2.2 watch 源码实现
无论哪种函数签名,在 watch
函数实现中的源码如下:
javascript
// implementation
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
if (__DEV__ && !isFunction(cb)) {
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)
}
在该函数中,watch
函数接收了 3 个参数,分别是:source
侦听的数据源,cb
回调函数,options
侦听选项。watch 函数最终都调用了 doWatch
函数。
2.2.1 source 参数
从定义可知,source
可以是一个 ref
类型的数据,或者是一个具有返回值的 getter
函数,也可以是一个响应式的 obj
对象。当侦听的是多个源时,source
可以是一个数组。
2.2.2 cb 参数
cb 参数的定义如下:
javascript
export type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: OnCleanup
) => any
value
:最新的值;oldValue
: 更新前的值;onCleanup
:用于清除副作用。
2.2.3 options 参数
options 参数的类型定义如下:
javascript
export interface WatchOptionsBase extends DebuggerOptions {
flush?: 'pre' | 'post' | 'sync'
}
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
immediate?: Immediate
deep?: boolean
}
- immediate:控制 watch 的回调是否立即执行;
- deep:控制 watch 的监听是否是深度的;
- flush:调整回调函数的刷新时机;
2.3 doWatch 函数实现
doWatch 的函数与 watch 函数的签名基本一致,也是接收三个参数。在 doWatch
函数中,为了便于 options
选项的使用,对 options
进行了解构。
javascript
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle
doWatch
函数实现很长,下面只贴出我们需要理解的关键部分。
2.3.1 标准化 source
通过前文我们知道,source
可以是 getter
函数,也可以是响应式对象甚至是响应式对象数组,因此,我们需要根据传入的 source
,生成标准化的 getter
函数,处理流程如下:
javascript
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(s => isReactive(s) || isShallow(s))
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 {
__DEV__ && warnInvalidSource(s)
}
})
} 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 callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
} else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
标准化 source
的处理流程拆解如下:
- 如果
source
是ref
对象,则创建一个访问source.value
的getter
函数; - 如果
source
是source
对象,则创建一个访问source
的getter
函数,并设置deep
为true
; - 如果
source
是一个函数,则会继续判断第二个参数cb
是否存在,然后对 cb 函数做简单的封装处理; - 如果
source
是一个数组,则内部会通过source.map
函数映射处一个新的数组,它会判断每个数组元素的类型,映射规则与前面的source
规则一致; - 如果
source
不满足上述条件,则在非生产环境下发出警告,提示source
类型不合法;
2.3.2 创建 job
处理完 watch
函数的第一个参数 source
后,接下来处理第二个参数 cb
。
我们知道,cb 是一个回调函数,其拥有三个参数:
newValue
: 代表新值;oldValue
:代表旧值;onInvalidate
:表示注册无效的回调函数;
那么如何判断值是否发生了变化,如何计算和存储旧值和新值呢?
我们可以在内部创建一个 job
,它是对 cb
回调函数做的一层封装,维护新值旧值的计算和存储,以及是否需要执行回调函数,当侦听的值发生变化时就会执行 job
。
javascript
// 注册无效回调函数
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
cleanup = effect.onStop = undefined
}
}
// 旧值初始值
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
if (cb) {
// 新值
const newValue = effect.run()
// 满足执行回调函数的条件
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
// 执行清理函数
if (cleanup) {
cleanup()
}
// 执行回调函数 cb
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
onCleanup
])
// 更新旧值
oldValue = newValue
}
} else {
// watchEffect
effect.run()
}
}
// 允许触发自身
job.allowRecurse = !!cb
创建 job
函数的处理流程如下:
- 判断回调函数
cb
是否传入,如果有传入,那么是watch
的调用场景,否则是watchEffect
函数被调用的场景;- 如果是
watch
函数被调用的场景,首先执行副作用函数获取最新的值newValue
,然后判断是否需要执行回调函数cb
的情况:- 监听的数据是
reactive
类型,即 deep 的值为 true; - 需要强制执行副作用函数,即
forceTrigger
为 true; - 新旧值发生了变化; 如果满足上面条件中的一个,那么先清除副作用函数,然后调用
callWithAsyncErrorHandling
函数,将新旧值newValue
和oldValue
传入该函数中,执行完毕后更新旧值 oldValue,避免在下一次执行回调函数cb
时获取到错误的旧值。
- 监听的数据是
- 如果是
watchEffect
函数被调用的场景,则直接执行副作用函数即可; - 设置 job 的 allowRecurse 属性,它能够让 job 作为侦听器的回调,这样调度器就能知道它允许调用自身。
- 如果是
2.3.3 创建 scheduler
当调用 watch
函数时,可以通过 options 的 flush
选项来指定回调函数的执行时机:
flush: sync
,代表它是一个同步的watcher
,即数据变化时同步执行回调函数;flush: post
,代表调度函数需要将副作用函数放到一个微任务队列中,并等待 DOM 更新结束后再执行;flush: pre
,即调度器函数默认的执行方式,在组件更新之前执行,如果组件还没有挂载,则在组件挂载之前同步执行回调函数。
javascript
let scheduler: EffectScheduler
if (flush === 'sync') {
// 同步执行,将job赋值给调度器
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
// 将调度函数job添加到微任务队列中执行
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
2.3.4 创建副作用 effect
初始化 getter
函数和调度函数 scheduler
后,调用 ReactiveEffect
来创建一个副作用函数。然后经过以下步骤处理:
javascript
const effect = new ReactiveEffect(getter, scheduler)
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
// initial run
// 初次执行
if (cb) {
// 选项参数 immediate 来指定回调是否需要立即执行
if (immediate) {
// 手动调用副作用函数,拿到的就是旧值
job()
} else {
// 求旧值
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
// 没有cb并且flush不为post的情况
effect.run()
}
- 判断传入的回调函数
cb
是否存在,如果存在,则根据传入的options
选项中immediate
,如果是否为 true,则会在创建watch
的时候立即执行一次,否则,否则就手动调用副作用函数,并将返回值作为旧值,赋值给oldValue
; - 如果
options
的flush
的选项的值为post
,需要将副作用函数放入到微任务队列中,等待组件挂载完成后再执行副作用函数; - 其余情况就是立即执行副作用函数。
2.3.5 返回销毁函数
最后,会返回销毁函数,也就是 watch
执行后返回的函数,我们可以通过调用它来停止 watcher
对数据的监听,如下代码所示:
javascript
const unwatch = () => {
effect.stop()
if (instance && instance.scope) {
// 移除组件effects对这个effect的引用
remove(instance.scope.effects!, effect)
}
}
销毁函数内部会执行 effect.stop()
函数让 effect
失效,并清理 effect
的相关依赖,这样就可以停止对数据的监听。同时,如果是组件中注册的 watcher
,也会移除组件 effects
对这个 effect
的引用。
3、总结
watch
本质上利用了副作用函数重新执行时的可调度性。一个 watch
本身会创建一个 effect
,当这个 effect
依赖的响应式数据发生变化时,会执行该 effect
的调度函数,即 scheduler
。
watch
可以侦听单一数据源,也可以侦听多个源。单一的数据源可以是具有返回值的 getter
函数,或者一个 ref
对象,也可以是 reactive
对象。
watch
可以通过 flush
指定回调函数和副作用函数的执行时机,可指定的参数值有 post
、sync
、pre
(默认)。
watch
可以通过指定 immediate
为 true,这样 watch
在创建的时候会立即执行一次回调函数。
4、参考资料
[1]vue官网
[2]vuejs设计与实现
[3]vue3源码