共计 12460 个字符,预计需要花费 32 分钟才能阅读完成。
【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}
)
// or
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
// 副作用函数,在初始化 effect 时应用
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 { // watchEffect getter = () => { // 如果组件实例曾经卸载,间接 return if (instance && instance.isUnmounted) {return} // 如果清理函数,则执行清理函数 if (cleanup) {cleanup() } // 执行 source,传入 onCleanup,用来注册清理函数 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
会被赋为一个空函数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>) {
// 如果 value 不是对象或 value 不可被转为代理(通过 markRaw 解决),间接 return value
if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {return value}
// sean 用于暂存拜访过的属性,防止出现循环援用的问题
// 如:// const obj = {a: 1}
// obj.b = obj
seen = seen || new Set()
// 如果 seen 中曾经存在了 value,意味着 value 中存在循环援用的状况,这时 return value
if (seen.has(value)) {return value}
// 增加 value 到 seen 中
seen.add(value)
// 如果是 ref,递归拜访 value.value
if (isRef(value)) {traverse(value.value, seen)
} else if (isArray(value)) { // 如果是数组,遍历数组并调用 traverse 递归拜访元素内的属性
for (let i = 0; i < value.length; i++) {traverse(value[i], seen)
}
} else if (isSet(value) || isMap(value)) { // 如果是 Set 或 Map,调用 traverse 递归拜访汇合中的值
value.forEach((v: any) => {traverse(v, seen)
})
} else if (isPlainObject(value)) { // 如果是原始对象,调用 traverse 递归方位 value 中的属性
for (const key in value) {traverse((value as any)[key], seen)
}
}
// 最初须要返回 value
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) {// we will also not call the invalidate callback (+ runner is not set up)
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,阐明是第一次 watch,那么 oldValue 是 undefined
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') {
// 提早执行,将 job 增加到一个提早队列,这个队列会在组件挂在后、更新的生命周期中执行
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// 默认 pre,将 job 增加到一个优先执行队列,该队列在挂载前执行
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
的函数。