【Vue3源码学习】响应式api:watchEffectwatch的实现原理

【Vue3源码学习】响应式api:watchEffectwatch的实现原理

2023年7月9日发(作者:)

【Vue3源码学习】响应式api:watchEffectwatch的实现原理在 Vue2中watch 是

option 写法中⼀个很常⽤的选项,使⽤它可以⾮常⽅便的监听⼀个数据源的变化,⽽在 Vue3 中watch 独⽴成了⼀个响应式api。源码地址:hEffect由于

watch中的许多⾏为都与

watchEffect ⼀致,所以

watchEffect 放在⾸位讲解,为了根据响应式状态⾃动应⽤和重新应⽤副作⽤,我们可以使⽤

watchEffect ⽅法。它⽴即执⾏传⼊的⼀个函数,同时响应式追踪其依赖,并在以来变更时重新运⾏该函数。watchEffect 函数的实现⾮常简洁://

⾸先来看参数类型export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => voidexport interface WatchOptionsBase extends DebuggerOptions { flush?: 'pre' | 'post' | 'sync'}export type WatchStopHandle = () => voidexport function watchEffect( effect: WatchEffect, //

接收函数类型的变量,并且在这个函数中会传⼊ onInvalidate

参数,⽤以清除副作⽤ options?: WatchOptionsBase //

在这个对象中有三个属性,你可以修改 flush

来改变副作⽤的刷新时机,默认为 pre,当修改为 post

时,就可以在组件更新后触发这个副作⽤侦听器,改同 sync

会强制同步触发。⽽ onTrack

和 onTrigger

选项可以⽤于调试侦听器的⾏为,并且两个参数只能在开发模式下⼯作。): WatchStopHandle { return doWatch(effect, null, options)}其中

DebuggerOptions位于rt interface DebuggerOptions { onTrack?: (event: DebuggerEvent) => void //

追踪时触发 onTrigger?: (event: DebuggerEvent) => void //

触发回调时触发}参数传⼊后,函数会执⾏并返回

doWatch 函数的返回值。由于

watch api 也会调⽤

doWatch 函数,所以

doWatch 函数的具体逻辑我们会放在最后看。先看

watch api 的函数实现。watchwatch 需要侦听特定的数据源,并在回调函数中执⾏副作⽤。默认情况下这个侦听是惰性的,即只有当被侦听的源发⽣变化时才执⾏回调。与

watchEffect 相⽐,watch 有以下不同:惰性地执⾏副作⽤;更具体地说明应触发侦听器重新运⾏的状态;访问被侦听状态的先前值和当前值。watch 函数的函数有四次重载,这⾥不做具体分析,这⾥先看下watch的实现参数,后⾯将详细分析watch实现的核⼼doWatch// overload: array of multiple sources + cbexport function watch< T extends MultiWatchSources, Immediate extends Readonly = false>( sources: [...T], cb: WatchCallback, MapSources>, options?: WatchOptions): WatchStopHandle// overload: multiple sources w/ `as const`// watch([foo, bar] as const, () => {})// somehow [...T] breaks when the type is readonlyexport function watch< T extends Readonly, Immediate extends Readonly = false>( source: T, cb: WatchCallback, MapSources>, options?: WatchOptions): WatchStopHandle// overload: single source + cbexport function watch = false>( source: WatchSource, cb: WatchCallback, options?: WatchOptions): WatchStopHandle// overload: watching reactive object w/ cbexport function watch< T extends object, Immediate extends Readonly = false>( source: T, cb: WatchCallback, options?: WatchOptions): WatchStopHandle// implementationexport 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 参数source 的类型如下:export type WatchSource = Ref | ComputedRef | (() => T)type MultiWatchSources = (WatchSource | object)[]从两个类型定义看出,数据源⽀持传⼊单个的

Ref、Computed 响应式对象,或者传⼊⼀个返回相同泛型类型的函数,以及

source ⽀持传⼊数组,以便能同时监听多个数据源。cb 参数在这个最通⽤的声明中,cb 的类型是

any,但

cb 这个回调函数也有类型:export type WatchCallback = ( value: V, oldValue: OV, onInvalidate: InvalidateCbRegistrator) => any在回调函数中,会提供最新的

value、旧

value,以及

onInvalidate 函数⽤以清除副作⽤。optionsexport interface WatchOptions extends WatchOptionsBase { immediate?: Immediate deep?: boolean}可以看到

options 的类型

WatchOptions 继承了

WatchOptionsBase。分析完参数后,可以看到函数体内的逻辑与

watchEffect ⼏乎⼀致,但是多了在开发环境下检测回调函数是否是函数类型,如果回调函数不是函数,就会报警。执⾏

doWatch 时的传参与

watchEffect 相⽐,多了第⼆个参数回调函数。doWatch`watchEffect、watch 还是组件内的 watch 选项,在执⾏时最终调⽤的都是 doWatch 中的逻辑。先从

doWatch 的函数签名看起,与

watch ⼀致。function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ): WatchStopHandledoWatch 函数主要分为以下⼏个部分:1. 标准化 source,组装成为 getter 函数2. 组装 job 函数。判断侦听的值是否有变化,有变化则执⾏ getter 函数和 cb 回调3. 组装 scheduler 函数,scheduler 负责在合适的时机调⽤ job 函数(根据 ,即副作⽤刷新的时机),默认在组件更新前执⾏4. 开启侦听5. 返回停⽌侦听函数function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ): WatchStopHandle {

// 1.

根据 source

的类型组装 getter

let getter: () => any

if (isRef(source)) { getter = ... } else if (isReactive(source)) { getter = ... } else if (isArray(source)) { isMultiSource = true forceTrigger = (isReactive)

getter = ... } else if (isFunction(source)) { if (cb) { getter = ... } else { getter = ... } } else { getter = ... __DEV__ && warnInvalidSource(source) } // 2.

组装 job const job: SchedulerJob = () => { // ... } // 3.

组装 scheduler let scheduler: EffectScheduler = ... // 4.

开启侦听,侦听的是 getter

函数 const effect = new ReactiveEffect(getter, scheduler) () // 5.

返回停⽌侦听函数 return () => { () if (instance && ) { remove(s!, effect) } }}1. 标准化 source,组装成为 getter 函数判断 source 的类型,source 的不同类型,标准化包装成 getter 函数:ref:() => eactive:() => traverse(source)数组:分别根据⼦元素类型,包装成 getter 函数函数:⽤

callWithErrorHandling 包装,实际上就是直接调⽤ source 函数相关代码及注释如下: const instance = currentInstance //

取当前组件实例 let getter: () => any // getter

最终会当做副作⽤的函数参数传⼊ let forceTrigger = false //

标识是否需要强制更新 let isMultiSource = false //

标记传⼊的是单个数据源还是以数组形式传⼊的多个数据源。 // 1. ref

类型

if (isRef(source)) { getter = () => //

直接解包取

值, forceTrigger = !!source._shallow //

标记会根据是否是shallowRef设置 // 2. reactive

类型 } else if (isReactive(source)) { getter = () => source //

直接返回 source,因为 reactive

的值不需要解包获取 deep = true //

由于 reactive

中往往有多个属性,所以将deep设置为 true。这⾥可以看出从外部给 reactive

设置 deep

是⽆效的 // 3.

数组 array

类型 } else if (isArray(source)) { isMultiSource = true forceTrigger = (isReactive) //

会根据数组中是否存在 reactive

响应式对象来设置 //

数组形式,由source内各个元素的单个 getter

结果 getter = () => (s => { if (isRef(s)) { return } else if (isReactive(s)) { return traverse(s) } else if (isFunction(s)) { return callWithErrorHandling(s, instance, _GETTER) } else { __DEV__ && warnInvalidSource(s) } }) // 4. source

是函数 function

类型 } else if (isFunction(source)) { // 4.1

如果有回调函数 if (cb) { // getter with cb // getter

就是 source

函数执⾏的结果,这种情况⼀般是 watch api

中的数据源以函数的形式传⼊。 // callWithErrorHandling

中做了⼀些 vue

错误信息的统⼀处理,有更好的错误提⽰ getter = () => callWithErrorHandling(source, instance, _GETTER) } else { // no cb -> simple effect // 4.2

如果没有回调函数,那么此时就是 watchEffect api

的场景了 getter = () => { //

如果组件实例已经卸载,则不执⾏,直接返回 if (instance && unted) { return } // cleanup不为void时

清除依赖 if (cleanup) { cleanup() } //

执⾏source函数 return callWithAsyncErrorHandling( source, instance, _CALLBACK, [onInvalidate] ) } } // 5.

其余情况

将 getter

设置为空函数,并且报出 source

不合法的警告 } else { getter = NOOP __DEV__ && warnInvalidSource(source) }接着会处理

watch 中的场景,当有回调,并且

deep 选项为

true 时,将使⽤

traverse 来包裹getter 函数,对数据源中的每个属性递归遍历进⾏监听。if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter())}traversetraverse 的作⽤:对于

reactive 对象或设置了参数

deep,需要侦听到深层次的变化,这需要深度遍历整个对象,深层次的访问其所有的响应式变量,并收集依赖。//

深度遍历对象,只是访问响应式变量,不做任何处理//

访问就会触发响应式变量的 getter,从⽽触发依赖收集export function traverse(value: unknown, seen?: Set) { if (!isObject(value) || (value as any)[]) { return value } seen = seen || new Set() if ((value)) { return value } (value) if (isRef(value)) { traverse(, seen) } else if (isArray(value)) { //

继续深⼊遍历数组 for (let i = 0; i < ; i++) { traverse(value[i], seen) } } else if (isSet(value) || isMap(value)) { h((v: any) => { traverse(v, seen) }) } else if (isPlainObject(value)) { //

是对象则继续深⼊遍历 for (const key in value) { traverse((value as any)[key], seen) } } return value}之后会声明

cleanup 和

onInvalidate 函数,并在

onInvalidate 函数的执⾏过程中给

cleanup 函数赋值,当副作⽤函数执⾏⼀些异步的副作⽤,这些响应需要在其失效时清除,所以侦听副作⽤传⼊的函数可以接收⼀个

onInvalidate 函数作为⼊参,⽤来注册清理失效时的回调。当以下情况发⽣时,这个失效回调会被触发:副作⽤即将重新执⾏时。侦听器被停⽌(如果在

setup() 或⽣命周期钩⼦函数中使⽤了

watchEffect,则在组件卸载时)。let cleanup: () => voidlet onInvalidate: InvalidateCbRegistrator = (fn: () => void) => { cleanup = = () => { callWithErrorHandling(fn, instance, _CLEANUP) }}2. 组装 jobJob 函数在 scheduler 函数中被直接或间接调⽤。声明⼀个 job 函数,这个函数最终会作为调度器中的回调函数传⼊,由于是⼀个闭包形式依赖外部作⽤域中的许多变量。根据是否有回调函数,设置

job 的

allowRecurse 属性,这个设置很重要,能够让 job 作为⼀个观察者的回调这样调度器就能知道它允许调⽤⾃⾝。 //

初始化 oldValue let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE //

声明⼀个 job

调度器任务 const job: SchedulerJob = () => { if (!) { //

如果副作⽤以停⽤则直接返回 return } if (cb) { // watch(source, cb) //

在 scheduler

中需要⼿动直接执⾏ ,这⾥会执⾏ getter

函数 //

先执⾏ getter

获取返回值,如果返回值变化,才执⾏ cb。 const newValue = ()

//

判断是否需要执⾏ cb // 1. getter

函数的值被改变,没有发⽣改变则不执⾏ cb

回调 // 2.

设置了 deep

深度监听 // 3. forceTrigger

为 true if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(_ARRAY, instance)) ) { // cleanup before running cb again //

当回调再次执⾏前先清除副作⽤ if (cleanup) { cleanup() } //

触发 watch api

的回调,并将 newValue、oldValue、onInvalidate

传⼊ callWithAsyncErrorHandling(cb, instance, _CALLBACK, [ newValue, // pass undefined as the old value when it's changed for the first time //

⾸次调⽤时,将 oldValue

的值设置为 undefined oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onInvalidate ]) oldValue = newValue //

触发回调后,更新 oldValue } } else { // watchEffect的场景,直接执⾏ runner () } } // important: mark the job as a watcher callback so that scheduler knows//

重要:让调度器任务作为侦听器的回调以⾄于调度器能知道它可以被允许⾃⼰派发更新// it is allowed to self-trigger (#1727)ecurse = !!cb3. 组装 scheduler当

getter 中侦听的响应式变量发⽣改变时,就会执⾏

scheduler 函数。声明⼀个

scheduler 的调度器对象,并根据

flush的传参来确定调度器的执⾏时机。这⼀部分逻辑的源码及注释如下:let scheduler: ReactiveEffectOptions['scheduler'] //

声明⼀个调度器if (flush === 'sync') { //

同步 scheduler = job as any //

这个调度器函数会⽴即被执⾏} else if (flush === 'post') { //

延迟 //

调度器会将任务推⼊⼀个延迟执⾏的队列中,在组件被挂载后、更新的⽣命周期中执⾏。 scheduler = () => queuePostRenderEffect(job, instance && se)} else { //

默认情况 'pre' scheduler = () => { //

区分组件是否已经挂载

if (!instance || ted) { queuePreFlushCb(job) //

组件挂载后则会被推⼊优先执⾏时机的队列中 } else { //

在 pre

选型中,第⼀次调⽤必须发⽣在组件挂载之前 //

所以这次调⽤是同步的 job() } }}4. 开启侦听在处理完以上的调度器部分后,会开始创建const effect = new ReactiveEffect(getter, scheduler)对象,并⾸次执⾏副作⽤函数。这⾥会⽴即调⽤getter 函数,进⾏依赖收集。如果依赖有变化,则执⾏

scheduler 函数:如果

watch 有回调函数如果

watch 设置了

immediate 选项,则⽴即执⾏

job 调度器任务。否则⾸次执⾏

()副作⽤,并将返回值赋值给

oldValue。如果

flush 的刷新时机是

post,则将

()放⼊延迟时机的队列中,等待组件挂载后执⾏。其余情况都直接⾸次执⾏()副作⽤。 const effect = new ReactiveEffect(getter, scheduler) if (__DEV__) { k = onTrack ger = onTrigger } // initial run if (cb) { if (immediate) { job() //

有回调函数且是 imeediate

选项的⽴即执⾏调度器任务 } else { oldValue = () //

否则执⾏⼀次(),并将返回值赋值给 oldValue } } else if (flush === 'post') { //

如果调⽤时机为 post,则推⼊延迟执⾏队列 queuePostRenderEffect( (effect), instance && se ) } else { //

其余情况⽴即⾸次执⾏副作⽤ () }5. 返回停⽌监听函数最后

doWatch 函数会返回⼀个函数,这个函数的作⽤是停⽌侦听,所以⼤家在使⽤时可以显式的为

watch、watchEffect 调⽤返回值以停⽌侦听。//

返回⼀个函数,⽤以显式的结束侦听return () => { () //

移除当前组件上的对应的 effect if (instance && ) { remove(s!, effect) }}doWatch 函数到这⾥就全部运⾏完毕了,现在所有的变量已经声明完毕。结语如果本⽂对你有⼀丁点帮助,点个赞⽀持⼀下吧,感谢感谢

发布者:admin,转转请注明出处:http://www.yc00.com/web/1688906760a182257.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信