广告:宝塔Linux面板高效运维的服务器管理软件 点击【 https://www.bt.cn/p/uNLv1L 】立即购买
所谓的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 + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): 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 readonlyexport 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登录后复制侦听单一源
侦听的数据源是一个 ref 类型的数据 或者是一个具有返回值的 getter 函数,如下面的函数签名所示:
// packages/runtime-core/src/apiWatch.ts// 数据源是一个 ref 类型的数据 或者是一个具有返回值的 getter 函数// overload: single source + cbexport function watch<T, Immediate extends Readonly<boolean> = false>(source: WatchSource<T>, cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, options?: WatchOptions<Immediate>): WatchStopHandleexport type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)登录后复制
侦听的数据源是一个响应式的 obj 对象,如下面的函数签名所示:
// packages/runtime-core/src/apiWatch.ts// 数据源是一个响应式的 obj 对象// overload: watching reactive object w/ cbexport 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登录后复制watch 的实现watch 函数
// packages/runtime-core/src/apiWatch.ts// implementationexport 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 函数接收3个参数,分别是:source 侦听的数据源,cb 回调函数,options 侦听选项。
source 参数从watch的函数重载中可以知道,当侦听的是单一源时,source 可以是一个 ref 类型的数据 或者是一个具有返回值的 getter 函数,也可以是一个响应式的 obj 对象。当侦听的是多个源时,source 可以是一个数组。
cb 参数在 cb 回调函数中,给开发者提供了最新的value,旧的value以及onCleanup函数用与清除副作用。如下面的类型定义所示:
export type WatchCallback<V = any, OV = any> = ( 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<Immediate = boolean> 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 的函数签名基本一致,也是接收三个参数。为了方便使用 options 选项,doWatch函数对其进行了解构操作。
初始化变量首先,通过 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 变量的初始化。如下面的代码所示:
// packages/runtime-core/src/apiWatch.ts// 数据源是一个数组// overload: array of multiple sources + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle0登录后复制
侦听的数据源是一个响应式数据
当侦听的数据源是一个响应式数据时,直接返回 source 来初始化 getter ,即 getter 函数被触发时直接返回 侦听的数据源。由于响应式数据中可能会是一个object 对象,因此将 deep 设置为 true,在触发 getter 函数时可以递归地读取对象的属性值。如下面的代码所示:
// packages/runtime-core/src/apiWatch.ts// 数据源是一个数组// overload: array of multiple sources + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle1登录后复制
侦听的数据源是一个数组
当侦听的数据源是一个数组,即同时侦听多个源。此时直接将 isMultiSource 变量设置为 true,表示侦听的是多个源。接着通过数组的 some 方法来检测侦听的多个源中是否存在响应式对象,将其结果赋值给 forceTrigger 。遍历数组并根据每个源的类型,完成getter函数的初始化。如下面的代码所示:
// packages/runtime-core/src/apiWatch.ts// 数据源是一个数组// overload: array of multiple sources + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle2登录后复制
侦听的数据源是一个函数
当侦听的数据源是一个具有返回值的 getter 函数时,判断 doWatch 函数的第二个参数 cb 是否有传入。如果有传入,则处理的是 watch 函数的场景,此时执行 source 函数,将执行结果赋值给 getter 。该情况仅适用于 watchEffect 函数未接收到参数的情况。如果组件实例已被卸载,则直接返回而不执行 source 函数,根据该场景进行处理。如果未能执行成功,则执行清除依赖的代码并调用source函数,将返回结果赋值给getter。如下面的代码所示:
// packages/runtime-core/src/apiWatch.ts// 数据源是一个数组// overload: array of multiple sources + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle3登录后复制递归读取响应式数据
如果侦听的数据源是一个响应式数据,需要递归读取响应式数据中的属性值。如下面的代码所示:
// packages/runtime-core/src/apiWatch.ts// 数据源是一个数组// overload: array of multiple sources + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle4登录后复制
在上面的代码中,doWatch 函数的第二个参数 cb 有传入,说明处理的是 watch 中的场景。deep 变量为 true ,说明此时侦听的数据源是一个响应式数据,因此需要调用 traverse 函数来递归读取数据源中的每个属性,对其进行监听,从而当任意属性发生变化时都能够触发回调函数执行。
定义清除副作用函数声明 cleanup 和 onCleanup 函数,并在 onCleanup 函数的执行过程中给 cleanup 函数赋值,当副作用函数执行一些异步的副作用时,这些响应需要在其失效是清除。如下面的代码所示:
// packages/runtime-core/src/apiWatch.ts// 数据源是一个数组// overload: array of multiple sources + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle5登录后复制封装 scheduler 调度函数
为了便于控制 watch 的回调函数 cb 的执行时机,需要将 scheduler 调度函数封装为一个独立的 job 函数,如下面的代码所示:
// packages/runtime-core/src/apiWatch.ts// 数据源是一个数组// overload: array of multiple sources + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle6登录后复制
在 job 函数中,判断回调函数 cb 是否传入,如果有传入,那么是 watch 函数被调用的场景,否则就是 watchEffect 函数被调用的场景。
如果是 watch 函数被调用的场景,首先执行副作用函数,将执行结果赋值给 newValue 变量,作为最新的值。然后判断需要执行回调函数 cb 的情况:
如果侦听的数据源是响应式数据,需要深度侦听,即 deep 为 true
如果需要强制触发副作用函数执行,即 forceTrigger 为 true
如果新旧值发生了变化
如果存在上述三种情况之一,就必须执行 watch 函数的回调函数 cb。如果回调函数 cb 是再次执行,在执行之前需要先清除副作用。然后调用 callWithAsyncErrorHandling 函数执行回调函数cb,并将新值newValue 和旧值 oldValue 传入回调函数cb中。在回调函数cb执行后,更新旧值oldValue,避免在下一次执行回调函数cb时获取到错误的旧值。
如果是 watchEffect 函数被调用的场景,则直接执行副作用函数即可。
设置 job 的 allowRecurse 属性设置 job 函数的 allowRecurse 属性根据是否传递回调函数 cb 来进行。这个设置非常关键,因为它可以使作业充当监听器的回调,这样调度程序就能够知道它是否允许调用自身。
// packages/runtime-core/src/apiWatch.ts// 数据源是一个数组// overload: array of multiple sources + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle7登录后复制flush 选项指定回调函数的执行时机
在调用 watch 函数时,可以通过 options 的 flush 选项来指定回调函数的执行时机:
当 flush 的值为 sync 时,代表调度器函数是同步执行,此时直接将 job 赋值给 scheduler,这样调度器函数就会直接执行。
当 flush 的值为 post 时,代表调度函数需要将副作用函数放到一个微任务队列中,并等待 DOM 更新结束后再执行。
当 flush 的值为 pre 时,即调度器函数默认的执行方式,这时调度器会区分组件是否已经挂载。如果组件未挂载,则先执行一次调度函数,即执行回调函数cb。在组件挂载之后,将调度函数推入一个优先执行时机的队列中。
// 这里处理的是回调函数的执行时机let scheduler: EffectScheduler if (flush === 'sync') { // 同步执行,将 job 直接赋值给调度器 scheduler = job as any // the scheduler function gets called directly } else if (flush === 'post') { // 将调度函数 job 添加到微任务队列中执行 scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { // default: 'pre' // 调度器函数默认的执行模式 scheduler = () => { if (!instance || instance.isMounted) { // 组件挂载后将 job 推入一个优先执行时机的队列中 queuePreFlushCb(job) } else { // with 'pre' option, the first call must happen before // the component is mounted so it is called synchronously. // 在 pre 选型中,第一次调用必须发生在组件挂载之前 // 所以这次调用是同步的 job() } } }
创建副作用函数初始化完 getter 函数和调度器函数 scheduler 后,调用 ReactiveEffect 类来创建一个副作用函数
// packages/runtime-core/src/apiWatch.ts// 数据源是一个数组// overload: array of multiple sources + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle8登录后复制执行副作用函数
在执行副作用函数之前,首先判断是否传入了回调函数cb,如果有传入,则根据 options 的 immediate 选项来判断是否需要立即执行回调函数cb,如果指定了immediate 选项,则立即执行 job 函数,即 watch 的回调函数会在 watch 创建时立即执行一次。如果不这样做,就需要手动调用副作用函数,将其返回值赋值给oldValue作为旧值。如下面的代码所示:
// packages/runtime-core/src/apiWatch.ts// 数据源是一个数组// overload: array of multiple sources + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle9登录后复制
如果 options 的 flush 选项的值为 post ,需要将副作用函数放入到微任务队列中,等待组件挂载完成后再执行副作用函数。如下面的代码所示:
// packages/runtime-core/src/apiWatch.ts// 使用数组同时侦听多个源// overload: multiple sources w/ `as const`// watch([foo, bar] as const, () => {})// somehow [...T] breaks when the type is readonlyexport function watch< T extends Readonly<MultiWatchSources>, Immediate extends Readonly<boolean> = false>( source: T, cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle0登录后复制
其余情况都是立即执行副作用函数。如下面的代码所示:
// packages/runtime-core/src/apiWatch.ts// 使用数组同时侦听多个源// overload: multiple sources w/ `as const`// watch([foo, bar] as const, () => {})// somehow [...T] breaks when the type is readonlyexport function watch< T extends Readonly<MultiWatchSources>, Immediate extends Readonly<boolean> = false>( source: T, cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle1登录后复制返回匿名函数,停止侦听
最终,doWatch函数返回了一个匿名函数,该函数用于取消对数据源的监听。因此在调用 watch 或者 watchEffect 时,可以调用其返回值类结束侦听。
// packages/runtime-core/src/apiWatch.ts// 使用数组同时侦听多个源// overload: multiple sources w/ `as const`// watch([foo, bar] as const, () => {})// somehow [...T] breaks when the type is readonlyexport function watch< T extends Readonly<MultiWatchSources>, Immediate extends Readonly<boolean> = false>( source: T, cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle2登录后复制
以上就是Vue3侦听器watch的实现原理是什么的详细内容,更多请关注9543建站博客其它相关文章!
发表评论