使用案例
import { reactive, watch } from 'vue'
// 1. 传入getter函数
const state = reactive({ count: 0 })
watch(() => state.count, (count, prevCount) => {
// 当 state.count 更新,会触发此回调函数
})
// 2. 传入reactive对象
watch(state, (count, prevCount) => {
// 当 state.count 更新,会触发此回调函数
})
// 3. 传入ref对象
const stateRef = ref(0)
watch(stateRef, (count, prevCount) => {
// 当 stateRef.value 更新,会触发此回调函数
})
// 4.监听多个数据源,回调函数接受两个数组,分别对应来源数组中的新值和旧值:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => { /* ... */ })
从上面的例子可以看到,对于 watch,它能接收的第一个参数类型非常多。
你可以传入一个ref对象、一个响应式对象、一个 getter 函数、甚至是一个数组。
我们在上一篇中知道了computed对象其内部是借助了 effect函数 创建了一个 reactiveEffect函数,在访问computed对象的值时,执行其 runner函数 求值。
对于watch来说,其实它的内部也是借助了effect函数来实现
下面请看源码:
处理getter函数
const reactiveGetter = (source: object) =>
deep === true
? source // traverse will happen in wrapped getter below
: // for deep: false, only traverse root-level properties
traverse(source, deep === false ? 1 : undefined)
let getter: () => any
let forceTrigger = false
let isMultiSource = false
// 侦听的数据源是一个 ref 类型的数据
if (isRef(source)) {
getter = () => source.value
// 判断数据源是否是浅响应
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// 侦听的数据源是响应式数据
getter = () => reactiveGetter(source)
forceTrigger = 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 reactiveGetter(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_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 (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup],
)
}
}
} 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
}
}
// 处理的是 watch 的场景
// 递归读取对象的属性值
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
收集依赖的前提是访问了对象属性。只有收集了依赖后去修改属性,才会通知对应的依赖更新。所以这里递归访问对象的子属性,就是为了收集依赖,收集的依赖就是这个 watch内部 的runner函数
export function traverse(
value: unknown,
depth = Infinity,
seen?: Set<unknown>,
) {
if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value
}
seen = seen || new Set()
if (seen.has(value)) {
return value
}
seen.add(value)
depth--
if (isRef(value)) {
traverse(value.value, depth, seen)
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], depth, seen)
}
} else if (isSet(value) || isMap(value)) {
value.forEach((v: any) => {
traverse(v, depth, seen)
})
} else if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key], depth, seen)
}
for (const key of Object.getOwnPropertySymbols(value)) {
if (Object.prototype.propertyIsEnumerable.call(value, key)) {
traverse(value[key as any], depth, seen)
}
}
}
return value
}
标准化 source的处理流程拆解如下:
- 如果 source是 ref对象,则创建一个访问 source.value的 getter函数;
- 如果 source是 source对象,则创建一个访问 source的 getter函数,并设置 deep为 true;
- 如果 source是一个函数,则会继续判断第二个参数 cb是否存在,然后对 cb 函数做简单的封装处理;
- 如果 source是一个数组,则内部会通过 source.map函数映射处一个新的数组,它会判断每个数组元素的类型,映射规则与前面的 source规则一致;
- 如果 source不满足上述条件,则在非生产环境下发出警告,提示 source类型不合法;
创建 scheduler
为了便于控制 watch 的回调函数 cb 的执行时机,需要将 scheduler 调度函数封装为一个独立的 job 函数,如下面的代码所示
创建 job函数的处理流程如下:
判断回调函数 cb是否传入,如果有传入,那么是 watch的调用场景,否则是 watchEffect函数被调用的场景;
-
如果是 watch函数被调用的场景,首先执行副作用函数获取最新的值 newValue,然后判断是否需要执行回调函数 cb的情况:
- 监听的数据是 reactive类型,即 deep 的值为 true;
- 需要强制执行副作用函数,即 forceTrigger为 true;
- 新旧值发生了变化; 如果满足上面条件中的一个,那么先清除副作用函数,然后调用 callWithAsyncErrorHandling函数,将新旧值 newValue和 oldValue传入该函数中,执行完毕后更新旧值 oldValue,避免在下一次执行回调函数 cb时获取到错误的旧值。
-
如果是 watchEffect函数被调用的场景,则直接执行副作用函数即可;
-
设置 job 的 allowRecurse 属性,它能够让 job 作为侦听器的回调,这样调度器就能知道它允许调用自身。
// 将 scheduler 调度函数封装为一个独立的 job 函数,便于在初始化和变更时执行它
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
// watch
if (cb) {
// 处理 watch 的场景
// watch(source, cb)
// 执行副作用函数获取新值
const newValue = effect.run()
// 如果数据源是响应式数据或者需要强制触发副作用函数执行或者新旧值发生了变化
// 则执行回调函数,并更新旧值
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// 当回调再次执行前先清除副作用
// cleanup before running cb again
if (cleanup) {
cleanup()
}
// 执行watch 函数的回调函数 cb,将旧值和新值作为回调函数的参数
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// 首次调用时,将 oldValue 的值设置为 undefined
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
])
// 更新旧值,不然下一次会得到错误的旧值
oldValue = newValue
}
} else {
// watchEffect
// 处理 watchEffect 的场景
effect.run()
}
}
当调用 watch函数时,可以通过 options 的 flush 选项来指定回调函数的执行时机:
- flush: sync,代表它是一个同步的 watcher,即数据变化时同步执行回调函数;
- flush: post,代表调度函数需要将副作用函数放到一个微任务队列中,并等待 DOM 更新结束后再执行;
- flush: pre,即调度器函数默认的执行方式,在组件更新之前执行,如果组件还没有挂载,则在组件挂载之前同步执行回调函数。
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'
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
创建 watch effect
const effect = new ReactiveEffect(getter, NOOP, scheduler)
执行watchEffect
- 判断传入的回调函数 cb是否存在,如果存在,则根据传入的 options选项中 immediate,如果是否为 true,则会在创建 watch的时候立即执行一次,否则,否则就手动调用副作用函数,并将返回值作为旧值,赋值给 oldValue;
- 如果 options的 flush的选项的值为 post,需要将副作用函数放入到微任务队列中,等待组件挂载完成后再执行副作用函数;
- 其余情况就是立即执行副作用函数。
if (cb) {
// 选项参数 immediate 来指定回调是否需要立即执行
if (immediate) {
// 回调函数会在 watch 创建时立即执行一次
job()
} else {
// 手动调用副作用函数,拿到的就是旧值
oldValue = effect.run()
}
}