引言
想起上次面试 ,问了个古老的问题: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<Immediate = boolean> extends WatchOptionsBase {
immediate?: Immediate
deep?: boolean
}
2. 回调cb
了解完options ,接下来我们看看回调**cb** 。通常我们的cb接收三个参数:value 、oldValue和onCleanUp,然后执行我们需要的操作 ,比如侦听表格的页码 ,发生变化时重新请求数据 。第三个参数onCleanUp,用于注册副作用清理的回调函数, 在副作用下次执行之前 ,这个回调函数会被调用 ,通常用来清除不需要的或者无效的副作用 。
// 副作用
export type WatchEffect = (onCleanup: OnCleanup) => void
export type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: OnCleanup
) => any
type OnCleanup = (cleanupFn: () => void) => void
3. 数据源source
watch函数可以侦听单个数据或者多个数据 ,共有四种重载 ,对应四种类型的source 。其中 ,单个数据源的类型有WatchSource和响应式的object ,多个数据源的类型为MultiWatchSources ,Readonly<MultiWatchSources> ,而MultiWatchSources其实也就是由单个数据源组成的数组 。
// 单数据源类型:可以是 Ref 或 ComputedRef 或 函数
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
// 多数据源类型
type MultiWatchSources = (WatchSource<unknown> | object)[]
二 、watch函数
下面是源码中的类型声明 ,以及watch的重载签名和实现签名:
// watch的重载与实现
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
// 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<MultiWatchSources>,
Immediate extends Readonly<boolean> = false
>(
source: T,
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// overload: single source + 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
// overload: watching reactive object w/ 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
// implementation
export 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的实现签名中可以看到 ,和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<unknown>) {
if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value
}
seen = seen || new Set()
if (seen.has(value)) {
return value
}
seen.add(value)
// Ref
if (isRef(value)) {
traverse(value.value, seen)
} else if (isArray(value)) {
// 数组
for (let i = 0; i < value.length; i++) {
traverse(value[i], seen)
}
} else if (isSet(value) || isMap(value)) {
// 集合与映射
value.forEach((v: any) => {
traverse(v, seen)
})
} else if (isPlainObject(value)) {
// 普通对象
for (const key in value) {
traverse((value as any)[key], seen)
}
}
return value
}
至此 ,getter就设置好了 。之后声明了cleanup和onCleanup,用于清除副作用 。以及SSR检测 。虽然不是本文的重点 ,但还是贴一下源码:
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
// in SSR there is no need to setup an actual effect, and it should be noop
// unless its eager
if (__SSR__ && isInSSRComponentSetup) {
// we will also not call the invalidate callback (+ runner is not set up)
onCleanup = NOOP
if (!cb) {
getter()
} else if (immediate) {
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
getter(),
isMultiSource ? [] : undefined,
onCleanup
])
}
return NOOP
}
随后就是重头戏了 ,拿到oldValue ,以及在job函数中取得newValue ,这不就是我们在使用watch的时候的熟悉套路嘛 。
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
// job为当前watch要做的工作 ,后续通过调度器来处理
const job: SchedulerJob = () => {
// 当前effect不在active状态 ,说明没有触发该effect的响应式变化 ,直接返回
if (!effect.active) {
return
}
// cb存在 ,说明是watch ,而不是watchEffect
if (cb) {
// watch(source, cb)
// 调用 effect.run 得到新的值 newValue
const newValue = effect.run()
if (
deep ||
forceTrigger ||
// 取到的新值和旧值是否相同 ,如果有变化则进入分支
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue)) ||
// 兼容2.x
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
// 用异步异常处理程序包裹了一层来调用cb
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when its changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
])
// cb执行完成 ,当前的新值就变成了旧值
oldValue = newValue
}
} else {
// cb不存在 ,则是watchEffect
// watchEffect
effect.run()
}
}
// 设置allowRecurse,让调度器知道它可以自己触发
job.allowRecurse = !!cb
一看job里 ,在watch的分支出现了effect ,但是这个分支并没有effect呀,再往下看 ,噢 ,原来是由之前取得的getter来创建的effect。在这之前 ,还定义了调度器 ,调度器scheduler被糅合进了effect里 ,影响了newValue的获取 ,从而影响cb的调用时机:
sync:同步执行 ,也就是回调cb直接执行;
pre:默认值是pre ,表示组件更新前执行;
post:组件更新后执行 。
let scheduler: EffectScheduler
// 根据flush的值来创建不同的调度器
if (flush === sync) {
scheduler = job as any // the scheduler function gets called directly
} else if (flush === post) {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: pre
scheduler = () => queuePreFlushCb(job)
}
// 为 watch 创建 effect ,watchEffect就不必了 ,因为自带的有
const effect = new ReactiveEffect(getter, scheduler)
// 主要是调试用的onTrack和onTrigger ,当收集依赖和触发更新时做一些操作
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
现在来到了doWatch最后的环节了:侦听器的初始化 。
immediate:如果为真值。将直接调用一次job ,上文我们知道,job是包裹了一层错误处理程序来调用cb ,所以我们现在终于亲眼看到了为什么immediate能让cb立即触发一次 。
// initial run
// 有cb ,是 watch
if (cb) {
if (immediate) {
job()
} else {
// 获取一下当前的值作为旧值
oldValue = effect.run()
}
} else if (flush === post) {
// 没有cb,是watchEffect ,副作用的时机在组件更新之后 ,用queuePostRenderEffect包裹一层来调整时机
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
// watchEffect ,副作用的时机在组件更新之前 ,直接执行一次effect.run
effect.run()
}
// 返回一个WatchStopHandle ,内部执行 effect.stop来达到停止侦听的作用
return () => {
effect.stop()
// 移除当前实例作用域下的当前effect
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
到这里 ,watch的源码算是差不多结束了 。小结一下核心流程:
watch:判断若没有cb则告警;
watch:尾调用doWatch ,之后的操作都在doWatch里进行;
doWatch:判断没有cb时若设置了deep或immediate则告警;
doWatch:根据source的类型得到getter;
doWatch:如果cb存在且deep为真则对getter()进行递归遍历;
doWatch:获取oldValue ,声明job函数 ,在job内部获取newValue并使用callWithAsyncErrorHandling来调用cb 。
doWatch:根据post的值定义的调度器scheduler;
doWatch:根据getter和scheduler创建effect;
doWatch:初始化侦听器 ,如果有cb且immediate为真值 ,则立即调用job函数 ,相当于调用我们写的cb;如果immediate为假值,则只调用effect.run()来初始化oldValue;
doWatch:返回一个WatchStopHandle ,内部通过effect.stop()来实现停止侦听 。
watch:接收到doWatch返回的WatchStopHandle ,并返回给外部使用 。
以上就是Vue3源码解析watch函数实例的详细内容,更多关于Vue3 watch函数的资料请关注本站其它相关文章!
声明:本站所有文章 ,如无特殊说明或标注 ,均为本站原创发布 。任何个人或组织 ,在未征得本站同意时 ,禁止复制 、盗用 、采集 、发布本站内容到任何网站 、书籍等各类媒体平台 。如若本站内容侵犯了原著者的合法权益 ,可联系我们进行处理 。