首页IT科技vue3.0 watch(Vue3源码解析watch函数实例)

vue3.0 watch(Vue3源码解析watch函数实例)

时间2025-06-15 09:50:54分类IT科技浏览4476
导读:引言 想起上次面试,问了个古老的问题:...

引言

想起上次面试            ,问了个古老的问题: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                  ,在源码中的声明如下:

// reactivity/src/effect.ts export interface DebuggerOptions { onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void } ​ // runtime-core/apiWatch.ts 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   ,用于注册副作用清理的回调函数, 在副作用下次执行之前            ,这个回调函数会被调用                   ,通常用来清除不需要的或者无效的副作用   。

// 副作用 export type WatchEffect = (onCleanup: OnCleanup) => voidexport type WatchCallback<V = any, OV = any> = ( value: V, oldValue: OV, onCleanup: OnCleanup ) => anytype OnCleanup = (cleanupFn: () => void) => void

3. 数据源source

watch函数可以侦听单个数据或者多个数据       ,共有四种重载         ,对应四种类型的source                  。其中                  ,单个数据源的类型有WatchSource和响应式的object          ,多个数据源的类型为MultiWatchSources      ,Readonly<MultiWatchSources>                  ,而MultiWatchSources其实也就是由单个数据源组成的数组                。

// 单数据源类型:可以是 Ref 或 ComputedRef 或 函数 export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T) ​ // 多数据源类型 type MultiWatchSources = (WatchSource<unknown> | object)[] ​

二                   、watch函数

下面是源码中的类型声明             ,以及watch的重载签名和实现签名:

// watch的重载与实现 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// 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的实现签名中可以看到   ,和watchEffect不同                  ,watch的第二个参数cb必须是函数                ,否则会警告。最后,尾调用了doWatch               ,那么具体的实现细节就都得看doWatch了               。让我们来瞅瞅它到底是何方神圣                   。

三         、watch的核心:doWatch 函数

先瞄一下doWatch的签名:接收的参数大体和watch一致                   ,其中source里多了个WatchEffect类型   ,这是由于在watchApi.js文件里            ,还导出了三个函数:watchEffect      、watchSyncEffect和watchPostEffect                   ,它们接收的第一个参数的类型就是WatchEffect       ,然后传递给doWatch         ,会在后面讲到                  ,也可能不会;而options默认值为空对象          ,函数返回一个WatchStopHandle      ,用于停止侦听   。

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参数不合法时的警告函数               ,代码如下:

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.` ) } } ​ // 声明一个source参数不合法的警告函数 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       ,一个空的函数      。
// 拿到当前实例         ,声明了空的getter                  ,初始化关闭强制触发          ,且默认为单数据源的侦听 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 = () => // 可见      ,数组成员只能是Ref                   、Reactive或者函数                  ,其它类型无法通过校验             ,将引发告警 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) }

然后还顺便兼容了下vue2.x版本的watch:

// 2.x array mutation watch compat 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要侦听的那些数据                  。代码如下(影响主线可先跳过):

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) // Ref 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检测                  。虽然不是本文的重点            ,但还是贴一下源码:

let cleanup: () => void let onCleanup: OnCleanup = (fn: () => void) => { cleanup = effect.onStop = () => { callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } } // in SSR there is no need to setup an actual effect, and it should be noop // unless its eager 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函数中取得newValue         ,这不就是我们在使用watch的时候的熟悉套路嘛                。

let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE // job为当前watch要做的工作                  ,后续通过调度器来处理 const job: SchedulerJob = () => { // 当前effect不在active状态          ,说明没有触发该effect的响应式变化      ,直接返回 if (!effect.active) { return } // cb存在                  ,说明是watch             ,而不是watchEffect if (cb) { // watch(source, cb) // 调用 effect.run 得到新的值 newValue const newValue = effect.run() if ( deep || forceTrigger || // 取到的新值和旧值是否相同   ,如果有变化则进入分支 (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || // 兼容2.x (__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 its changed for the first time oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onCleanup ]) // cb执行完成                  ,当前的新值就变成了旧值 oldValue = newValue } } else { // cb不存在                ,则是watchEffect // watchEffect effect.run() } } // 设置allowRecurse,让调度器知道它可以自己触发 job.allowRecurse = !!cb

一看job里               ,在watch的分支出现了effect                   ,但是这个分支并没有effect呀   ,再往下看            ,噢                   ,原来是由之前取得的getter来创建的effect。在这之前       ,还定义了调度器         ,调度器scheduler被糅合进了effect里                  ,影响了newValue的获取          ,从而影响cb的调用时机:

sync:同步执行      ,也就是回调cb直接执行; pre:默认值是pre                  ,表示组件更新前执行; post:组件更新后执行               。
let scheduler: EffectScheduler // 根据flush的值来创建不同的调度器 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 scheduler = () => queuePreFlushCb(job) } // 为 watch 创建 effect              ,watchEffect就不必了   ,因为自带的有 const effect = new ReactiveEffect(getter, scheduler) // 主要是调试用的onTrack和onTrigger                  ,当收集依赖和触发更新时做一些操作 if (__DEV__) { effect.onTrack = onTrack effect.onTrigger = onTrigger }

现在来到了doWatch最后的环节了:侦听器的初始化                   。

immediate:如果为真值   。将直接调用一次job                ,上文我们知道,job是包裹了一层错误处理程序来调用cb               ,所以我们现在终于亲眼看到了为什么immediate能让cb立即触发一次            。
// initial run // 有cb                   ,是 watch if (cb) { if (immediate) { job() } else { // 获取一下当前的值作为旧值 oldValue = effect.run() } } else if (flush === post) { // 没有cb   ,是watchEffect            ,副作用的时机在组件更新之后                   ,用queuePostRenderEffect包裹一层来调整时机 queuePostRenderEffect( effect.run.bind(effect), instance && instance.suspense ) } else { // watchEffect       ,副作用的时机在组件更新之前         ,直接执行一次effect.run effect.run() } // 返回一个WatchStopHandle                  ,内部执行 effect.stop来达到停止侦听的作用 return () => { effect.stop() // 移除当前实例作用域下的当前effect 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                   ,并返回给外部使用                  。

以上就是Vue3源码解析watch函数实例的详细内容   ,更多关于Vue3 watch函数的资料请关注本站其它相关文章!

声明:本站所有文章            ,如无特殊说明或标注                   ,均为本站原创发布          。任何个人或组织       ,在未征得本站同意时         ,禁止复制            、盗用   、采集                   、发布本站内容到任何网站               、书籍等各类媒体平台      。如若本站内容侵犯了原著者的合法权益                  ,可联系我们进行处理                  。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
esxi如何进命令行(esxcfg-advcfg命令 – 配置主机) 给女朋友写短文(给女友的网页小惊喜,(生日,周年,表白通用) ☞谁说程序员不懂浪漫)