您现在的位置是:网站首页> 编程资料编程资料
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 ): WatchStopHandledoWatch 的函数签名与 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) } }
相关内容
- javascript实现简单下拉菜单效果_javascript技巧_
- 关于element ui 表格中的常见特殊属性说明_vue.js_
- vue3+vue-cli4中使用svg的方式详解(亲测可用)_vue.js_
- vue3+vite2中使用svg的方法详解(亲测可用)_vue.js_
- vue在同一个页面重复引用相同组件如何区分二者_vue.js_
- JavaScript markdown 编辑器实现双屏同步滚动_javascript技巧_
- vue设计与实现合理的触发响应_vue.js_
- 普通js文件里面如何访问vue实例this指针_javascript技巧_
- vue中使用elementui实现树组件tree右键增删改功能_vue.js_
- VueJS设计与实现之浅响应与深响应详解_vue.js_
