「这是我参与2022首次更文挑战的第28天,活动详情查看:2022首次更文挑战」。
前言
watch
需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。watch
与前文 Vue3.0源码学习——Computed 中的 computed
都是Vue响应式系统中的API,都是根据观察的对象响应的做一些事情,本文主要学习一下 watch
的源码与执行的过程。
源码分析
- 找到
watch
函数定义的位置\packages\runtime-core\src\apiWatch.ts
,watch
有多种定义重载,因此watch
有多种使用方式,观察源可以是 数组,单值,多值,reactive
对象等
- 观察源
source
类型可以是Ref
、ComputedRef
或者是一个函数返回任意类型T
watch
最终定义,接受两个必选参数:观察源source
,回调函数cb
,一个可选参数 选项options
,返回一个doWatch
函数的执行结果就是真正watch
的实现
// implementation
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
...
return doWatch(source as any, cb, options)
}
复制代码
dowatch
的定义,下面看一下主要的执行流程
- 首先会获取当前组件实例,并声明一个
getter
函数
const instance = currentInstance
let getter: () => any
复制代码
- 根据传入
source
的类型,重新定义getter
函数,可以看到getter
最终会是一个函数并且返回我们要观察的值
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
getter = () => source
deep = true // 如果直接传入 reactive 默认 deep 会递归
} else if (isArray(source)) {
isMultiSource = true
...
} else if (isFunction(source)) { // 直接传入一个函数
if (cb) {
// getter with cb
getter = () =>
// 防止报错,去执行 cb
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// no cb -> simple effect
// 没有传参
...
}
} else {
...
}
复制代码
如果设置了
deep
就会进行递归操作(递归必然导致性能下降,因此平时开发因劲量避免观察复杂类型数据对象)
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter()) // 递归
}
复制代码
- 创建响应式副作用函数,最终会在未来某个时刻当 依赖getter 发生变化时,进行异步的更新操作 job,即将
cb
再执行一遍,具体更新的操作在之前的文章中学习过 Vue3.0源码学习——更新流程分析
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
// 创建一个计划任务
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
if (cb) {
// watch(source, cb)
// 如果设置了cb,则立刻调用副作用函数,
// 在这里立即获取watch响应式变量的值
// 因此这是我们观察的响应式数据的最新值
const newValue = effect.run()
if (
...
// 比对新老数据不同,即值发生了变化
: hasChanged(newValue, oldValue)) ||
...
) {
// cleanup before running cb again
// 每次执行cb前清理
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 : oldValue,
onCleanup
])
oldValue = newValue
}
} else {
...
}
}
...
let scheduler: EffectScheduler
// 根据用户传递的watchOptions,决定如何flush回调cb
if (flush === 'sync') {
// 同步执行
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
// 默认不传flush
scheduler = () => {
if (!instance || instance.isMounted) {
// 在pre更新队列中排队,即插队到其他更新队列前
queuePreFlushCb(job)
} else {
...
}
}
}
// 创建响应式副作用
const effect = new ReactiveEffect(getter, scheduler)
复制代码
- 判断是否立即执行
cb
// 是否在初始化时运行cb
if (cb) {
if (immediate) { // 配置了立即执行
job()
} else {
oldValue = effect.run() // 保存oldValue,等于执行一次getter
}
} else if (flush === 'post') {
...
} else {
effect.run()
}
复制代码
- 最终返回一个清理函数,当不需要
watch
时执行这个函数即可
// 返回清理函数
return () => {
effect.stop()
if (instance && instance.scope) {
// 从组件实例上清理响应式副作用函数
remove(instance.scope.effects!, effect)
}
}
复制代码
这样就分析了 watch
函数源码执行的流程,为了更清晰的了解这一过程,将在下一篇中通过代码的调试的形式进一步分析。