本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
前言
之前在一个大佬的粉丝群里看到了这样一个问题,具体代码如下:
// reactive的案例const user: UnwrapNestedRefs<{ name: string }> = reactive({ name: '卖鱼强' })watch(user, (value) => console.log('第-', value)) // 有效watch(user.name, (value) => console.log('第二', value)) // 无效watch(() => user,(value) => console.log('第三', value)) // 无效watch(() => user.name,(value) => console.log('第四', value)) // 有效// ref案例const userRef: Ref<string> = ref('卖鱼强')watch(userRef, (value) => console.log('第一个watch', value)) // 有效watch(userRef.value, (value) => console.log('第二个watch', value)) // 无效watch(() => userRef, (value) => console.log('第三次watch', value)) // 无效watch(() => userRef.value, (value) => console.log('第四次watch', value)) // 有效user.name = '狂飙强'userRef.value = '狂飙强'疑问就是为什么有些情况下会不生效?
watch 函数定义
首先我们看一下官方对 watch 函数参数与返回值的定义:
// 侦听单个来源function watch<T>( source: WatchSource<T>, callback: WatchCallback<T>, options?: WatchOptions): StopHandle// 侦听多个来源function watch<T>( sources: WatchSource<T>[], callback: WatchCallback<T[]>, options?: WatchOptions): StopHandletype WatchCallback<T> = ( value: T, oldValue: T, onCleanup: (cleanupFn: () => void) => void) => voidtype WatchSource<T> = | Ref<T> // ref | (() => T) // getter | T extends object ? T : never // 响应式对象interface WatchOptions extends WatchEffectOptions { immediate?: boolean // 默认:false deep?: boolean // 默认:false flush?: 'pre' | 'post' | 'sync' // 默认:'pre' onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void}可以看到 watch 方法一共有两种定义:
接收一个
source监听的源数据,一个回调函数,与一个可选配置项,返回一个stop停止监听的方法接收一个
sources源数据数组,一个回调函数,与一个可选配置项,返回一个stop停止监听的方法
其最大的差别就是在 sources 这里,watch 函数既可以监听一个响应式数据对象,也可以通过数组形式同时监听多个数据对象。
并且,这个数据对象的类型默认只有四种情况:
Ref<T>: 一个响应式数据对象() => T: 一个返回对象的箭头函数T extends object: 一个响应式对象以上三种类型的组合(也就是同时监听多个数据源的情况)
不过呢在源码中其实对 watch 函数有多次 重载:
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)type MultiWatchSources = (WatchSource<unknown> | object)[]// 1. 多个数据源,且具有 cb 回调函数export function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false>( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate>): WatchStopHandle// 2. 监听多个数据源,但是其中有只读数据,会在参数处理时中断export 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// 3. 单个数据源,且具有 cb 回调函数export function watch<T, Immediate extends Readonly<boolean> = false>( source: WatchSource<T>, cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, options?: WatchOptions<Immediate>): WatchStopHandle// 4. 监听一个响应式对象,且具有 cb 回调函数export 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// 实现逻辑,调用 doWatch 创建相应的 Effect 副作用export function watch<T = any, Immediate extends Readonly<boolean> = false>( source: T | WatchSource<T>, cb: any, options?: WatchOptions<Immediate>): WatchStopHandle { return doWatch(source as any, cb, options)}由上可见,虽然 watch 函数在 ts 的支持上对参数进行了多次限制说明,但是核心依然是使用 doWatch函数来创建相应的副作用进行依赖收集和 cb 执行的。
doWatch 函数
首先我们先看一下源码:
function doWatch(source, cb, { immediate, deep, flush } = {}) { 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 = () => 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 if (isFunction(source)) { if (cb) { getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER) } } else { getter = NOOP } // 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 } } if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) } let cleanup: () => void let onCleanup: OnCleanup = (fn: () => void) => { cleanup = effect.onStop = () => { callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } } let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE const job: SchedulerJob = () => { if (!effect.active) { return } if (cb) { const newValue = effect.run() if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i])) : hasChanged(newValue, oldValue)) ) { if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onCleanup ]) oldValue = newValue } } } job.allowRecurse = !!cb let scheduler: EffectScheduler ... const effect = new ReactiveEffect(getter, scheduler) if (cb) { if (immediate) { job() } else { oldValue = effect.run() } } return () => { effect.stop() if (instance && instance.scope) { remove(instance.scope.effects!, effect) } }}上面的代码中,从第 7 行直到第 47 行,都是在处理 sources 数据源的处理,最终会将其转换为一个 getter 函数用来创建 ReactiveEffect 和收集数据依赖。
在 getter 的处理过程中,分为以下情况:
isRef(source):是一个ref构造的变量,getter为读取该变量value值的方法,正常收集依赖isReactive(source):如果是reactive构造的变量,此时会默认deep为true,getter为读取该变量的方法,后面会通过traverse进行 依赖收集isFunction(source):(这里将函数和数组置换一下顺序)如果是函数的话,其实getter就是对该函数的调用和执行;但是,这里会通过callWithErrorHandling执行函数获取函数返回值来进行依赖收集,最终的数据是否变化依赖的是函数的返回结果是否发生改变isArray(source):数组格式的数据源配置,则是对上面三种类型数据的集合,会遍历每个数组元素进行依赖收集其他情况,默认
getter为一个空函数,即 不会依赖其他数据,非immediate情况下cb基本上不会执行。
然后,则是上篇文章 Vue 3 Effect 任务调度详解 中说过的 scheduler 调度函数处理部分,确定 cb 的执行时机
回顾问题
在了解了 watch 函数的具体过程之后,我们再回头来看一下上面的问题。
针对 const user: UnwrapNestedRefs<{ name: string }> = reactive({ name: '卖鱼强' }) 的情况
watch(user.name, cb)不生效的原因是因为source为一个 简单数据,无法监听变量;这也侧面反应了,Vue的响应式系统都是面向对象(引用数据类型)来实现的,简单数据都无法被监听watch(() => user, cb)不生效的原因是因为source是一个函数,而函数都是以最终的函数返回值来确定变化的,这里返回的都是user这个对象的引用地址,修改属性是不会触发其改变的,所以不生效
针对 const userRef: Ref<string> = ref('卖鱼强')的情况:
watch(userRef.value, cb)这个原因与watch(user.name, cb)的原因一样watch(() => userRef, cb)这个原因与watch(() => user, cb)的原因一样
综上,watch 可以监听的数据类型总的来说只有 Ref(ComputedRef), Reactive, Function 这三大类或者由这三类数据组成的数组,其他类型则会被直接抛弃;而其中的 Function 类型,则是根据其返回值来进行依赖收集的