您现在的位置是:网站首页> 编程资料编程资料

Vue3源码分析侦听器watch的实现原理_vue.js_

2023-05-24 325人已围观

简介 Vue3源码分析侦听器watch的实现原理_vue.js_

watch 的本质

所谓的watch,其本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。实际上,watch 的实现本质就是利用了 effect 和 options.scheduler 选项。如下例子所示:

// watch 函数接收两个参数,source 是响应式数据,cb 是回调函数 function watch(source, cb){ effect( // 触发读取操作,从而建立联系 () => source.foo, { scheduler(){ // 当数据变化时,调用回调函数 cb cb() } } ) }

如上面的代码所示吗,source 是响应式数据,cb 是回调函数。如果副作用函数中存在 scheduler 选项,当响应式数据发生变化时,会触发 scheduler 函数执行,而不是直接触发副作用函数执行。从这个角度来看, scheduler 调度函数就相当于是一个回调函数,而 watch 的实现就是利用了这点。

watch 的函数签名

侦听多个源

侦听的数据源可以 是一个数组,如下面的函数签名所示:

// packages/runtime-core/src/apiWatch.ts // 数据源是一个数组 // overload: array of multiple sources + cb export function watch< T extends MultiWatchSources, Immediate extends Readonly = false >( sources: [...T], cb: WatchCallback, MapSources>, options?: WatchOptions ): WatchStopHandle

也可以使用数组同时侦听多个源,如下面的函数签名所示:

// packages/runtime-core/src/apiWatch.ts // 使用数组同时侦听多个源 // 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, Immediate extends Readonly = false >( source: T, cb: WatchCallback, MapSources>, options?: WatchOptions ): WatchStopHandle

侦听单一源

侦听的数据源是一个 ref 类型的数据 或者是一个具有返回值的 getter 函数,如下面的函数签名所示:

// packages/runtime-core/src/apiWatch.ts // 数据源是一个 ref 类型的数据 或者是一个具有返回值的 getter 函数 // overload: single source + cb export function watch = false>( source: WatchSource, cb: WatchCallback, options?: WatchOptions ): WatchStopHandle export type WatchSource = Ref | ComputedRef | (() => T)

侦听的数据源是一个响应式的 obj 对象,如下面的函数签名所示:

// packages/runtime-core/src/apiWatch.ts // 数据源是一个响应式的 obj 对象 // overload: watching reactive object w/ cb export function watch< T extends object, Immediate extends Readonly = false >( source: T, cb: WatchCallback, options?: WatchOptions ): WatchStopHandle

watch 的实现

watch 函数

// packages/runtime-core/src/apiWatch.ts // implementation export function watch = false>( source: T | WatchSource, cb: any, options?: WatchOptions ): 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 函数接收3个参数,分别是:source 侦听的数据源,cb 回调函数,options 侦听选项。

source 参数

从watch的函数重载中可以知道,当侦听的是单一源时,source 可以是一个 ref 类型的数据 或者是一个具有返回值的 getter 函数,也可以是一个响应式的 obj 对象。当侦听的是多个源时,source 可以是一个数组。

cb 参数

在 cb 回调函数中,给开发者提供了最新的value,旧的value以及onCleanup函数用与清除副作用。如下面的类型定义所示:

export type WatchCallback = ( value: V, oldValue: OV, onCleanup: OnCleanup ) => any

options 参数

options 选项可以控制 watch 的行为,例如通过options的选项参数immediate来控制watch的回调是否立即执行,通过options的选项参数来控制watch的回调函数是同步执行还是异步执行。options 参数的类型定义如下:

export interface WatchOptionsBase extends DebuggerOptions { flush?: 'pre' | 'post' | 'sync' } export interface WatchOptions extends WatchOptionsBase { immediate?: Immediate deep?: boolean }

可以看到 options 的类型定义 WatchOptions 继承了 WatchOptionsBase。也就是说,watch 的 options 中除了 immediate 和 deep 这两个特有的参数外,还可以传递 WatchOptionsBase 中的所有参数以控制副作用执行的行为。

在 watch 的函数体中调用了 doWatch 函数,我们来看看它的实现。

doWatch 函数

实际上,无论是watch函数,还是 watchEffect 函数,在执行时最终调用的都是 doWatch 函数。

doWatch 函数签名

function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): WatchStopHandle

doWatch 的函数签名与 watch 的函数签名基本一致,也是接收三个参数。在 doWatch 函数中,为了便于options 选项的使用,对 options 进行了解构。

初始化变量

首先从 component 中获取当前的组件实例,然后分别定义三个变量。其中 getter 是一个函数,她或作为副作用的函数参数传入到副作用函数中。forceTrigger 变量是一个布尔值,用来标识是否需要强制触发副作用函数执行。isMultiSource 变量同样也是一个布尔值,用来标记侦听的数据源是单一源还是以数组形式传入的多个源,初始值为 false,表示侦听的是单一源。如下面的代码所示:

 const instance = currentInstance let getter: () => any // 是否需要强制触发副作用函数执行 let forceTrigger = false // 侦听的是否是多个源 let isMultiSource = false

接下来根据侦听的数据源来初始化这三个变量。

侦听的数据源是一个 ref 类型的数据

当侦听的数据源是一个 ref 类型的数据时,通过返回 source.value 来初始化 getter,也就是说,当 getter 函数被触发时,会通过source.value 获取到实际侦听的数据。然后通过 isShallow 函数来判断侦听的数据源是否是浅响应,并将其结果赋值给 forceTrigger,完成 forceTrigger 变量的初始化。如下面的代码所示:

if (isRef(source)) { // 侦听的数据源是 ref getter = () => source.value // 判断数据源是否是浅响应 forceTrigger = isShallow(source) }

侦听的数据源是一个响应式数据

当侦听的数据源是一个响应式数据时,直接返回 source 来初始化 getter ,即 getter 函数被触发时直接返回 侦听的数据源。由于响应式数据中可能会是一个object 对象,因此将 deep 设置为 true,在触发 getter 函数时可以递归地读取对象的属性值。如下面的代码所示:

else if (isReactive(source)) { // 侦听的数据源是响应式数据 getter = () => source deep = true }

侦听的数据源是一个数组

当侦听的数据源是一个数组,即同时侦听多个源。此时直接将 isMultiSource 变量设置为 true,表示侦听的是多个源。接着通过数组的 some 方法来检测侦听的多个源中是否存在响应式对象,将其结果赋值给 forceTrigger 。然后遍历数组,判断每个源的类型,从而完成 getter 函数的初始化。如下面的代码所示:

else if (isArray(source)) { // 侦听的数据源是一个数组,即同时侦听多个源 isMultiSource = true forceTrigger = source.some(isReactive) getter = () => // 遍历数组,判断每个源的类型 source.map(s => { if (isRef(s)) { // 侦听的数据源是 ref return s.value } else if (isReactive(s)) { // 侦听的数据源是响应式数据 return traverse(s) } else if (isFunction(s)) { // 侦听的数据源是一个具有返回值的 getter 函数 return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER) } else { __DEV__ && warnInvalidSource(s) } }) } 

侦听的数据源是一个函数

当侦听的数据源是一个具有返回值的 getter 函数时,判断 doWatch 函数的第二个参数 cb 是否有传入。如果有传入,则处理的是 watch 函数的场景,此时执行 source 函数,将执行结果赋值给 getter 。如果没有传入,则处理的是 watchEffect 函数的场景。在该场景下,如果组件实例已经卸载,则直接返回,不执行 source 函数。否则就执行 cleanup 清除依赖,然后执行 source 函数,将执行结果赋值给 getter 。如下面的代码所示:

else if (isFunction(source)) { // 处理 watch 和 watchEffect 的场景 // watch 的第二个参数可以是一个具有返回值的 getter 参数,第二个参数是一个回调函数 // watchEffect 的参数是一个 函数 // 侦听的数据源是一个具有返回值的 getter 函数 if (cb) { // getter with cb // 处理的是 watch 的场景 // 执行 source 函数,将执行结果赋值给 getter getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER) } else { // no cb -> simple effect // 没有回调,即为 watchEffect 的场景 getter = () => { // 件实例已经卸载,则不执行,直接返回 if (instance && instance.isUnmounted) { return } // 清除依赖 if (cleanup) { cleanup() } // 执行 source 函数 return callWithAsyncErrorHandling( source, instance, ErrorCodes.WATCH_CALLBACK, [onCleanup] ) } } }

递归读取响应式数据

如果侦听的数据源是一个响应式数据,需要递归读取响应式数据中的属性值。如下面的代码所示:

// 处理的是 watch 的场景 // 递归读取对象的属性值 if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) }

在上面的代码中,doWatch 函数的第二个参数 cb 有传入,说明处理的是 watch 中的场景。deep 变量为 true ,说明此时侦听的数据源是一个响应式数据,因此需要调用 traverse 函数来递归读取数据源中的每个属性,对其进行监听,从而当任意属性发生变化时都能够触发回调函数执行。

定义清除副作用函数

声明 cleanup 和 onCleanup 函数,并在 onCleanup 函数的执行过程中给 cleanup 函数赋值,当副作用函数执行一些异步的副作用时,这些响应需要在其失效是清除。如下面的代码所示:

// 清除副作用函数 let cleanup: () => void let onCleanup: OnCleanup = (fn: () => void) => { cleanup = effect.onStop = () => { callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } }

-六神源码网