vue3 源码解析 watch
背景
很多同学之前写vue2,应该都用过watch,但是watchEffect用的相对较少,这次和秋老师一起来探索下,vue3中 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
): StopHandle
type WatchCallback<T> = (
value: T,
oldValue: T,
onCleanup: (cleanupFn: () => void) => void
) => void
type 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
}
简单说下就是支持三种用法,从 WatchSource 可以发现
- 监听一个getter 函数
- 监听一个响应式对象 ref
- 监听以上多个来源
从 WatchOptions 可以看到这里继承的是 WatchEffectOptions ,这个不重要,我们一会讲 WatchEffect的时候分析
watch 实现原理
目前最新的main分支代码
// packages/runtime-core/src/apiWatch.ts
import {
isRef,
isShallow,
Ref,
ComputedRef,
ReactiveEffect,
isReactive,
ReactiveFlags,
EffectScheduler,
DebuggerOptions
} from '@vue/reactivity'
import { SchedulerJob, queueJob } from './scheduler'
import {
EMPTY_OBJ,
isObject,
isArray,
isFunction,
isString,
hasChanged,
NOOP,
remove,
isMap,
isSet,
isPlainObject
} from '@vue/shared'
import {
currentInstance,
ComponentInternalInstance,
isInSSRComponentSetup,
setCurrentInstance,
unsetCurrentInstance
} from './component'
import {
ErrorCodes,
callWithErrorHandling,
callWithAsyncErrorHandling
} from './errorHandling'
import { queuePostRenderEffect } from './renderer'
import { warn } from './warning'
import { DeprecationTypes } from './compat/compatConfig'
import { checkCompatEnabled, isCompatEnabled } from './compat/compatConfig'
import { ObjectWatchOptionItem } from './componentOptions'
export type WatchEffect = (onCleanup: OnCleanup) => void
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
export type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: OnCleanup
) => any
type MapSources<T, Immediate> = {
[K in keyof T]: T[K] extends WatchSource<infer V>
? Immediate extends true
? V | undefined
: V
: T[K] extends object
? Immediate extends true
? T[K] | undefined
: T[K]
: never
}
type OnCleanup = (cleanupFn: () => void) => void
export interface WatchOptionsBase extends DebuggerOptions {
flush?: 'pre' | 'post' | 'sync'
}
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
immediate?: Immediate
deep?: boolean
}
export type WatchStopHandle = () => void
// Simple effect.
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
export function watchPostEffect(
effect: WatchEffect,
options?: DebuggerOptions
) {
return doWatch(
effect,
null,
(__DEV__
? { ...options, flush: 'post' }
: { flush: 'post' }) as WatchOptionsBase
)
}
export function watchSyncEffect(
effect: WatchEffect,
options?: DebuggerOptions
) {
return doWatch(
effect,
null,
(__DEV__
? { ...options, flush: 'sync' }
: { flush: 'sync' }) as WatchOptionsBase
)
}
// initial value for watchers to trigger on undefined initial values
const INITIAL_WATCHER_VALUE = {}
type MultiWatchSources = (WatchSource<unknown> | object)[]
// overload: array of multiple sources + 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
// 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)
}
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
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.`
)
}
}
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.`
)
}
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 {
__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)
}
// 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)
}
}
// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's 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
}
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
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'
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
const effect = new ReactiveEffect(getter, scheduler)
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
// initial run
if (cb) {
if (immediate) {
job()
} else {
oldValue = effect.run()
}
} 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)
}
}
}
// this.$watch
export function instanceWatch(
this: ComponentInternalInstance,
source: string | Function,
value: WatchCallback | ObjectWatchOptionItem,
options?: WatchOptions
): WatchStopHandle {
const publicThis = this.proxy as any
const getter = isString(source)
? source.includes('.')
? createPathGetter(publicThis, source)
: () => publicThis[source]
: source.bind(publicThis, publicThis)
let cb
if (isFunction(value)) {
cb = value
} else {
cb = value.handler as Function
options = value
}
const cur = currentInstance
setCurrentInstance(this)
const res = doWatch(getter, cb.bind(publicThis), options)
if (cur) {
setCurrentInstance(cur)
} else {
unsetCurrentInstance()
}
return res
}
export function createPathGetter(ctx: any, path: string) {
const segments = path.split('.')
return () => {
let cur = ctx
for (let i = 0; i < segments.length && cur; i++) {
cur = cur[segments[i]]
}
return cur
}
}
// traverse 会递归去访问对象的深层子属性,对性能敏感的场景此处是个优化点
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
}
太复杂了看看测试用例
it('watching single source: getter', async () => {
const state = reactive({ count: 0 })
let dummy
watch(
() => state.count,
(count, prevCount) => {
dummy = [count, prevCount]
// assert types
count + 1
if (prevCount) {
prevCount + 1
}
}
)
state.count++
await nextTick()
expect(dummy).toMatchObject([1, 0])
})
把上面的源码精简后
function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
// 判断是开发环境 然后判断cb 是不是函数,不是的话 warn 用户,这点提示很友好
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)
}
主要是用了doWatch ,接着往下看 PS : 出现了 anyScript
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
if (__DEV__ && !cb) {
// 友好的开发提示
}
const warnInvalidSource = (s: unknown) => {
// 友好的开发提示
}
const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false
// 接收多个数据来源,这边统一处理下
if (isRef(source)) {
// 处理ref的情况,获取值,forceTrigger设为true
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// 处理Reactive的情况,递归遍历,deep标记为true
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)) {
// 如果是一个函数,判断cb是否存在,存在就callWithErrorHandling
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)
}
// 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())
}
// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's 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
}
// 这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
// 创建job
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) {
// 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
// 第一次更改时,传递的旧值为 undefined
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
// 如果存在cb,会先执行effect.run 函数,实际就是前面创建的getter函数,然后进行判断,如果是deep或者是forceTrigger强制更新
// 创建 scheduler 的作用,是根据某种调度的方法去执行某种函数。
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'
// 默认选项,在组件更新前执行
job.pre = true
if (instance){
job.id = instance.uid
}
scheduler = () => queueJob(job)
}
// flush 属性有3种情况
// 当 flush 为 sync ,表示它是个同步 watcher,即数据变化时同步执行回调函数
// 当 flush 为 post ,表示它是通过 queuePostRenderEffect,当组件更新之后执行
// 当 flush 为 pre ,当组件更新之前执行
// ------
// 创建effect
const effect = new ReactiveEffect(getter, scheduler)
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
// initial run
// 初次执行
if (cb) {
if (immediate) {
job()
} else {
// 求旧值
oldValue = effect.run()
}
} 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)
}
}
}
这里贴出部分代码供参考
// packages/runtime-core/src/scheduler.ts
export function queueJob(job: SchedulerJob) {
// the dedupe search uses the startIndex argument of Array.includes()
// by default the search index includes the current job that is being run
// so it cannot recursively trigger itself again.
// if the job is a watch() callback, the search will start with a +1 index to
// allow it recursively trigger itself - it is the user's responsibility to
// ensure it doesn't end up in an infinite loop.
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
if (job.id == null) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
// 内部是promise.then
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
下次来探索下这个 queueJob 到底干了啥