feat(reactivity): base `watch`, `getCurrentWatcher`, and `onWatcherCleanup` (#9927)
This commit is contained in:
parent
44973bb3e7
commit
205e5b5e27
|
@ -0,0 +1,196 @@
|
||||||
|
import {
|
||||||
|
EffectScope,
|
||||||
|
type Ref,
|
||||||
|
WatchErrorCodes,
|
||||||
|
type WatchOptions,
|
||||||
|
type WatchScheduler,
|
||||||
|
onWatcherCleanup,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from '../src'
|
||||||
|
|
||||||
|
const queue: (() => void)[] = []
|
||||||
|
|
||||||
|
// a simple scheduler for testing purposes
|
||||||
|
let isFlushPending = false
|
||||||
|
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
|
||||||
|
const nextTick = (fn?: () => any) =>
|
||||||
|
fn ? resolvedPromise.then(fn) : resolvedPromise
|
||||||
|
|
||||||
|
const scheduler: WatchScheduler = (job, isFirstRun) => {
|
||||||
|
if (isFirstRun) {
|
||||||
|
job()
|
||||||
|
} else {
|
||||||
|
queue.push(job)
|
||||||
|
flushJobs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flushJobs = () => {
|
||||||
|
if (isFlushPending) return
|
||||||
|
isFlushPending = true
|
||||||
|
resolvedPromise.then(() => {
|
||||||
|
queue.forEach(job => job())
|
||||||
|
queue.length = 0
|
||||||
|
isFlushPending = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('watch', () => {
|
||||||
|
test('effect', () => {
|
||||||
|
let dummy: any
|
||||||
|
const source = ref(0)
|
||||||
|
watch(() => {
|
||||||
|
dummy = source.value
|
||||||
|
})
|
||||||
|
expect(dummy).toBe(0)
|
||||||
|
source.value++
|
||||||
|
expect(dummy).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('with callback', () => {
|
||||||
|
let dummy: any
|
||||||
|
const source = ref(0)
|
||||||
|
watch(source, () => {
|
||||||
|
dummy = source.value
|
||||||
|
})
|
||||||
|
expect(dummy).toBe(undefined)
|
||||||
|
source.value++
|
||||||
|
expect(dummy).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('call option with error handling', () => {
|
||||||
|
const onError = vi.fn()
|
||||||
|
const call: WatchOptions['call'] = function call(fn, type, args) {
|
||||||
|
if (Array.isArray(fn)) {
|
||||||
|
fn.forEach(f => call(f, type, args))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fn.apply(null, args)
|
||||||
|
} catch (e) {
|
||||||
|
onError(e, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => {
|
||||||
|
throw 'oops in effect'
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{ call },
|
||||||
|
)
|
||||||
|
|
||||||
|
const source = ref(0)
|
||||||
|
const effect = watch(
|
||||||
|
source,
|
||||||
|
() => {
|
||||||
|
onWatcherCleanup(() => {
|
||||||
|
throw 'oops in cleanup'
|
||||||
|
})
|
||||||
|
throw 'oops in watch'
|
||||||
|
},
|
||||||
|
{ call },
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(onError.mock.calls.length).toBe(1)
|
||||||
|
expect(onError.mock.calls[0]).toMatchObject([
|
||||||
|
'oops in effect',
|
||||||
|
WatchErrorCodes.WATCH_CALLBACK,
|
||||||
|
])
|
||||||
|
|
||||||
|
source.value++
|
||||||
|
expect(onError.mock.calls.length).toBe(2)
|
||||||
|
expect(onError.mock.calls[1]).toMatchObject([
|
||||||
|
'oops in watch',
|
||||||
|
WatchErrorCodes.WATCH_CALLBACK,
|
||||||
|
])
|
||||||
|
|
||||||
|
effect!.stop()
|
||||||
|
source.value++
|
||||||
|
expect(onError.mock.calls.length).toBe(3)
|
||||||
|
expect(onError.mock.calls[2]).toMatchObject([
|
||||||
|
'oops in cleanup',
|
||||||
|
WatchErrorCodes.WATCH_CLEANUP,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('watch with onWatcherCleanup', async () => {
|
||||||
|
let dummy = 0
|
||||||
|
let source: Ref<number>
|
||||||
|
const scope = new EffectScope()
|
||||||
|
|
||||||
|
scope.run(() => {
|
||||||
|
source = ref(0)
|
||||||
|
watch(onCleanup => {
|
||||||
|
source.value
|
||||||
|
|
||||||
|
onCleanup(() => (dummy += 2))
|
||||||
|
onWatcherCleanup(() => (dummy += 3))
|
||||||
|
onWatcherCleanup(() => (dummy += 5))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
expect(dummy).toBe(0)
|
||||||
|
|
||||||
|
scope.run(() => {
|
||||||
|
source.value++
|
||||||
|
})
|
||||||
|
expect(dummy).toBe(10)
|
||||||
|
|
||||||
|
scope.run(() => {
|
||||||
|
source.value++
|
||||||
|
})
|
||||||
|
expect(dummy).toBe(20)
|
||||||
|
|
||||||
|
scope.stop()
|
||||||
|
expect(dummy).toBe(30)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('nested calls to baseWatch and onWatcherCleanup', async () => {
|
||||||
|
let calls: string[] = []
|
||||||
|
let source: Ref<number>
|
||||||
|
let copyist: Ref<number>
|
||||||
|
const scope = new EffectScope()
|
||||||
|
|
||||||
|
scope.run(() => {
|
||||||
|
source = ref(0)
|
||||||
|
copyist = ref(0)
|
||||||
|
// sync by default
|
||||||
|
watch(
|
||||||
|
() => {
|
||||||
|
const current = (copyist.value = source.value)
|
||||||
|
onWatcherCleanup(() => calls.push(`sync ${current}`))
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
// with scheduler
|
||||||
|
watch(
|
||||||
|
() => {
|
||||||
|
const current = copyist.value
|
||||||
|
onWatcherCleanup(() => calls.push(`post ${current}`))
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{ scheduler },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual([])
|
||||||
|
|
||||||
|
scope.run(() => source.value++)
|
||||||
|
expect(calls).toEqual(['sync 0'])
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual(['sync 0', 'post 0'])
|
||||||
|
calls.length = 0
|
||||||
|
|
||||||
|
scope.run(() => source.value++)
|
||||||
|
expect(calls).toEqual(['sync 1'])
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual(['sync 1', 'post 1'])
|
||||||
|
calls.length = 0
|
||||||
|
|
||||||
|
scope.stop()
|
||||||
|
expect(calls).toEqual(['sync 2', 'post 2'])
|
||||||
|
})
|
||||||
|
})
|
|
@ -80,3 +80,14 @@ export {
|
||||||
} from './effectScope'
|
} from './effectScope'
|
||||||
export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations'
|
export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations'
|
||||||
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
|
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
|
||||||
|
export {
|
||||||
|
watch,
|
||||||
|
getCurrentWatcher,
|
||||||
|
traverse,
|
||||||
|
onWatcherCleanup,
|
||||||
|
WatchErrorCodes,
|
||||||
|
type WatchOptions,
|
||||||
|
type WatchScheduler,
|
||||||
|
type WatchStopHandle,
|
||||||
|
type WatchHandle,
|
||||||
|
} from './watch'
|
||||||
|
|
|
@ -0,0 +1,368 @@
|
||||||
|
import {
|
||||||
|
EMPTY_OBJ,
|
||||||
|
NOOP,
|
||||||
|
hasChanged,
|
||||||
|
isArray,
|
||||||
|
isFunction,
|
||||||
|
isMap,
|
||||||
|
isObject,
|
||||||
|
isPlainObject,
|
||||||
|
isSet,
|
||||||
|
remove,
|
||||||
|
} from '@vue/shared'
|
||||||
|
import { warn } from './warning'
|
||||||
|
import type { ComputedRef } from './computed'
|
||||||
|
import { ReactiveFlags } from './constants'
|
||||||
|
import {
|
||||||
|
type DebuggerOptions,
|
||||||
|
EffectFlags,
|
||||||
|
type EffectScheduler,
|
||||||
|
ReactiveEffect,
|
||||||
|
pauseTracking,
|
||||||
|
resetTracking,
|
||||||
|
} from './effect'
|
||||||
|
import { isReactive, isShallow } from './reactive'
|
||||||
|
import { type Ref, isRef } from './ref'
|
||||||
|
import { getCurrentScope } from './effectScope'
|
||||||
|
|
||||||
|
// These errors were transferred from `packages/runtime-core/src/errorHandling.ts`
|
||||||
|
// to @vue/reactivity to allow co-location with the moved base watch logic, hence
|
||||||
|
// it is essential to keep these values unchanged.
|
||||||
|
export enum WatchErrorCodes {
|
||||||
|
WATCH_GETTER = 2,
|
||||||
|
WATCH_CALLBACK,
|
||||||
|
WATCH_CLEANUP,
|
||||||
|
}
|
||||||
|
|
||||||
|
type WatchEffect = (onCleanup: OnCleanup) => void
|
||||||
|
type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
|
||||||
|
type WatchCallback<V = any, OV = any> = (
|
||||||
|
value: V,
|
||||||
|
oldValue: OV,
|
||||||
|
onCleanup: OnCleanup,
|
||||||
|
) => any
|
||||||
|
type OnCleanup = (cleanupFn: () => void) => void
|
||||||
|
|
||||||
|
export interface WatchOptions<Immediate = boolean> extends DebuggerOptions {
|
||||||
|
immediate?: Immediate
|
||||||
|
deep?: boolean | number
|
||||||
|
once?: boolean
|
||||||
|
scheduler?: WatchScheduler
|
||||||
|
onWarn?: (msg: string, ...args: any[]) => void
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
augmentJob?: (job: (...args: any[]) => void) => void
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
call?: (
|
||||||
|
fn: Function | Function[],
|
||||||
|
type: WatchErrorCodes,
|
||||||
|
args?: unknown[],
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WatchStopHandle = () => void
|
||||||
|
|
||||||
|
export interface WatchHandle extends WatchStopHandle {
|
||||||
|
pause: () => void
|
||||||
|
resume: () => void
|
||||||
|
stop: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// initial value for watchers to trigger on undefined initial values
|
||||||
|
const INITIAL_WATCHER_VALUE = {}
|
||||||
|
|
||||||
|
export type WatchScheduler = (job: () => void, isFirstRun: boolean) => void
|
||||||
|
|
||||||
|
const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()
|
||||||
|
let activeWatcher: ReactiveEffect | undefined = undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current active effect if there is one.
|
||||||
|
*/
|
||||||
|
export function getCurrentWatcher(): ReactiveEffect<any> | undefined {
|
||||||
|
return activeWatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a cleanup callback on the current active effect. This
|
||||||
|
* registered cleanup callback will be invoked right before the
|
||||||
|
* associated effect re-runs.
|
||||||
|
*
|
||||||
|
* @param cleanupFn - The callback function to attach to the effect's cleanup.
|
||||||
|
*/
|
||||||
|
export function onWatcherCleanup(
|
||||||
|
cleanupFn: () => void,
|
||||||
|
failSilently = false,
|
||||||
|
owner: ReactiveEffect | undefined = activeWatcher,
|
||||||
|
): void {
|
||||||
|
if (owner) {
|
||||||
|
let cleanups = cleanupMap.get(owner)
|
||||||
|
if (!cleanups) cleanupMap.set(owner, (cleanups = []))
|
||||||
|
cleanups.push(cleanupFn)
|
||||||
|
} else if (__DEV__ && !failSilently) {
|
||||||
|
warn(
|
||||||
|
`onWatcherCleanup() was called when there was no active watcher` +
|
||||||
|
` to associate with.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function watch(
|
||||||
|
source: WatchSource | WatchSource[] | WatchEffect | object,
|
||||||
|
cb?: WatchCallback | null,
|
||||||
|
options: WatchOptions = EMPTY_OBJ,
|
||||||
|
): WatchHandle {
|
||||||
|
const { immediate, deep, once, scheduler, augmentJob, call } = options
|
||||||
|
|
||||||
|
const warnInvalidSource = (s: unknown) => {
|
||||||
|
;(options.onWarn || 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 reactiveGetter = (source: object) => {
|
||||||
|
// traverse will happen in wrapped getter below
|
||||||
|
if (deep) return source
|
||||||
|
// for `deep: false | 0` or shallow reactive, only traverse root-level properties
|
||||||
|
if (isShallow(source) || deep === false || deep === 0)
|
||||||
|
return traverse(source, 1)
|
||||||
|
// for `deep: undefined` on a reactive object, deeply traverse all properties
|
||||||
|
return traverse(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
let effect: ReactiveEffect
|
||||||
|
let getter: () => any
|
||||||
|
let cleanup: (() => void) | undefined
|
||||||
|
let boundCleanup: typeof onWatcherCleanup
|
||||||
|
let forceTrigger = false
|
||||||
|
let isMultiSource = false
|
||||||
|
|
||||||
|
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 call ? call(s, WatchErrorCodes.WATCH_GETTER) : s()
|
||||||
|
} else {
|
||||||
|
__DEV__ && warnInvalidSource(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (isFunction(source)) {
|
||||||
|
if (cb) {
|
||||||
|
// getter with cb
|
||||||
|
getter = call
|
||||||
|
? () => call(source, WatchErrorCodes.WATCH_GETTER)
|
||||||
|
: (source as () => any)
|
||||||
|
} else {
|
||||||
|
// no cb -> simple effect
|
||||||
|
getter = () => {
|
||||||
|
if (cleanup) {
|
||||||
|
pauseTracking()
|
||||||
|
try {
|
||||||
|
cleanup()
|
||||||
|
} finally {
|
||||||
|
resetTracking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const currentEffect = activeWatcher
|
||||||
|
activeWatcher = effect
|
||||||
|
try {
|
||||||
|
return call
|
||||||
|
? call(source, WatchErrorCodes.WATCH_CALLBACK, [boundCleanup])
|
||||||
|
: source(boundCleanup)
|
||||||
|
} finally {
|
||||||
|
activeWatcher = currentEffect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getter = NOOP
|
||||||
|
__DEV__ && warnInvalidSource(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cb && deep) {
|
||||||
|
const baseGetter = getter
|
||||||
|
const depth = deep === true ? Infinity : deep
|
||||||
|
getter = () => traverse(baseGetter(), depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (once) {
|
||||||
|
if (cb) {
|
||||||
|
const _cb = cb
|
||||||
|
cb = (...args) => {
|
||||||
|
_cb(...args)
|
||||||
|
effect.stop()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const _getter = getter
|
||||||
|
getter = () => {
|
||||||
|
_getter()
|
||||||
|
effect.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let oldValue: any = isMultiSource
|
||||||
|
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
|
||||||
|
: INITIAL_WATCHER_VALUE
|
||||||
|
|
||||||
|
const job = (immediateFirstRun?: boolean) => {
|
||||||
|
if (
|
||||||
|
!(effect.flags & EffectFlags.ACTIVE) ||
|
||||||
|
(!effect.dirty && !immediateFirstRun)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (cb) {
|
||||||
|
// watch(source, cb)
|
||||||
|
const newValue = effect.run()
|
||||||
|
if (
|
||||||
|
deep ||
|
||||||
|
forceTrigger ||
|
||||||
|
(isMultiSource
|
||||||
|
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
|
||||||
|
: hasChanged(newValue, oldValue))
|
||||||
|
) {
|
||||||
|
// cleanup before running cb again
|
||||||
|
if (cleanup) {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
const currentWatcher = activeWatcher
|
||||||
|
activeWatcher = effect
|
||||||
|
try {
|
||||||
|
const args = [
|
||||||
|
newValue,
|
||||||
|
// pass undefined as the old value when it's changed for the first time
|
||||||
|
oldValue === INITIAL_WATCHER_VALUE
|
||||||
|
? undefined
|
||||||
|
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
|
||||||
|
? []
|
||||||
|
: oldValue,
|
||||||
|
boundCleanup,
|
||||||
|
]
|
||||||
|
call
|
||||||
|
? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args)
|
||||||
|
: // @ts-expect-error
|
||||||
|
cb!(...args)
|
||||||
|
oldValue = newValue
|
||||||
|
} finally {
|
||||||
|
activeWatcher = currentWatcher
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// watchEffect
|
||||||
|
effect.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (augmentJob) {
|
||||||
|
augmentJob(job)
|
||||||
|
}
|
||||||
|
|
||||||
|
effect = new ReactiveEffect(getter)
|
||||||
|
|
||||||
|
effect.scheduler = scheduler
|
||||||
|
? () => scheduler(job, false)
|
||||||
|
: (job as EffectScheduler)
|
||||||
|
|
||||||
|
boundCleanup = fn => onWatcherCleanup(fn, false, effect)
|
||||||
|
|
||||||
|
cleanup = effect.onStop = () => {
|
||||||
|
const cleanups = cleanupMap.get(effect)
|
||||||
|
if (cleanups) {
|
||||||
|
if (call) {
|
||||||
|
call(cleanups, WatchErrorCodes.WATCH_CLEANUP)
|
||||||
|
} else {
|
||||||
|
for (const cleanup of cleanups) cleanup()
|
||||||
|
}
|
||||||
|
cleanupMap.delete(effect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
effect.onTrack = options.onTrack
|
||||||
|
effect.onTrigger = options.onTrigger
|
||||||
|
}
|
||||||
|
|
||||||
|
// initial run
|
||||||
|
if (cb) {
|
||||||
|
if (immediate) {
|
||||||
|
job(true)
|
||||||
|
} else {
|
||||||
|
oldValue = effect.run()
|
||||||
|
}
|
||||||
|
} else if (scheduler) {
|
||||||
|
scheduler(job.bind(null, true), true)
|
||||||
|
} else {
|
||||||
|
effect.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = getCurrentScope()
|
||||||
|
const watchHandle: WatchHandle = () => {
|
||||||
|
effect.stop()
|
||||||
|
if (scope) {
|
||||||
|
remove(scope.effects, effect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchHandle.pause = effect.pause.bind(effect)
|
||||||
|
watchHandle.resume = effect.resume.bind(effect)
|
||||||
|
watchHandle.stop = watchHandle
|
||||||
|
|
||||||
|
return watchHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traverse(
|
||||||
|
value: unknown,
|
||||||
|
depth: number = Infinity,
|
||||||
|
seen?: Set<unknown>,
|
||||||
|
): 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
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import {
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
nextTick,
|
nextTick,
|
||||||
onErrorCaptured,
|
onErrorCaptured,
|
||||||
|
onWatcherCleanup,
|
||||||
reactive,
|
reactive,
|
||||||
ref,
|
ref,
|
||||||
watch,
|
watch,
|
||||||
|
@ -435,6 +436,35 @@ describe('api: watch', () => {
|
||||||
expect(cleanup).toHaveBeenCalledTimes(2)
|
expect(cleanup).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('onWatcherCleanup', async () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const cleanupEffect = vi.fn()
|
||||||
|
const cleanupWatch = vi.fn()
|
||||||
|
|
||||||
|
const stopEffect = watchEffect(() => {
|
||||||
|
onWatcherCleanup(cleanupEffect)
|
||||||
|
count.value
|
||||||
|
})
|
||||||
|
const stopWatch = watch(count, () => {
|
||||||
|
onWatcherCleanup(cleanupWatch)
|
||||||
|
})
|
||||||
|
|
||||||
|
count.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(cleanupEffect).toHaveBeenCalledTimes(1)
|
||||||
|
expect(cleanupWatch).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
|
count.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(cleanupEffect).toHaveBeenCalledTimes(2)
|
||||||
|
expect(cleanupWatch).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
stopEffect()
|
||||||
|
expect(cleanupEffect).toHaveBeenCalledTimes(3)
|
||||||
|
stopWatch()
|
||||||
|
expect(cleanupWatch).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
it('flush timing: pre (default)', async () => {
|
it('flush timing: pre (default)', async () => {
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
const count2 = ref(0)
|
const count2 = ref(0)
|
||||||
|
|
|
@ -1,50 +1,28 @@
|
||||||
import {
|
import {
|
||||||
|
type WatchOptions as BaseWatchOptions,
|
||||||
type ComputedRef,
|
type ComputedRef,
|
||||||
type DebuggerOptions,
|
type DebuggerOptions,
|
||||||
EffectFlags,
|
|
||||||
type EffectScheduler,
|
|
||||||
ReactiveEffect,
|
|
||||||
ReactiveFlags,
|
|
||||||
type ReactiveMarker,
|
type ReactiveMarker,
|
||||||
type Ref,
|
type Ref,
|
||||||
getCurrentScope,
|
type WatchHandle,
|
||||||
isReactive,
|
watch as baseWatch,
|
||||||
isRef,
|
|
||||||
isShallow,
|
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler'
|
import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler'
|
||||||
import {
|
import { EMPTY_OBJ, NOOP, extend, isFunction, isString } from '@vue/shared'
|
||||||
EMPTY_OBJ,
|
|
||||||
NOOP,
|
|
||||||
extend,
|
|
||||||
hasChanged,
|
|
||||||
isArray,
|
|
||||||
isFunction,
|
|
||||||
isMap,
|
|
||||||
isObject,
|
|
||||||
isPlainObject,
|
|
||||||
isSet,
|
|
||||||
isString,
|
|
||||||
remove,
|
|
||||||
} from '@vue/shared'
|
|
||||||
import {
|
import {
|
||||||
type ComponentInternalInstance,
|
type ComponentInternalInstance,
|
||||||
currentInstance,
|
currentInstance,
|
||||||
isInSSRComponentSetup,
|
isInSSRComponentSetup,
|
||||||
setCurrentInstance,
|
setCurrentInstance,
|
||||||
} from './component'
|
} from './component'
|
||||||
import {
|
import { callWithAsyncErrorHandling } from './errorHandling'
|
||||||
ErrorCodes,
|
|
||||||
callWithAsyncErrorHandling,
|
|
||||||
callWithErrorHandling,
|
|
||||||
} from './errorHandling'
|
|
||||||
import { queuePostRenderEffect } from './renderer'
|
import { queuePostRenderEffect } from './renderer'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { DeprecationTypes } from './compat/compatConfig'
|
|
||||||
import { checkCompatEnabled, isCompatEnabled } from './compat/compatConfig'
|
|
||||||
import type { ObjectWatchOptionItem } from './componentOptions'
|
import type { ObjectWatchOptionItem } from './componentOptions'
|
||||||
import { useSSRContext } from './helpers/useSsrContext'
|
import { useSSRContext } from './helpers/useSsrContext'
|
||||||
|
|
||||||
|
export type { WatchHandle, WatchStopHandle } from '@vue/reactivity'
|
||||||
|
|
||||||
export type WatchEffect = (onCleanup: OnCleanup) => void
|
export type WatchEffect = (onCleanup: OnCleanup) => void
|
||||||
|
|
||||||
export type WatchSource<T = any> = Ref<T, any> | ComputedRef<T> | (() => T)
|
export type WatchSource<T = any> = Ref<T, any> | ComputedRef<T> | (() => T)
|
||||||
|
@ -77,14 +55,6 @@ export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
|
||||||
once?: boolean
|
once?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WatchStopHandle = () => void
|
|
||||||
|
|
||||||
export interface WatchHandle extends WatchStopHandle {
|
|
||||||
pause: () => void
|
|
||||||
resume: () => void
|
|
||||||
stop: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple effect.
|
// Simple effect.
|
||||||
export function watchEffect(
|
export function watchEffect(
|
||||||
effect: WatchEffect,
|
effect: WatchEffect,
|
||||||
|
@ -96,7 +66,7 @@ export function watchEffect(
|
||||||
export function watchPostEffect(
|
export function watchPostEffect(
|
||||||
effect: WatchEffect,
|
effect: WatchEffect,
|
||||||
options?: DebuggerOptions,
|
options?: DebuggerOptions,
|
||||||
): WatchStopHandle {
|
): WatchHandle {
|
||||||
return doWatch(
|
return doWatch(
|
||||||
effect,
|
effect,
|
||||||
null,
|
null,
|
||||||
|
@ -107,7 +77,7 @@ export function watchPostEffect(
|
||||||
export function watchSyncEffect(
|
export function watchSyncEffect(
|
||||||
effect: WatchEffect,
|
effect: WatchEffect,
|
||||||
options?: DebuggerOptions,
|
options?: DebuggerOptions,
|
||||||
): WatchStopHandle {
|
): WatchHandle {
|
||||||
return doWatch(
|
return doWatch(
|
||||||
effect,
|
effect,
|
||||||
null,
|
null,
|
||||||
|
@ -115,9 +85,6 @@ export function watchSyncEffect(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initial value for watchers to trigger on undefined initial values
|
|
||||||
const INITIAL_WATCHER_VALUE = {}
|
|
||||||
|
|
||||||
export type MultiWatchSources = (WatchSource<unknown> | object)[]
|
export type MultiWatchSources = (WatchSource<unknown> | object)[]
|
||||||
|
|
||||||
// overload: single source + cb
|
// overload: single source + cb
|
||||||
|
@ -178,22 +145,9 @@ export function watch<T = any, Immediate extends Readonly<boolean> = false>(
|
||||||
function doWatch(
|
function doWatch(
|
||||||
source: WatchSource | WatchSource[] | WatchEffect | object,
|
source: WatchSource | WatchSource[] | WatchEffect | object,
|
||||||
cb: WatchCallback | null,
|
cb: WatchCallback | null,
|
||||||
{
|
options: WatchOptions = EMPTY_OBJ,
|
||||||
immediate,
|
|
||||||
deep,
|
|
||||||
flush,
|
|
||||||
once,
|
|
||||||
onTrack,
|
|
||||||
onTrigger,
|
|
||||||
}: WatchOptions = EMPTY_OBJ,
|
|
||||||
): WatchHandle {
|
): WatchHandle {
|
||||||
if (cb && once) {
|
const { immediate, deep, flush, once } = options
|
||||||
const _cb = cb
|
|
||||||
cb = (...args) => {
|
|
||||||
_cb(...args)
|
|
||||||
watchHandle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (__DEV__ && !cb) {
|
if (__DEV__ && !cb) {
|
||||||
if (immediate !== undefined) {
|
if (immediate !== undefined) {
|
||||||
|
@ -216,230 +170,65 @@ function doWatch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const warnInvalidSource = (s: unknown) => {
|
const baseWatchOptions: BaseWatchOptions = extend({}, options)
|
||||||
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
|
if (__DEV__) baseWatchOptions.onWarn = warn
|
||||||
const reactiveGetter = (source: object) => {
|
|
||||||
// traverse will happen in wrapped getter below
|
|
||||||
if (deep) return source
|
|
||||||
// for `deep: false | 0` or shallow reactive, only traverse root-level properties
|
|
||||||
if (isShallow(source) || deep === false || deep === 0)
|
|
||||||
return traverse(source, 1)
|
|
||||||
// for `deep: undefined` on a reactive object, deeply traverse all properties
|
|
||||||
return traverse(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
let getter: () => any
|
|
||||||
let forceTrigger = false
|
|
||||||
let isMultiSource = false
|
|
||||||
|
|
||||||
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 {
|
|
||||||
__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 (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
|
|
||||||
const depth = deep === true ? Infinity : deep
|
|
||||||
getter = () => traverse(baseGetter(), depth)
|
|
||||||
}
|
|
||||||
|
|
||||||
let cleanup: (() => void) | undefined
|
|
||||||
let onCleanup: OnCleanup = (fn: () => void) => {
|
|
||||||
cleanup = effect.onStop = () => {
|
|
||||||
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
|
|
||||||
cleanup = effect.onStop = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// in SSR there is no need to setup an actual effect, and it should be noop
|
|
||||||
// unless it's eager or sync flush
|
|
||||||
let ssrCleanup: (() => void)[] | undefined
|
let ssrCleanup: (() => void)[] | undefined
|
||||||
if (__SSR__ && isInSSRComponentSetup) {
|
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,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
if (flush === 'sync') {
|
if (flush === 'sync') {
|
||||||
const ctx = useSSRContext()!
|
const ctx = useSSRContext()!
|
||||||
ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
|
ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
|
||||||
|
} else if (!cb || immediate) {
|
||||||
|
// immediately watch or watchEffect
|
||||||
|
baseWatchOptions.once = true
|
||||||
} else {
|
} else {
|
||||||
const watchHandle: WatchHandle = () => {}
|
return {
|
||||||
watchHandle.stop = NOOP
|
stop: NOOP,
|
||||||
watchHandle.resume = NOOP
|
resume: NOOP,
|
||||||
watchHandle.pause = NOOP
|
pause: NOOP,
|
||||||
return watchHandle
|
} as WatchHandle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let oldValue: any = isMultiSource
|
const instance = currentInstance
|
||||||
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
|
baseWatchOptions.call = (fn, type, args) =>
|
||||||
: INITIAL_WATCHER_VALUE
|
callWithAsyncErrorHandling(fn, instance, type, args)
|
||||||
const job: SchedulerJob = (immediateFirstRun?: boolean) => {
|
|
||||||
if (
|
// scheduler
|
||||||
!(effect.flags & EffectFlags.ACTIVE) ||
|
let isPre = false
|
||||||
(!effect.dirty && !immediateFirstRun)
|
if (flush === 'post') {
|
||||||
) {
|
baseWatchOptions.scheduler = job => {
|
||||||
return
|
queuePostRenderEffect(job, instance && instance.suspense)
|
||||||
}
|
|
||||||
if (cb) {
|
|
||||||
// watch(source, cb)
|
|
||||||
const newValue = effect.run()
|
|
||||||
if (
|
|
||||||
deep ||
|
|
||||||
forceTrigger ||
|
|
||||||
(isMultiSource
|
|
||||||
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[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
|
|
||||||
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
|
|
||||||
? []
|
|
||||||
: oldValue,
|
|
||||||
onCleanup,
|
|
||||||
])
|
|
||||||
oldValue = newValue
|
|
||||||
}
|
}
|
||||||
|
} else if (flush !== 'sync') {
|
||||||
|
// default: 'pre'
|
||||||
|
isPre = true
|
||||||
|
baseWatchOptions.scheduler = (job, isFirstRun) => {
|
||||||
|
if (isFirstRun) {
|
||||||
|
job()
|
||||||
} else {
|
} else {
|
||||||
// watchEffect
|
queueJob(job)
|
||||||
effect.run()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
baseWatchOptions.augmentJob = (job: SchedulerJob) => {
|
||||||
// important: mark the job as a watcher callback so that scheduler knows
|
// important: mark the job as a watcher callback so that scheduler knows
|
||||||
// it is allowed to self-trigger (#1727)
|
// it is allowed to self-trigger (#1727)
|
||||||
if (cb) job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
|
if (cb) {
|
||||||
|
job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
|
||||||
const effect = new ReactiveEffect(getter)
|
}
|
||||||
|
if (isPre) {
|
||||||
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.flags! |= SchedulerJobFlags.PRE
|
job.flags! |= SchedulerJobFlags.PRE
|
||||||
if (instance) {
|
if (instance) {
|
||||||
job.id = instance.uid
|
job.id = instance.uid
|
||||||
job.i = instance
|
;(job as SchedulerJob).i = instance
|
||||||
}
|
}
|
||||||
scheduler = () => queueJob(job)
|
|
||||||
}
|
|
||||||
effect.scheduler = scheduler
|
|
||||||
|
|
||||||
const scope = getCurrentScope()
|
|
||||||
const watchHandle: WatchHandle = () => {
|
|
||||||
effect.stop()
|
|
||||||
if (scope) {
|
|
||||||
remove(scope.effects, effect)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watchHandle.pause = effect.pause.bind(effect)
|
const watchHandle = baseWatch(source, cb, baseWatchOptions)
|
||||||
watchHandle.resume = effect.resume.bind(effect)
|
|
||||||
watchHandle.stop = watchHandle
|
|
||||||
|
|
||||||
if (__DEV__) {
|
|
||||||
effect.onTrack = onTrack
|
|
||||||
effect.onTrigger = onTrigger
|
|
||||||
}
|
|
||||||
|
|
||||||
// initial run
|
|
||||||
if (cb) {
|
|
||||||
if (immediate) {
|
|
||||||
job(true)
|
|
||||||
} else {
|
|
||||||
oldValue = effect.run()
|
|
||||||
}
|
|
||||||
} else if (flush === 'post') {
|
|
||||||
queuePostRenderEffect(
|
|
||||||
effect.run.bind(effect),
|
|
||||||
instance && instance.suspense,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
effect.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (__SSR__ && ssrCleanup) ssrCleanup.push(watchHandle)
|
if (__SSR__ && ssrCleanup) ssrCleanup.push(watchHandle)
|
||||||
return watchHandle
|
return watchHandle
|
||||||
|
@ -481,41 +270,3 @@ export function createPathGetter(ctx: any, path: string) {
|
||||||
return cur
|
return cur
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function traverse(
|
|
||||||
value: unknown,
|
|
||||||
depth: number = Infinity,
|
|
||||||
seen?: Set<unknown>,
|
|
||||||
): 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
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import type {
|
import {
|
||||||
Component,
|
type Component,
|
||||||
ComponentInternalInstance,
|
type ComponentInternalInstance,
|
||||||
ComponentInternalOptions,
|
type ComponentInternalOptions,
|
||||||
ConcreteComponent,
|
type ConcreteComponent,
|
||||||
Data,
|
type Data,
|
||||||
InternalRenderFunction,
|
type InternalRenderFunction,
|
||||||
SetupContext,
|
type SetupContext,
|
||||||
|
currentInstance,
|
||||||
} from './component'
|
} from './component'
|
||||||
import {
|
import {
|
||||||
type LooseRequired,
|
type LooseRequired,
|
||||||
|
@ -18,7 +19,7 @@ import {
|
||||||
isPromise,
|
isPromise,
|
||||||
isString,
|
isString,
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { type Ref, isRef } from '@vue/reactivity'
|
import { type Ref, getCurrentScope, isRef, traverse } from '@vue/reactivity'
|
||||||
import { computed } from './apiComputed'
|
import { computed } from './apiComputed'
|
||||||
import {
|
import {
|
||||||
type WatchCallback,
|
type WatchCallback,
|
||||||
|
@ -71,7 +72,7 @@ import { warn } from './warning'
|
||||||
import type { VNodeChild } from './vnode'
|
import type { VNodeChild } from './vnode'
|
||||||
import { callWithAsyncErrorHandling } from './errorHandling'
|
import { callWithAsyncErrorHandling } from './errorHandling'
|
||||||
import { deepMergeData } from './compat/data'
|
import { deepMergeData } from './compat/data'
|
||||||
import { DeprecationTypes } from './compat/compatConfig'
|
import { DeprecationTypes, checkCompatEnabled } from './compat/compatConfig'
|
||||||
import {
|
import {
|
||||||
type CompatConfig,
|
type CompatConfig,
|
||||||
isCompatEnabled,
|
isCompatEnabled,
|
||||||
|
@ -848,18 +849,55 @@ export function createWatcher(
|
||||||
publicThis: ComponentPublicInstance,
|
publicThis: ComponentPublicInstance,
|
||||||
key: string,
|
key: string,
|
||||||
): void {
|
): void {
|
||||||
const getter = key.includes('.')
|
let getter = key.includes('.')
|
||||||
? createPathGetter(publicThis, key)
|
? createPathGetter(publicThis, key)
|
||||||
: () => (publicThis as any)[key]
|
: () => (publicThis as any)[key]
|
||||||
|
|
||||||
|
const options: WatchOptions = {}
|
||||||
|
if (__COMPAT__) {
|
||||||
|
const instance =
|
||||||
|
currentInstance && getCurrentScope() === currentInstance.scope
|
||||||
|
? currentInstance
|
||||||
|
: null
|
||||||
|
|
||||||
|
const newValue = getter()
|
||||||
|
if (
|
||||||
|
isArray(newValue) &&
|
||||||
|
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
|
||||||
|
) {
|
||||||
|
options.deep = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseGetter = getter
|
||||||
|
getter = () => {
|
||||||
|
const val = baseGetter()
|
||||||
|
if (
|
||||||
|
isArray(val) &&
|
||||||
|
checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
|
||||||
|
) {
|
||||||
|
traverse(val)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isString(raw)) {
|
if (isString(raw)) {
|
||||||
const handler = ctx[raw]
|
const handler = ctx[raw]
|
||||||
if (isFunction(handler)) {
|
if (isFunction(handler)) {
|
||||||
|
if (__COMPAT__) {
|
||||||
|
watch(getter, handler as WatchCallback, options)
|
||||||
|
} else {
|
||||||
watch(getter, handler as WatchCallback)
|
watch(getter, handler as WatchCallback)
|
||||||
|
}
|
||||||
} else if (__DEV__) {
|
} else if (__DEV__) {
|
||||||
warn(`Invalid watch handler specified by key "${raw}"`, handler)
|
warn(`Invalid watch handler specified by key "${raw}"`, handler)
|
||||||
}
|
}
|
||||||
} else if (isFunction(raw)) {
|
} else if (isFunction(raw)) {
|
||||||
|
if (__COMPAT__) {
|
||||||
|
watch(getter, raw.bind(publicThis), options)
|
||||||
|
} else {
|
||||||
watch(getter, raw.bind(publicThis))
|
watch(getter, raw.bind(publicThis))
|
||||||
|
}
|
||||||
} else if (isObject(raw)) {
|
} else if (isObject(raw)) {
|
||||||
if (isArray(raw)) {
|
if (isArray(raw)) {
|
||||||
raw.forEach(r => createWatcher(r, ctx, publicThis, key))
|
raw.forEach(r => createWatcher(r, ctx, publicThis, key))
|
||||||
|
@ -868,7 +906,7 @@ export function createWatcher(
|
||||||
? raw.handler.bind(publicThis)
|
? raw.handler.bind(publicThis)
|
||||||
: (ctx[raw.handler] as WatchCallback)
|
: (ctx[raw.handler] as WatchCallback)
|
||||||
if (isFunction(handler)) {
|
if (isFunction(handler)) {
|
||||||
watch(getter, handler, raw)
|
watch(getter, handler, __COMPAT__ ? extend(raw, options) : raw)
|
||||||
} else if (__DEV__) {
|
} else if (__DEV__) {
|
||||||
warn(`Invalid watch handler specified by key "${raw.handler}"`, handler)
|
warn(`Invalid watch handler specified by key "${raw.handler}"`, handler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,7 @@ import { currentRenderingInstance } from './componentRenderContext'
|
||||||
import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
|
import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
|
||||||
import type { ComponentPublicInstance } from './componentPublicInstance'
|
import type { ComponentPublicInstance } from './componentPublicInstance'
|
||||||
import { mapCompatDirectiveHook } from './compat/customDirective'
|
import { mapCompatDirectiveHook } from './compat/customDirective'
|
||||||
import { pauseTracking, resetTracking } from '@vue/reactivity'
|
import { pauseTracking, resetTracking, traverse } from '@vue/reactivity'
|
||||||
import { traverse } from './apiWatch'
|
|
||||||
|
|
||||||
export interface DirectiveBinding<
|
export interface DirectiveBinding<
|
||||||
Value = any,
|
Value = any,
|
||||||
|
|
|
@ -4,16 +4,20 @@ import type { ComponentInternalInstance } from './component'
|
||||||
import { popWarningContext, pushWarningContext, warn } from './warning'
|
import { popWarningContext, pushWarningContext, warn } from './warning'
|
||||||
import { EMPTY_OBJ, isArray, isFunction, isPromise } from '@vue/shared'
|
import { EMPTY_OBJ, isArray, isFunction, isPromise } from '@vue/shared'
|
||||||
import { LifecycleHooks } from './enums'
|
import { LifecycleHooks } from './enums'
|
||||||
|
import { WatchErrorCodes } from '@vue/reactivity'
|
||||||
|
|
||||||
// contexts where user provided function may be executed, in addition to
|
// contexts where user provided function may be executed, in addition to
|
||||||
// lifecycle hooks.
|
// lifecycle hooks.
|
||||||
export enum ErrorCodes {
|
export enum ErrorCodes {
|
||||||
SETUP_FUNCTION,
|
SETUP_FUNCTION,
|
||||||
RENDER_FUNCTION,
|
RENDER_FUNCTION,
|
||||||
WATCH_GETTER,
|
// The error codes for the watch have been transferred to the reactivity
|
||||||
WATCH_CALLBACK,
|
// package along with baseWatch to maintain code compatibility. Hence,
|
||||||
WATCH_CLEANUP,
|
// it is essential to keep these values unchanged.
|
||||||
NATIVE_EVENT_HANDLER,
|
// WATCH_GETTER,
|
||||||
|
// WATCH_CALLBACK,
|
||||||
|
// WATCH_CLEANUP,
|
||||||
|
NATIVE_EVENT_HANDLER = 5,
|
||||||
COMPONENT_EVENT_HANDLER,
|
COMPONENT_EVENT_HANDLER,
|
||||||
VNODE_HOOK,
|
VNODE_HOOK,
|
||||||
DIRECTIVE_HOOK,
|
DIRECTIVE_HOOK,
|
||||||
|
@ -27,7 +31,7 @@ export enum ErrorCodes {
|
||||||
APP_UNMOUNT_CLEANUP,
|
APP_UNMOUNT_CLEANUP,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ErrorTypeStrings: Record<LifecycleHooks | ErrorCodes, string> = {
|
export const ErrorTypeStrings: Record<ErrorTypes, string> = {
|
||||||
[LifecycleHooks.SERVER_PREFETCH]: 'serverPrefetch hook',
|
[LifecycleHooks.SERVER_PREFETCH]: 'serverPrefetch hook',
|
||||||
[LifecycleHooks.BEFORE_CREATE]: 'beforeCreate hook',
|
[LifecycleHooks.BEFORE_CREATE]: 'beforeCreate hook',
|
||||||
[LifecycleHooks.CREATED]: 'created hook',
|
[LifecycleHooks.CREATED]: 'created hook',
|
||||||
|
@ -44,9 +48,9 @@ export const ErrorTypeStrings: Record<LifecycleHooks | ErrorCodes, string> = {
|
||||||
[LifecycleHooks.RENDER_TRIGGERED]: 'renderTriggered hook',
|
[LifecycleHooks.RENDER_TRIGGERED]: 'renderTriggered hook',
|
||||||
[ErrorCodes.SETUP_FUNCTION]: 'setup function',
|
[ErrorCodes.SETUP_FUNCTION]: 'setup function',
|
||||||
[ErrorCodes.RENDER_FUNCTION]: 'render function',
|
[ErrorCodes.RENDER_FUNCTION]: 'render function',
|
||||||
[ErrorCodes.WATCH_GETTER]: 'watcher getter',
|
[WatchErrorCodes.WATCH_GETTER]: 'watcher getter',
|
||||||
[ErrorCodes.WATCH_CALLBACK]: 'watcher callback',
|
[WatchErrorCodes.WATCH_CALLBACK]: 'watcher callback',
|
||||||
[ErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function',
|
[WatchErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function',
|
||||||
[ErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler',
|
[ErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler',
|
||||||
[ErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler',
|
[ErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler',
|
||||||
[ErrorCodes.VNODE_HOOK]: 'vnode hook',
|
[ErrorCodes.VNODE_HOOK]: 'vnode hook',
|
||||||
|
@ -61,7 +65,7 @@ export const ErrorTypeStrings: Record<LifecycleHooks | ErrorCodes, string> = {
|
||||||
[ErrorCodes.APP_UNMOUNT_CLEANUP]: 'app unmount cleanup function',
|
[ErrorCodes.APP_UNMOUNT_CLEANUP]: 'app unmount cleanup function',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ErrorTypes = LifecycleHooks | ErrorCodes
|
export type ErrorTypes = LifecycleHooks | ErrorCodes | WatchErrorCodes
|
||||||
|
|
||||||
export function callWithErrorHandling(
|
export function callWithErrorHandling(
|
||||||
fn: Function,
|
fn: Function,
|
||||||
|
|
|
@ -28,6 +28,8 @@ export {
|
||||||
// effect
|
// effect
|
||||||
effect,
|
effect,
|
||||||
stop,
|
stop,
|
||||||
|
getCurrentWatcher,
|
||||||
|
onWatcherCleanup,
|
||||||
ReactiveEffect,
|
ReactiveEffect,
|
||||||
// effect scope
|
// effect scope
|
||||||
effectScope,
|
effectScope,
|
||||||
|
|
Loading…
Reference in New Issue