您现在的位置是:网站首页> 编程资料编程资料
Vue3源码解析watch函数实例_vue.js_
2023-05-24
746人已围观
简介 Vue3源码解析watch函数实例_vue.js_
引言
想起上次面试,问了个古老的问题: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 extends WatchOptionsBase { immediate?: Immediate deep?: boolean } 2. 回调cb
了解完options,接下来我们看看回调**cb**。通常我们的cb接收三个参数:value、oldValue和onCleanUp,然后执行我们需要的操作,比如侦听表格的页码,发生变化时重新请求数据。第三个参数onCleanUp,用于注册副作用清理的回调函数, 在副作用下次执行之前,这个回调函数会被调用,通常用来清除不需要的或者无效的副作用。
// 副作用 export type WatchEffect = (onCleanup: OnCleanup) => void export type WatchCallback= ( value: V, oldValue: OV, onCleanup: OnCleanup ) => any type OnCleanup = (cleanupFn: () => void) => void
3. 数据源source
watch函数可以侦听单个数据或者多个数据,共有四种重载,对应四种类型的source。其中,单个数据源的类型有WatchSource和响应式的object,多个数据源的类型为MultiWatchSources,Readonly,而MultiWatchSources其实也就是由单个数据源组成的数组。
// 单数据源类型:可以是 Ref 或 ComputedRef 或 函数 export type WatchSource= Ref | ComputedRef | (() => T) // 多数据源类型 type MultiWatchSources = (WatchSource | object)[]
二、watch函数
下面是源码中的类型声明,以及watch的重载签名和实现签名:
// watch的重载与实现 export 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 readonly export function watch< T extends Readonly , Immediate extends Readonly = false >( source: T, cb: WatchCallback , MapSources >, options?: WatchOptions ): WatchStopHandle // overload: single source + cb export function watch = false>( source: WatchSource , cb: WatchCallback , options?: WatchOptions ): WatchStopHandle // overload: watching reactive object w/ cb export function watch< T extends object, Immediate extends Readonly = false >( source: T, cb: WatchCallback , options?: WatchOptions ): WatchStopHandle // 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的实现签名中可以看到,和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) { if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) { return value } seen =
相关内容
- antDesign 自定义分页样式的实现代码_javascript技巧_
- Vue watch原理源码层深入讲解_vue.js_
- Vue3源码分析组件挂载初始化props与slots_vue.js_
- Vue3源码分析组件挂载创建虚拟节点_vue.js_
- Vue computed实现原理深入讲解_vue.js_
- Nodejs处理Json文件并将处理后的数据写入新文件中_node.js_
- Vue3 SFC 和 TSX 方式自定义组件实现 v-model的详细过程_vue.js_
- 详解Angular组件数据不能实时更新到视图上的问题_AngularJS_
- react组件的创建与更新实现流程详解_React_
- react源码层分析协调与调度_React_
点击排行
本栏推荐
