本文为原创文章,未获授权禁止转载,侵权必究!
本篇是 Vue3 源码解析系列第 5 篇,关注专栏
前言
Vue3 中响应式系统除了 reactive
和 ref
这两个函数外,我们还需了解下 computed
和 watch
这两个函数,它们也是响应式系统的关键所在,本篇我们来看下 watch
是如何实现的。
案例
首先引入 reactive
、effect
、watch
三个函数,之后声明 obj
响应式数据,接着执行 watch
函数,该函数第一个参为监听数据,第二个参为监听回调,最后两秒后又修改 obj
的 name
值。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { reactive, effect, watch } = Vue
// 1. reactive 构建响应性数据
const obj = reactive({
name: 'jc'
})
// 2. 执行了 watch 函数
watch(obj, (value, oldValue) => {
console.log('watch 触发了')
console.log(value)
})
// 3. 两秒后触发 setter 行为
setTimeout(() => {
obj.name = 'cc'
}, 2000)
</script>
</body>
</html>
watch 实现
watch
函数定义在 packages/runtime-core/src/apiWatch.ts
文件下:
ts
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
函数实际执行的是 doWatch
方法:
ts
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
// 省略
const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false
if (isRef(source)) {
// 是否 ref 类型
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// 是否 reactive 类型
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 {
__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)
}
// 省略
if (cb && deep) {
const baseGetter = getter // getter 为 () => source
getter = () => traverse(baseGetter())
}
// 省略
// 定义 oldValue isMultiSource 是否有多个源 [value1, value2] 需要监听
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
// job 核心逻辑
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
if (cb) {
// 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()
}
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// 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
effect.run()
}
}
// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb
let scheduler: EffectScheduler
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) // 调度器赋值 也是核心逻辑
}
const effect = new ReactiveEffect(getter, scheduler)
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
// initial run
if (cb) {
if (immediate) {
// 默认自动执行 watch 一次
job() // job 触发意味着 watch 被立即执行一次
} else {
oldValue = effect.run() // 等于执行 fn 函数 即 () => traverse(baseGetter()) 即 () => source 即 传入的监听数据
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
effect.run()
}
return () => {
effect.stop() // 监听停止
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
}
根据传入 source
监听数据类型不同走不同逻辑,当前 source
为 reactive
类型,所以 getter
直接赋值为 () => source
。另外还可以看到类型为 reactive
时,默认开启深度监听 deep = true
。由于存在 cb
监听回调和 deep
,所以baseGetter
等于 getter
,即 () => source
, getter
赋值为 () => traverse(baseGetter())
。
之后又定义了 oldValue
值,默认为空对象,也是回调函数中的 oldValue
。接着定义了一个 job
函数,这是 watch
的核心逻辑 ,我们稍后再来分析。然后又创建了一个调度器 scheduler
,我们在 computed
文中也提到过,在依赖触发时,会执行该方法。此时 scheduler
被赋值为 () => queuePreFlushCb(job)
,将 job
函数传入到 queuePreFlushCb
方法中,该逻辑之后来分析。
接着又创建了一个 ReactiveEffect
实例,将赋值后的 getter
和 scheduler
传入,ReactiveEffect
的作用之前文章也提到过,这里不再具体说明。
由于存在 cb
回调函数,根据判断配置中 immediate
存在时,就执行 job
方法,我们可以理解为 job
的触发 watch
被立即执行一次。否则执行 effect.run
即执行 fn
方法,当前 fn
为 getter
即 () => traverse(baseGetter())
,就是执行 () => source
,结果为传入的监听对象 source
:
最后 oldValue
赋值:
此时 watch
函数执行完毕,两秒后触发 obj
的 setter
行为,依赖触发 trigger
执行,再看下当前 effects
:
之后再遍历执行每个 effect
,此时存在 scheduler
调度器,执行 scheduler
方法。当前 scheduler
为 之前赋值的 () => queuePreFlushCb(job)
,我们再来看下 queuePreFlushCb
方法,该方法定义在 packages/runtime-core/src/scheduler.ts
文件中:
ts
export function queuePreFlushCb(cb: SchedulerJob) {
queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}
实际执行的是 queueCb
方法:
ts
function queueCb(
cb: SchedulerJobs,
activeQueue: SchedulerJob[] | null,
pendingQueue: SchedulerJob[],
index: number
) {
if (!isArray(cb)) {
if (
!activeQueue ||
!activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
) {
pendingQueue.push(cb)
}
} else {
// if cb is an array, it is a component lifecycle hook which can only be
// triggered by a job, which is already deduped in the main queue, so
// we can skip duplicate check here to improve perf
pendingQueue.push(...cb)
}
queueFlush()
}
该方法定义了一个 pendingQueue
队列数组,插入传入的 cb
回调即传入的 job
函数,然后执行 queueFlush
方法:
ts
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
可以看出 watch
的 job
执行都是一个 微任务
。当前同步任务执行完毕后,执行微任务 ,之后执行 flushJobs
方法:
ts
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map()
}
flushPreFlushCbs(seen)
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child so its render effect will have smaller
// priority number)
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped.
queue.sort((a, b) => getId(a) - getId(b))
// conditional usage of checkRecursiveUpdate must be determined out of
// try ... catch block since Rollup by default de-optimizes treeshaking
// inside try-catch. This can leave all warning code unshaked. Although
// they would get eventually shaken by a minifier like terser, some minifiers
// would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (__DEV__ && check(job)) {
continue
}
// console.log(`running:`, job.id)
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0
queue.length = 0
flushPostFlushCbs(seen)
isFlushing = false
currentFlushPromise = null
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (
queue.length ||
pendingPreFlushCbs.length ||
pendingPostFlushCbs.length
) {
flushJobs(seen)
}
}
}
然后执行 flushPreFlushCbs(seen)
方法:
ts
export function flushPreFlushCbs(
seen?: CountMap,
parentJob: SchedulerJob | null = null
) {
if (pendingPreFlushCbs.length) {
currentPreFlushParentJob = parentJob // job 函数
activePreFlushCbs = [...new Set(pendingPreFlushCbs)] // 取代 pendingPreFlushCbs
pendingPreFlushCbs.length = 0 // 置空 下次不会再触发
if (__DEV__) {
seen = seen || new Map()
}
for (
preFlushIndex = 0;
preFlushIndex < activePreFlushCbs.length;
preFlushIndex++
) {
if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
) {
continue
}
activePreFlushCbs[preFlushIndex]() // 当前 job 函数执行
}
activePreFlushCbs = null
preFlushIndex = 0
currentPreFlushParentJob = null
// recursively flush until it drains
flushPreFlushCbs(seen, parentJob)
}
}
当前 pendingPreFlushCbs
为传入的 job
方法,之后将去重后的 pendingPreFlushCbs
赋值给 activePreFlushCbs
,遍历执行 activePreFlushCbs[preFlushIndex]()
,实际是执行每个 job
函数,我们再回过来看下 job
函数:
ts
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
if (cb) {
// 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()
}
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// 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
effect.run()
}
}
run
方法执行实际执行 getter
即 () => traverse(baseGetter())
,此时 newValue
:
这里还得再看下 traverse
方法:
ts
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)
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
}
该方法由于值类型不同,会递归处理返回最终的值。接着执行 callWithAsyncErrorHandling
方法:
ts
export function callWithAsyncErrorHandling(
fn: Function | Function[],
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
): any[] {
if (isFunction(fn)) {
const res = callWithErrorHandling(fn, instance, type, args)
if (res && isPromise(res)) {
res.catch(err => {
handleError(err, instance, type)
})
}
return res
}
const values = []
for (let i = 0; i < fn.length; i++) {
values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
}
return values
}
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
) {
let res
// 统一处理监听 错误
try {
res = args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type)
}
return res
}
可以看出 callWithErrorHandling
这里执行了 cb
回调函数即 watch
传入的匿名函数,callWithAsyncErrorHandling
主要是对错误统一监听处理。最后将 newValue
赋值给 oldValue
,watch
至此执行完毕。
总结
watch
函数实际执行的是doWatch
方法。- 调度器
scheduler
在watch
中很关键。 scheduler
、ReactiveEffect
两者之间存在互相作用的关系,一旦effect
触发了scheduler
,那么会导致queuePreFlushCb(job)
执行,job
方法就被塞入微任务的队列中。- 只要
job()
触发,那么就表示watch
触发了一次。