想起上次面试,问了个古老的问题:watch 和 computed 的区别 。多少有点感慨,现在已经很少见这种耳熟能详的问题了,网络上八股文不少。今天,我更想分享一下从源码的层面来区别这八竿子打不着的两者。本篇针对watch 做分析,下一篇分析computed 。
一、watch参数类型 我们知道,vue3里的watch接收三个参数:侦听的数据源source、回调cb、以及可选的optiions。
1. 选项options 我们可以在options里根据需要设置immediate 来控制是否立即执行一次回调;设置deep 来控制是否进行深度侦听;设置flush 来控制回调的触发时机,默认为{ flush: 'pre' },即vue组件更新前;若设置为{ flush: 'post' }则回调将在vue组件更新之后触发;此外还可以设置为{ flush: 'sync' },表示同步触发;以及设置收集依赖时的onTrack和触发更新时的onTrigger两个listener,主要用于debugger。watch函数会返回一个watchStopHandle用于停止侦听。options 的类型便是WatchOptions,在源码中的声明如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export interface DebuggerOptions { onTrack?: (event: DebuggerEvent ) => void ; onTrigger?: (event: DebuggerEvent ) => void ; } export interface WatchOptionsBase extends DebuggerOptions { flush?: "pre" | "post" | "sync" ; } export interface WatchOptions <Immediate = boolean > extends WatchOptionsBase { immediate?: Immediate ; deep?: boolean ; }
2. 回调cb 了解完options,接下来我们看看回调cb 。通常我们的cb接收三个参数:value、oldValue和onCleanUp,然后执行我们需要的操作,比如侦听表格的页码,发生变化时重新请求数据。第三个参数onCleanUp,用于注册副作用清理的回调函数, 在副作用下次执行之前,这个回调函数会被调用,通常用来清除不需要的或者无效的副作用。
1 2 3 4 5 6 7 8 9 10 export type WatchEffect = (onCleanup: OnCleanup ) => void ;export type WatchCallback <V = any , OV = any > = ( value: V, oldValue: OV, onCleanup: OnCleanup ) => any ;type OnCleanup = (cleanupFn: () => void ) => void ;
3. 数据源source watch函数可以侦听单个数据或者多个数据,共有四种重载,对应四种类型的source。其中,单个数据源的类型有WatchSource和响应式的object,多个数据源的类型为MultiWatchSources,Readonly<MultiWatchSources>,而MultiWatchSources其实也就是由单个数据源组成的数组。
1 2 3 4 5 export type WatchSource <T = any > = Ref <T> | ComputedRef <T> | (() => T);type MultiWatchSources = (WatchSource <unknown > | object )[];
二、watch函数 下面是源码中的类型声明,以及watch的重载签名和实现签名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 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 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 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 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 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的实现签名中可以看到,和watchEffect不同,watch的第二个参数cb必须是函数,否则会警告。最后,尾调用了doWatch,那么具体的实现细节就都得看doWatch了。让我们来瞅瞅它到底是何方神圣。
三、watch的核心:doWatch 函数 先瞄一下doWatch的签名:接收的参数大体和watch一致,其中source里多了个WatchEffect类型,这是由于在watchApi.js文件里,还导出了三个函数:watchEffect、watchSyncEffect和watchPostEffect,它们接收的第一个参数的类型就是WatchEffect,然后传递给doWatch,会在后面讲到,也可能不会;而options默认值为空对象,函数返回一个WatchStopHandle,用于停止侦听。
1 2 3 4 5 6 7 function doWatch ( source: WatchSource | WatchSource[] | WatchEffect | object , cb: WatchCallback | null , { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): WatchStopHandle { }
再来看看doWatch的函数体,了解一下它干了些啥:
首先是判断在没有cb的情况下,如果options里设置了immediate和deep,就会告警,这俩属性只对有cb的doWatch签名有效。其实也就是上面说到的watchEffect等三个函数,它们是没有cb这个参数的,因此它们设置的immediate和deep是无效的。声明一个当source参数不合法时的警告函数,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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.` ); };
接下来,就到了正文了。第一步的目标是设置getter,顺便配置一下强制触发和深层侦听 等。拿到getter的目的是为了之后创建effect ,vue3的响应式离不开effect,日后再出一篇文章介绍。
先拿到当前实例,声明了空的 getter,初始化关闭强制触发,且默认为单数据源的侦听,然后根据传入的source的类型,做不同的处理:
Ref: getter返回值为Ref的·value,强制触发由source是否为浅层的Ref决定;
Reactive响应式对象:getter的返回值为source本身,且设置深层侦听;
Array:source为数组,则是多数据源侦听,将isMultiSource设置为true,强制触发由数组中是否存在Reactive响应式对象或者浅层的Ref来决定;并且设置getter的返回值为从source映射而来的新数组;
function:当source为函数时,会判断有无cb,有cb则是watch,否则是watchEffect等。当有cb时,使用callWithErrorHandling包裹一层来调用source得到的结果,作为getter的返回值;
otherTypes:其它类型,则告警source参数不合法,且getter设置为NOOP,一个空的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 const instance = currentInstance;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 ((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 = () => callWithErrorHandling (source, instance, ErrorCodes .WATCH_GETTER ); } else { getter = () => { if (instance && instance.isUnmounted ) { return ; } if (cleanup) { cleanup (); } return callWithAsyncErrorHandling ( source, instance, ErrorCodes .WATCH_CALLBACK , [onCleanup] ); }; } } else { getter = NOOP ; __DEV__ && warnInvalidSource (source); }
然后还顺便兼容了下vue2.x版本的watch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (__COMPAT__ && cb && !deep) { const baseGetter = getter; getter = () => { const val = baseGetter (); if ( isArray (val) && checkCompatEnabled (DeprecationTypes .WATCH_ARRAY , instance) ) { traverse (val); } return val; }; }
然后判断了下deep和cb,在深度侦听且有cb的情况下(说白了就是watch而不是watchEffect等),对getter做个traverse,该函数的作用是对getter的返回值做一个递归遍历,将遍历到的值添加到一个叫做seen的集合中,seen的成员即为当前watch要侦听的那些数据。代码如下(影响主线可先跳过):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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就设置好了。之后声明了cleanup和onCleanup,用于清除副作用。以及SSR检测。虽然不是本文的重点,但还是贴一下源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let cleanup : () => void ;let onCleanup : OnCleanup = (fn: () => void ) => { cleanup = effect.onStop = () => { callWithErrorHandling (fn, instance, ErrorCodes .WATCH_CLEANUP ); }; }; 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函数中取得newValue,这不就是我们在使用watch的时候的熟悉套路嘛。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 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;
一看job里,在watch的分支出现了effect,但是这个分支并没有effect呀,再往下看,噢,原来是由之前取得的getter来创建的effect。在这之前,还定义了调度器,调度器scheduler被糅合进了effect里,影响了newValue的获取,从而影响cb的调用时机:
sync:同步执行,也就是回调cb直接执行;
pre:默认值是pre,表示组件更新前执行;
post:组件更新后执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 let scheduler : EffectScheduler ;if (flush === "sync" ) { scheduler = job as any ; } else if (flush === "post" ) { scheduler = () => queuePostRenderEffect (job, instance && instance.suspense ); } else { scheduler = () => queuePreFlushCb (job); } const effect = new ReactiveEffect (getter, scheduler);if (__DEV__) { effect.onTrack = onTrack; effect.onTrigger = onTrigger; }
现在来到了doWatch最后的环节了:侦听器的初始化。
immediate:如果为真值。将直接调用一次job,上文我们知道,job是包裹了一层错误处理程序来调用cb,所以我们现在终于亲眼看到了为什么immediate能让cb立即触发一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 if (cb) { if (immediate) { job (); } else { oldValue = effect.run (); } } else if (flush === "post" ) { queuePostRenderEffect (effect.run .bind (effect), instance && instance.suspense ); } else { effect.run (); } return () => { effect.stop (); if (instance && instance.scope ) { remove (instance.scope .effects !, effect); } };
到这里,watch的源码算是差不多结束了。小结一下核心流程:
watch:判断若没有cb则告警;
watch:尾调用doWatch,之后的操作都在doWatch里进行;
doWatch:判断没有cb时若设置了deep或immediate则告警;
doWatch:根据source的类型得到getter;
doWatch:如果cb存在且deep为真则对getter()进行递归遍历;
doWatch:获取oldValue,声明job函数,在job内部获取newValue并使用callWithAsyncErrorHandling来调用cb。
doWatch:根据post的值定义的调度器scheduler;
doWatch:根据getter和scheduler创建effect;
doWatch:初始化侦听器,如果有cb且immediate为真值,则立即调用job函数,相当于调用我们写的cb;如果immediate为假值,则只调用effect.run()来初始化oldValue;
doWatch:返回一个WatchStopHandle,内部通过effect.stop()来实现停止侦听。
watch:接收到doWatch返回的WatchStopHandle,并返回给外部使用。