【vue3源码】五、watch源码解析
参考代码版本:vue 3.2.37
官方文档:https://vuejs.org/
watch用来监听特定数据源,并在单独的回调函数中执行副作用。默认是惰性的——即回调仅在侦听源发生变化时被调用。 文件位置:packages/runtime-core/src/apiWatch.ts
使用示例
监听一个getter 函数:
const state = reactive({ count: 0 })
watch(
() => state.count,
(newVal, oldVal) => {
}
)
监听一个ref :
const count = ref(0)
watch(
count,
(newVal, oldVal) => {
}
)
监听多个数据源:
const foo = ref('')
const bar = ref('')
watch(
[ foo, bar ],
([ newFoo, newBar ], [ oldFoo, oldBar ]) => {
}
)
深度监听:
const state = reactive({ count: 0 })
watch(
() => state,
() => {
},
{ deep: true }
)
watch(state, () => {
})
源码分析
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 接收三个参数:source 监听的源、cb 回调函数、options 监听配置,watch 函数返回一个停止监听函数。。
在watch 中调用了一个叫做doWatch 的函数,与watch 作用相似的watchEffect 、watchPostEffect 、watchSyncEffect 内部也都使用了这个doWatch 函数。
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
export function watchPostEffect(
effect: WatchEffect,
options?: DebuggerOptions
) {
return doWatch(
effect,
null,
(__DEV__
? Object.assign(options || {}, { flush: 'post' })
: { flush: 'post' }) as WatchOptionsBase
)
}
export function watchSyncEffect(
effect: WatchEffect,
options?: DebuggerOptions
) {
return doWatch(
effect,
null,
(__DEV__
? Object.assign(options || {}, { flush: 'sync' })
: { flush: 'sync' }) as WatchOptionsBase
)
}
可见doWatch 是watch API 的核心,接下来重点研究doWatch 的实现。
doWatch
doWatch 源码过长,这里就不搬运了,在分析过程中,会展示相关代码。
doWatch 函数接收三个参数:source 监听的数据源,cb 回调函数,options :监听配置。doWatch 返回一个停止监听函数。
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
}
首先需要对immediate 、deep 做校验,如果cb 为null ,immediate 、deep 不为undefined 进行提示。
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
let getter: () => any
let forceTrigger = false
let isMultiSource = false
然后根据传入的soure 确定getter 、forceTrigger 、isMultiSource 。这里分了5个分支:
- 如果
source 是ref 类型,getter 是个返回source.value 的函数,forceTrigger 取决于source 是否是浅层响应式。
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
}
- 如果
source 是reactive 类型,getter 是个返回source 的函数,并将deep 设置为true 。
if (isReactive(source)) {
getter = () => source
deep = true
}
- 如果
source 是个数组,将isMultiSource 设为true ,forceTrigger 取决于source 是否有reactive 类型的数据,getter 函数中会遍历source ,针对不同类型的source 做不同处理。
if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(isReactive)
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)
}
})
}
- 如果
source 是个function 。存在cb 的情况下,getter 函数中会执行source ,这里source 会通过callWithErrorHandling 函数执行,在callWithErrorHandling 中会处理source 执行过程中出现的错误;不存在cb 的话,在getter 中,如果组件已经被卸载了,直接return ,否则判断cleanup (cleanup 是在watchEffect 中通过onCleanup 注册的清理函数),如果存在cleanup 执行cleanup ,接着执行source ,并返回执行结果。source 会被callWithAsyncErrorHandling 包装,该函数作用会处理source 执行过程中出现的错误,与callWithErrorHandling 不同的是,callWithAsyncErrorHandling 会处理异步错误。
if (isFunction(source)) {
if (cb) {
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
}
callWithErrorHandling 函数可以接收四个参数:fn 待执行的函数、instance 组件实例、type fn执行过程中出现的错误类型、args fn执行所需的参数。
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
) {
let res
try {
res = args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type)
}
return res
}
callWithAsyncErrorHandling 的参数与callWithErrorHandling 类似,与callWithErrorHandling 不同的是,callWithAsyncErrorHandling 可以接受一个fn 数组。
export function callWithAsyncErrorHandling(
fn: Function | Function[],
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
): any[] {
if (isFunction(fn)) {
const res = callWithErrorHandling(fn, instance, type, args)
if (res && isPromise(res)) {
res.catch(err => {
handleError(err, instance, type)
})
}
return res
}
const values = []
for (let i = 0; i < fn.length; i++) {
values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
}
return values
}
getter = NOOP
__DEV__ && warnInvalidSource(source)
接下来会对vue2 的数组的进行兼容性处理,breaking-changes/watch
if (__COMPAT__ && cb && !deep) {
const baseGetter = getter
getter = () => {
const val = baseGetter()
if (
isArray(val) &&
checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
) {
traverse(val)
}
return val
}
}
如果存在cb 并且deep 为true ,那么需要对数据进行深度监听,这时,会重新对getter 赋值,在新的getter 函数中递归访问之前getter 的返回结果。
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
traverse 实现,递归遍历所有属性,seen 用于防止循环引用问题。
export function traverse(value: unknown, seen?: Set<unknown>) {
if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value
}
seen = seen || new Set()
if (seen.has(value)) {
return value
}
seen.add(value)
if (isRef(value)) {
traverse(value.value, seen)
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], seen)
}
} else if (isSet(value) || isMap(value)) {
value.forEach((v: any) => {
traverse(v, seen)
})
} else if (isPlainObject(value)) {
for (const key in value) {
traverse((value as any)[key], seen)
}
}
return value
}
到此,getter 函数(getter 函数中会尽可能访问响应式数据,尤其是deep 为true 并存在cb 的情况时,会调用traverse 完成对source 的递归属性访问)、forceTrigger 、isMultiSource 已经被确定,接下来声明了两个变量:cleanup 、onCleanup 。onCleanup 会作为参数传递给watchEffect 中的effect 函数。当onCleanup 执行时,会将他的参数通过callWithErrorHandling 封装赋给cleanup 及effect.onStop (effect 在后文中创建)。
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
紧接着是一段SSR 处理过程:
if (__SSR__ && isInSSRComponentSetup) {
onCleanup = NOOP
if (!cb) {
getter()
} else if (immediate) {
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
getter(),
isMultiSource ? [] : undefined,
onCleanup
])
}
return NOOP
}
然后声明了一个oldValue 和job 变量。如果是多数据源oldValue 是个数组,否则是个对象。
job 函数的作用是触发cb (watch )或执行effect.run (watchEffect )。job 函数中会首先判断effect 的激活状态,如果未激活,则return 。然后判断如果存在cb ,调用effet.run 获取最新值,下一步就是触发cb ,这里触发cb 需要满足以下条件的任意一个条件即可:
- 深度监听
deep===true - 强制触发
forceTrigger===true - 如果多数据源,
newValue 中存在与oldValue 中的值不相同的项(利用Object.is 判断);如果不是多数据源,newValue 与oldValue 不相同。 - 开启了
vue2 兼容模式,并且newValue 是个数组,并且开启了WATCH_ARRAY
只要符合上述条件的任意一条,便可已触发cb ,在触发cb 之前会先调用cleanup 函数。执行完cb 后,需要将newValue 赋值给oldValue 。
如果不存在cb ,那么直接调用effect.run 即可。
let oldValue = isMultiSource ? [] : 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 as any[])[i])
)
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
if (cleanup) {
cleanup()
}
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
])
oldValue = newValue
}
} else {
effect.run()
}
}
job.allowRecurse = !!cb
接下来声明了一个调度器scheduler ,在scheduler 中会根据flush 的不同决定job 的触发时机:
let scheduler: EffectScheduler
if (flush === 'sync') {
scheduler = job as any
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job)
} else {
job()
}
}
}
此时,getter 与scheduler 准备完成,创建effect 实例。
const effect = new ReactiveEffect(getter, scheduler)
创建effect 实例后,开始首次执行副作用函数。这里针对不同情况有多个分支:
- 如果存在
cb 的情况
- 如果
immediate 为true ,执行job ,触发cb - 否则执行
effect.run() 进行依赖的收集,并将结果赋值给oldValue - 如果
flush===post ,会将effect.run 推入一个延迟队列中 - 其他情况,也就是
watchEffect ,则会执行effect.run 进行依赖的收集
if (cb) {
if (immediate) {
job()
} else {
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
effect.run()
}
最后,返回一个函数,这个函数的作用是停止watch 对数据源的监听。在函数内部调用effect.stop() 将effect 置为失活状态,如果存在组件实例,并且组件示例中存在effectScope ,那么需要将effect 从effectScope 中移除。
return () => {
effect.stop()
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
watchEffect、watchSyncEffect、watchPostEffect
watchEffect 、watchSyncEffect 、watchPostEffect 的实现均是通过doWatch 实现。
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
export function watchPostEffect(
effect: WatchEffect,
options?: DebuggerOptions
) {
return doWatch(
effect,
null,
(__DEV__
? Object.assign(options || {}, { flush: 'post' })
: { flush: 'post' }) as WatchOptionsBase
)
}
export function watchSyncEffect(
effect: WatchEffect,
options?: DebuggerOptions
) {
return doWatch(
effect,
null,
(__DEV__
? Object.assign(options || {}, { flush: 'sync' })
: { flush: 'sync' }) as WatchOptionsBase
)
}
watch与watchEffect的区别
watch 只会追踪在source 中明确的数据源,不会追踪回调函数中访问到的东西。而且只在数据源发生变化后触发回调。watch 会避免在发生副作用时追踪依赖(当发生副作用时,会执行调度器,在调度器中会将job 推入不同的任务队列,达到控制回调函数的触发时机的目的),因此,我们能更加精确地控制回调函数的触发时机。
watchEffect ,会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式property
示例分析
为了更好地理解watch 及watchEffect 的流程,我们以下面几个例子来理解watch 及watchEffect 。
例1
const state = reactive({ str: 'foo', obj: { num: 1 } })
const flag = ref(true)
watch(
[ flag, () => state.obj ],
([ newFlag, newObj ], [ oldFlag, oldObj ]) => {
console.log(newFlag)
console.log(newObj.num)
console.log(oldFlag)
console.log(oldObj && oldObj.num)
},
{
immediate: true,
flush: 'sync'
}
)
state.obj.num = 2
state.obj = {
num: 2
}
在watch 中调用doWatch 方法,在doWatch 会构造getter 函数,因为所监听的数据源是个数组,所以getter 函数返回值也是个数组,因为数据源的第一项是个ref ,所以getter 返回值第一项是ref.value ,数据源的第二项是个function ,所以getter 返回值第二项是() => state.obj 的返回值,也就是state.obj ,由于我们未指定depp ,最终生成的getter 是() => [ref.value, state.obj] 。
然后利用getter 与scheduler 生成effect ,因为我们指定了immediate: true ,所以会立即执行job 函数,在job 函数中,会执行effect.run() (这个过程中最终执行getter 函数,而在执行getter 函数的过程中会被对应响应式对象的proxy 所拦截,进而收集依赖),然后将effect.run() 的结果赋值给newValue 。然后对位比较newValue 与oldValue 中的元素,因为oldValue 此时是个空数组,所以会触发cb ,在cb 触发过程中将newValue 、oldValue 依次传入,此时打印true 1 undefined undefined ,当cb 执行完,将newValue 赋值为oldValue 。
当执行state.obj.num = 2 时。因为在上一次的依赖收集过程中(也就是getter 执行过程中),并没有访问到num 属性,也就不会收集它的依赖,所以该步骤不会影响到watch 。
当state.obj = { num: 2 } 时,会触发到obj 对应的依赖,而在依赖触发过程中会执行调度器,因为flush 为sync ,所以调度器就是job ,当执行job 时,通过effect.run() 得到newValue ,因为这时oldValue 中的state.value 与newValue 中的state.value 已经不是同一个对象了,所以触发cb 。打印true 2 true 2 。
为什么第二次打印newObj.num 与oldObj.num 相同?因为oldValue 中的oldObj 保存的是state.obj 的引用地址,一旦state.obj 发生改变,oldValue 也会对应改变。
例2
const state = reactive({ str: 'foo', obj: { num: 1 } })
const flag = ref(true)
watchEffect(() => {
console.log(flag.value)
console.log(state.obj.num)
})
state.obj.num = 2
state.obj = {
num: 3
}
与例1相同,例2先生成getter (getter 中会调用source )与scheduler ,然后生成effect 。因为watchEffect 是没有cb 参数,也未指定flush ,所以会直接执行effct.run() 。在effect.run 执行过程中,会调用source ,在source 执行过程中会将effect 收集到flag.dep 及targetMap[toRaw(state)].obj 、targetMap[toRaw(state).obj].num 中。所以第一次打印true 1 。
当执行state.obj.num = 2 ,会触发targetMap[toRaw(state).obj].num 中的依赖,也就是effect ,在触发依赖过程中会执行effect.scheduler ,将job 推入一个pendingPreFlushCbs 队列中。
当执行state.obj = { num: 3 } ,会触发targetMap[toRaw(state)].obj 中的依赖,也就是effect ,在触发依赖过程中会执行effect.scheduler ,将job 推入一个pendingPreFlushCbs 队列中。
最后会执行pendingPreFlushCbs 队列中的job ,在执行之前会对pendingPreFlushCbs 进行去重,也就是说最后只会执行一个job 。最终打印true 3 。
总结
watch 、watchEffect 、watchSyncEffect 、watchPostEffect 的实现均是通过一个doWatch 函数实现。
dowatch 中会首先生成一个getter 函数。如果是watch API,那么这个getter 函数中会根据传入参数,访问监听数据源中的属性(可能会递归访问对象中的属性,取决于deep ),并返回与数据源数据类型一致的数据(如果数据源是ref 类型,getter 函数返回ref.value ;如果数据源类型是reactive ,getter 函数返回值也是reactive ;如果数据源是数组,那么getter 函数返回值也应该是数组;如果数据源是函数类型,那么getter 函数返回值是数据源的返回值)。如果是watchEffect 等API,那么getter 函数中会执行source 函数。
然后定义一个job 函数。如果是watch ,job 函数中会执行effect.run 获取新的值,并比较新旧值,是否执行cb ;如果是watchEffect 等API,job 中执行effect.run 。那么如何只监听到state.obj.num 的变换呢?
当声明完job ,会紧跟着定义一个调度器,这个调度器的作用是根据flush 将job 放到不同的任务队列中。
然后根据getter 与调度器 scheduler初始化一个 ReactiveEffect`实例。
接着进行初始化:如果是watch ,如果是立即执行,则马上执行job ,否则执行effect.run 更新oldValue ;如果flush 是post ,会将effect.run 函数放到延迟队列中延迟执行;其他情况执行effect.run 。
最后返回一个停止watch 的函数。
|