mirror of https://github.com/vuejs/core.git
feat(runtime-core, reactivity): `onEffectCleanup` and `baseWatch` (#82)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
c9fe3f12e7
commit
bdf28de8e8
|
@ -0,0 +1,178 @@
|
||||||
|
import type { Scheduler, SchedulerJob } from '../src/baseWatch'
|
||||||
|
import {
|
||||||
|
BaseWatchErrorCodes,
|
||||||
|
EffectScope,
|
||||||
|
type Ref,
|
||||||
|
baseWatch,
|
||||||
|
onEffectCleanup,
|
||||||
|
ref,
|
||||||
|
} from '../src'
|
||||||
|
|
||||||
|
const queue: SchedulerJob[] = []
|
||||||
|
|
||||||
|
// these codes are a simple scheduler
|
||||||
|
let isFlushPending = false
|
||||||
|
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
|
||||||
|
const nextTick = (fn?: () => any) =>
|
||||||
|
fn ? resolvedPromise.then(fn) : resolvedPromise
|
||||||
|
const scheduler: Scheduler = job => {
|
||||||
|
queue.push(job)
|
||||||
|
flushJobs()
|
||||||
|
}
|
||||||
|
const flushJobs = () => {
|
||||||
|
if (isFlushPending) return
|
||||||
|
isFlushPending = true
|
||||||
|
resolvedPromise.then(() => {
|
||||||
|
queue.forEach(job => job())
|
||||||
|
queue.length = 0
|
||||||
|
isFlushPending = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('baseWatch', () => {
|
||||||
|
test('effect', () => {
|
||||||
|
let dummy: any
|
||||||
|
const source = ref(0)
|
||||||
|
baseWatch(() => {
|
||||||
|
dummy = source.value
|
||||||
|
})
|
||||||
|
expect(dummy).toBe(0)
|
||||||
|
source.value++
|
||||||
|
expect(dummy).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('watch', () => {
|
||||||
|
let dummy: any
|
||||||
|
const source = ref(0)
|
||||||
|
baseWatch(source, () => {
|
||||||
|
dummy = source.value
|
||||||
|
})
|
||||||
|
expect(dummy).toBe(undefined)
|
||||||
|
source.value++
|
||||||
|
expect(dummy).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('custom error handler', () => {
|
||||||
|
const onError = vi.fn()
|
||||||
|
|
||||||
|
baseWatch(
|
||||||
|
() => {
|
||||||
|
throw 'oops in effect'
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{ onError },
|
||||||
|
)
|
||||||
|
|
||||||
|
const source = ref(0)
|
||||||
|
const effect = baseWatch(
|
||||||
|
source,
|
||||||
|
() => {
|
||||||
|
onEffectCleanup(() => {
|
||||||
|
throw 'oops in cleanup'
|
||||||
|
})
|
||||||
|
throw 'oops in watch'
|
||||||
|
},
|
||||||
|
{ onError },
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(onError.mock.calls.length).toBe(1)
|
||||||
|
expect(onError.mock.calls[0]).toMatchObject([
|
||||||
|
'oops in effect',
|
||||||
|
BaseWatchErrorCodes.WATCH_CALLBACK,
|
||||||
|
])
|
||||||
|
|
||||||
|
source.value++
|
||||||
|
expect(onError.mock.calls.length).toBe(2)
|
||||||
|
expect(onError.mock.calls[1]).toMatchObject([
|
||||||
|
'oops in watch',
|
||||||
|
BaseWatchErrorCodes.WATCH_CALLBACK,
|
||||||
|
])
|
||||||
|
|
||||||
|
effect!.stop()
|
||||||
|
source.value++
|
||||||
|
expect(onError.mock.calls.length).toBe(3)
|
||||||
|
expect(onError.mock.calls[2]).toMatchObject([
|
||||||
|
'oops in cleanup',
|
||||||
|
BaseWatchErrorCodes.WATCH_CLEANUP,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('baseWatch with onEffectCleanup', async () => {
|
||||||
|
let dummy = 0
|
||||||
|
let source: Ref<number>
|
||||||
|
const scope = new EffectScope()
|
||||||
|
|
||||||
|
scope.run(() => {
|
||||||
|
source = ref(0)
|
||||||
|
baseWatch(onCleanup => {
|
||||||
|
source.value
|
||||||
|
|
||||||
|
onCleanup(() => (dummy += 2))
|
||||||
|
onEffectCleanup(() => (dummy += 3))
|
||||||
|
onEffectCleanup(() => (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 onEffectCleanup', 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
|
||||||
|
baseWatch(
|
||||||
|
() => {
|
||||||
|
const current = (copyist.value = source.value)
|
||||||
|
onEffectCleanup(() => calls.push(`sync ${current}`))
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
// with scheduler
|
||||||
|
baseWatch(
|
||||||
|
() => {
|
||||||
|
const current = copyist.value
|
||||||
|
onEffectCleanup(() => 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'])
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,417 @@
|
||||||
|
import {
|
||||||
|
EMPTY_OBJ,
|
||||||
|
NOOP,
|
||||||
|
hasChanged,
|
||||||
|
isArray,
|
||||||
|
isFunction,
|
||||||
|
isMap,
|
||||||
|
isObject,
|
||||||
|
isPlainObject,
|
||||||
|
isPromise,
|
||||||
|
isSet,
|
||||||
|
} from '@vue/shared'
|
||||||
|
import { warn } from './warning'
|
||||||
|
import type { ComputedRef } from './computed'
|
||||||
|
import { ReactiveFlags } from './constants'
|
||||||
|
import {
|
||||||
|
type DebuggerOptions,
|
||||||
|
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`
|
||||||
|
// along with baseWatch to maintain code compatibility. Hence,
|
||||||
|
// it is essential to keep these values unchanged.
|
||||||
|
export enum BaseWatchErrorCodes {
|
||||||
|
WATCH_GETTER = 2,
|
||||||
|
WATCH_CALLBACK,
|
||||||
|
WATCH_CLEANUP,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO move to a scheduler package
|
||||||
|
export interface SchedulerJob extends Function {
|
||||||
|
id?: number
|
||||||
|
pre?: boolean
|
||||||
|
active?: boolean
|
||||||
|
computed?: boolean
|
||||||
|
/**
|
||||||
|
* Indicates whether the effect is allowed to recursively trigger itself
|
||||||
|
* when managed by the scheduler.
|
||||||
|
*
|
||||||
|
* By default, a job cannot trigger itself because some built-in method calls,
|
||||||
|
* e.g. Array.prototype.push actually performs reads as well (#1740) which
|
||||||
|
* can lead to confusing infinite loops.
|
||||||
|
* The allowed cases are component update functions and watch callbacks.
|
||||||
|
* Component update functions may update child component props, which in turn
|
||||||
|
* trigger flush: "pre" watch callbacks that mutates state that the parent
|
||||||
|
* relies on (#1801). Watch callbacks doesn't track its dependencies so if it
|
||||||
|
* triggers itself again, it's likely intentional and it is the user's
|
||||||
|
* responsibility to perform recursive state mutation that eventually
|
||||||
|
* stabilizes (#1727).
|
||||||
|
*/
|
||||||
|
allowRecurse?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
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 BaseWatchOptions<Immediate = boolean> extends DebuggerOptions {
|
||||||
|
immediate?: Immediate
|
||||||
|
deep?: boolean
|
||||||
|
once?: boolean
|
||||||
|
scheduler?: Scheduler
|
||||||
|
onError?: HandleError
|
||||||
|
onWarn?: HandleWarn
|
||||||
|
}
|
||||||
|
|
||||||
|
// initial value for watchers to trigger on undefined initial values
|
||||||
|
const INITIAL_WATCHER_VALUE = {}
|
||||||
|
|
||||||
|
export type Scheduler = (
|
||||||
|
job: SchedulerJob,
|
||||||
|
effect: ReactiveEffect,
|
||||||
|
isInit: boolean,
|
||||||
|
) => void
|
||||||
|
export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void
|
||||||
|
export type HandleWarn = (msg: string, ...args: any[]) => void
|
||||||
|
|
||||||
|
const DEFAULT_SCHEDULER: Scheduler = job => job()
|
||||||
|
const DEFAULT_HANDLE_ERROR: HandleError = (err: unknown) => {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()
|
||||||
|
let activeEffect: ReactiveEffect | undefined = undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current active effect if there is one.
|
||||||
|
*/
|
||||||
|
export function getCurrentEffect() {
|
||||||
|
return activeEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 onEffectCleanup(cleanupFn: () => void) {
|
||||||
|
if (activeEffect) {
|
||||||
|
const cleanups =
|
||||||
|
cleanupMap.get(activeEffect) ||
|
||||||
|
cleanupMap.set(activeEffect, []).get(activeEffect)!
|
||||||
|
cleanups.push(cleanupFn)
|
||||||
|
} else if (__DEV__) {
|
||||||
|
warn(
|
||||||
|
`onEffectCleanup() was called when there was no active effect` +
|
||||||
|
` to associate with.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function baseWatch(
|
||||||
|
source: WatchSource | WatchSource[] | WatchEffect | object,
|
||||||
|
cb?: WatchCallback | null,
|
||||||
|
{
|
||||||
|
immediate,
|
||||||
|
deep,
|
||||||
|
once,
|
||||||
|
scheduler = DEFAULT_SCHEDULER,
|
||||||
|
onWarn = __DEV__ ? warn : NOOP,
|
||||||
|
onError = DEFAULT_HANDLE_ERROR,
|
||||||
|
onTrack,
|
||||||
|
onTrigger,
|
||||||
|
}: BaseWatchOptions = EMPTY_OBJ,
|
||||||
|
): ReactiveEffect | undefined {
|
||||||
|
const warnInvalidSource = (s: unknown) => {
|
||||||
|
onWarn(
|
||||||
|
`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) =>
|
||||||
|
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 effect: ReactiveEffect
|
||||||
|
let getter: () => any
|
||||||
|
let cleanup: (() => void) | undefined
|
||||||
|
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,
|
||||||
|
onError,
|
||||||
|
BaseWatchErrorCodes.WATCH_GETTER,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
__DEV__ && warnInvalidSource(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (isFunction(source)) {
|
||||||
|
if (cb) {
|
||||||
|
// getter with cb
|
||||||
|
getter = () =>
|
||||||
|
callWithErrorHandling(source, onError, BaseWatchErrorCodes.WATCH_GETTER)
|
||||||
|
} else {
|
||||||
|
// no cb -> simple effect
|
||||||
|
getter = () => {
|
||||||
|
if (cleanup) {
|
||||||
|
pauseTracking()
|
||||||
|
try {
|
||||||
|
cleanup()
|
||||||
|
} finally {
|
||||||
|
resetTracking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const currentEffect = activeEffect
|
||||||
|
activeEffect = effect
|
||||||
|
try {
|
||||||
|
return callWithAsyncErrorHandling(
|
||||||
|
source,
|
||||||
|
onError,
|
||||||
|
BaseWatchErrorCodes.WATCH_CALLBACK,
|
||||||
|
[onEffectCleanup],
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
activeEffect = currentEffect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getter = NOOP
|
||||||
|
__DEV__ && warnInvalidSource(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cb && deep) {
|
||||||
|
const baseGetter = getter
|
||||||
|
getter = () => traverse(baseGetter())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (once) {
|
||||||
|
if (!cb) {
|
||||||
|
// onEffectCleanup need use effect as a key
|
||||||
|
getCurrentScope()?.effects.push((effect = {} as any))
|
||||||
|
getter()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (immediate) {
|
||||||
|
// onEffectCleanup need use effect as a key
|
||||||
|
getCurrentScope()?.effects.push((effect = {} as any))
|
||||||
|
callWithAsyncErrorHandling(
|
||||||
|
cb,
|
||||||
|
onError,
|
||||||
|
BaseWatchErrorCodes.WATCH_CALLBACK,
|
||||||
|
[getter(), isMultiSource ? [] : undefined, onEffectCleanup],
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const _cb = cb
|
||||||
|
cb = (...args) => {
|
||||||
|
_cb(...args)
|
||||||
|
effect?.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let oldValue: any = isMultiSource
|
||||||
|
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
|
||||||
|
: INITIAL_WATCHER_VALUE
|
||||||
|
const job: SchedulerJob = () => {
|
||||||
|
if (!effect.active || !effect.dirty) {
|
||||||
|
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 currentEffect = activeEffect
|
||||||
|
activeEffect = effect
|
||||||
|
try {
|
||||||
|
callWithAsyncErrorHandling(
|
||||||
|
cb,
|
||||||
|
onError,
|
||||||
|
BaseWatchErrorCodes.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,
|
||||||
|
onEffectCleanup,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
oldValue = newValue
|
||||||
|
} finally {
|
||||||
|
activeEffect = currentEffect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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 effectScheduler: EffectScheduler = () => scheduler(job, effect, false)
|
||||||
|
|
||||||
|
effect = new ReactiveEffect(getter, NOOP, effectScheduler)
|
||||||
|
|
||||||
|
cleanup = effect.onStop = () => {
|
||||||
|
const cleanups = cleanupMap.get(effect)
|
||||||
|
if (cleanups) {
|
||||||
|
cleanups.forEach(cleanup =>
|
||||||
|
callWithErrorHandling(
|
||||||
|
cleanup,
|
||||||
|
onError,
|
||||||
|
BaseWatchErrorCodes.WATCH_CLEANUP,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
cleanupMap.delete(effect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
effect.onTrack = onTrack
|
||||||
|
effect.onTrigger = onTrigger
|
||||||
|
}
|
||||||
|
|
||||||
|
// initial run
|
||||||
|
if (cb) {
|
||||||
|
if (immediate) {
|
||||||
|
job()
|
||||||
|
} else {
|
||||||
|
oldValue = effect.run()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scheduler(job, effect, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return effect
|
||||||
|
}
|
||||||
|
|
||||||
|
export function traverse(
|
||||||
|
value: unknown,
|
||||||
|
depth?: number,
|
||||||
|
currentDepth = 0,
|
||||||
|
seen?: Set<unknown>,
|
||||||
|
) {
|
||||||
|
if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth && depth > 0) {
|
||||||
|
if (currentDepth >= depth) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
currentDepth++
|
||||||
|
}
|
||||||
|
|
||||||
|
seen = seen || new Set()
|
||||||
|
if (seen.has(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
seen.add(value)
|
||||||
|
if (isRef(value)) {
|
||||||
|
traverse(value.value, depth, currentDepth, seen)
|
||||||
|
} else if (isArray(value)) {
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
traverse(value[i], depth, currentDepth, seen)
|
||||||
|
}
|
||||||
|
} else if (isSet(value) || isMap(value)) {
|
||||||
|
value.forEach((v: any) => {
|
||||||
|
traverse(v, depth, currentDepth, seen)
|
||||||
|
})
|
||||||
|
} else if (isPlainObject(value)) {
|
||||||
|
for (const key in value) {
|
||||||
|
traverse(value[key], depth, currentDepth, seen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function callWithErrorHandling(
|
||||||
|
fn: Function,
|
||||||
|
handleError: HandleError,
|
||||||
|
type: BaseWatchErrorCodes,
|
||||||
|
args?: unknown[],
|
||||||
|
) {
|
||||||
|
let res
|
||||||
|
try {
|
||||||
|
res = args ? fn(...args) : fn()
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, type)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
function callWithAsyncErrorHandling(
|
||||||
|
fn: Function | Function[],
|
||||||
|
handleError: HandleError,
|
||||||
|
type: BaseWatchErrorCodes,
|
||||||
|
args?: unknown[],
|
||||||
|
): any[] {
|
||||||
|
if (isFunction(fn)) {
|
||||||
|
const res = callWithErrorHandling(fn, handleError, type, args)
|
||||||
|
if (res && isPromise(res)) {
|
||||||
|
res.catch(err => {
|
||||||
|
handleError(err, type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = []
|
||||||
|
for (let i = 0; i < fn.length; i++) {
|
||||||
|
values.push(callWithAsyncErrorHandling(fn[i], handleError, type, args))
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
|
@ -69,3 +69,11 @@ export {
|
||||||
onScopeDispose,
|
onScopeDispose,
|
||||||
} from './effectScope'
|
} from './effectScope'
|
||||||
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
|
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
|
||||||
|
export {
|
||||||
|
baseWatch,
|
||||||
|
onEffectCleanup,
|
||||||
|
traverse,
|
||||||
|
BaseWatchErrorCodes,
|
||||||
|
type BaseWatchOptions,
|
||||||
|
type Scheduler,
|
||||||
|
} from './baseWatch'
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
defineComponent,
|
defineComponent,
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
onEffectCleanup,
|
||||||
reactive,
|
reactive,
|
||||||
ref,
|
ref,
|
||||||
watch,
|
watch,
|
||||||
|
@ -393,6 +394,35 @@ describe('api: watch', () => {
|
||||||
expect(cleanup).toHaveBeenCalledTimes(2)
|
expect(cleanup).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('onEffectCleanup', async () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const cleanupEffect = vi.fn()
|
||||||
|
const cleanupWatch = vi.fn()
|
||||||
|
|
||||||
|
const stopEffect = watchEffect(() => {
|
||||||
|
onEffectCleanup(cleanupEffect)
|
||||||
|
count.value
|
||||||
|
})
|
||||||
|
const stopWatch = watch(count, () => {
|
||||||
|
onEffectCleanup(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,27 +1,22 @@
|
||||||
import {
|
import {
|
||||||
|
type BaseWatchErrorCodes,
|
||||||
|
type BaseWatchOptions,
|
||||||
type ComputedRef,
|
type ComputedRef,
|
||||||
type DebuggerOptions,
|
type DebuggerOptions,
|
||||||
type EffectScheduler,
|
|
||||||
ReactiveEffect,
|
|
||||||
ReactiveFlags,
|
|
||||||
type Ref,
|
type Ref,
|
||||||
|
baseWatch,
|
||||||
getCurrentScope,
|
getCurrentScope,
|
||||||
isReactive,
|
|
||||||
isRef,
|
|
||||||
isShallow,
|
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import { type SchedulerJob, queueJob } from './scheduler'
|
import {
|
||||||
|
type SchedulerFactory,
|
||||||
|
createPreScheduler,
|
||||||
|
createSyncScheduler,
|
||||||
|
} from './scheduler'
|
||||||
import {
|
import {
|
||||||
EMPTY_OBJ,
|
EMPTY_OBJ,
|
||||||
NOOP,
|
NOOP,
|
||||||
extend,
|
extend,
|
||||||
hasChanged,
|
|
||||||
isArray,
|
|
||||||
isFunction,
|
isFunction,
|
||||||
isMap,
|
|
||||||
isObject,
|
|
||||||
isPlainObject,
|
|
||||||
isSet,
|
|
||||||
isString,
|
isString,
|
||||||
remove,
|
remove,
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
|
@ -32,15 +27,9 @@ import {
|
||||||
setCurrentInstance,
|
setCurrentInstance,
|
||||||
unsetCurrentInstance,
|
unsetCurrentInstance,
|
||||||
} from './component'
|
} from './component'
|
||||||
import {
|
import { handleError as handleErrorWithInstance } from './errorHandling'
|
||||||
ErrorCodes,
|
import { createPostRenderScheduler } from './renderer'
|
||||||
callWithAsyncErrorHandling,
|
|
||||||
callWithErrorHandling,
|
|
||||||
} from './errorHandling'
|
|
||||||
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'
|
||||||
|
|
||||||
|
@ -110,9 +99,6 @@ export function watchSyncEffect(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initial value for watchers to trigger on undefined initial values
|
|
||||||
const INITIAL_WATCHER_VALUE = {}
|
|
||||||
|
|
||||||
type MultiWatchSources = (WatchSource<unknown> | object)[]
|
type MultiWatchSources = (WatchSource<unknown> | object)[]
|
||||||
|
|
||||||
// overload: single source + cb
|
// overload: single source + cb
|
||||||
|
@ -170,25 +156,23 @@ export function watch<T = any, Immediate extends Readonly<boolean> = false>(
|
||||||
return doWatch(source as any, cb, options)
|
return doWatch(source as any, cb, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScheduler(flush: WatchOptionsBase['flush']): SchedulerFactory {
|
||||||
|
if (flush === 'post') {
|
||||||
|
return createPostRenderScheduler
|
||||||
|
}
|
||||||
|
if (flush === 'sync') {
|
||||||
|
return createSyncScheduler
|
||||||
|
}
|
||||||
|
// default: 'pre'
|
||||||
|
return createPreScheduler
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
): WatchStopHandle {
|
): WatchStopHandle {
|
||||||
if (cb && once) {
|
const { immediate, deep, flush, once } = options
|
||||||
const _cb = cb
|
|
||||||
cb = (...args) => {
|
|
||||||
_cb(...args)
|
|
||||||
unwatch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO remove in 3.5
|
// TODO remove in 3.5
|
||||||
if (__DEV__ && deep !== void 0 && typeof deep === 'number') {
|
if (__DEV__ && deep !== void 0 && typeof deep === 'number') {
|
||||||
|
@ -219,211 +203,41 @@ function doWatch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const warnInvalidSource = (s: unknown) => {
|
const extendOptions: BaseWatchOptions = {}
|
||||||
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__) extendOptions.onWarn = warn
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
getter = () => traverse(baseGetter())
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
extendOptions.once = true
|
||||||
} else {
|
} else {
|
||||||
|
// watch(source, cb)
|
||||||
return NOOP
|
return NOOP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let oldValue: any = isMultiSource
|
const instance = currentInstance
|
||||||
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
|
extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) =>
|
||||||
: INITIAL_WATCHER_VALUE
|
handleErrorWithInstance(err, instance, type)
|
||||||
const job: SchedulerJob = () => {
|
extendOptions.scheduler = getScheduler(flush)(instance)
|
||||||
if (!effect.active || !effect.dirty) {
|
|
||||||
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)) ||
|
|
||||||
(__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 {
|
|
||||||
// watchEffect
|
|
||||||
effect.run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// important: mark the job as a watcher callback so that scheduler knows
|
let effect = baseWatch(source, cb, extend({}, options, extendOptions))
|
||||||
// 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, NOOP, scheduler)
|
|
||||||
|
|
||||||
const scope = getCurrentScope()
|
const scope = getCurrentScope()
|
||||||
const unwatch = () => {
|
const unwatch = !effect
|
||||||
effect.stop()
|
? NOOP
|
||||||
|
: () => {
|
||||||
|
effect!.stop()
|
||||||
if (scope) {
|
if (scope) {
|
||||||
remove(scope.effects, effect)
|
remove(scope.effects, effect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
|
if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
|
||||||
return unwatch
|
return unwatch
|
||||||
}
|
}
|
||||||
|
@ -469,43 +283,3 @@ export function createPathGetter(ctx: any, path: string) {
|
||||||
return cur
|
return cur
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function traverse(
|
|
||||||
value: unknown,
|
|
||||||
depth?: number,
|
|
||||||
currentDepth = 0,
|
|
||||||
seen?: Set<unknown>,
|
|
||||||
) {
|
|
||||||
if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (depth && depth > 0) {
|
|
||||||
if (currentDepth >= depth) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
currentDepth++
|
|
||||||
}
|
|
||||||
|
|
||||||
seen = seen || new Set()
|
|
||||||
if (seen.has(value)) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
seen.add(value)
|
|
||||||
if (isRef(value)) {
|
|
||||||
traverse(value.value, depth, currentDepth, seen)
|
|
||||||
} else if (isArray(value)) {
|
|
||||||
for (let i = 0; i < value.length; i++) {
|
|
||||||
traverse(value[i], depth, currentDepth, seen)
|
|
||||||
}
|
|
||||||
} else if (isSet(value) || isMap(value)) {
|
|
||||||
value.forEach((v: any) => {
|
|
||||||
traverse(v, depth, currentDepth, seen)
|
|
||||||
})
|
|
||||||
} else if (isPlainObject(value)) {
|
|
||||||
for (const key in value) {
|
|
||||||
traverse(value[key], depth, currentDepth, seen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import type {
|
import {
|
||||||
Component,
|
type Component,
|
||||||
ComponentInternalInstance,
|
type ComponentInternalInstance,
|
||||||
ComponentInternalOptions,
|
type ComponentInternalOptions,
|
||||||
ConcreteComponent,
|
type ConcreteComponent,
|
||||||
InternalRenderFunction,
|
type InternalRenderFunction,
|
||||||
SetupContext,
|
type SetupContext,
|
||||||
|
currentInstance,
|
||||||
} from './component'
|
} from './component'
|
||||||
import {
|
import {
|
||||||
type Data,
|
type Data,
|
||||||
|
@ -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,
|
||||||
|
@ -67,7 +68,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,
|
||||||
|
@ -937,18 +938,45 @@ export function createWatcher(
|
||||||
publicThis: ComponentPublicInstance,
|
publicThis: ComponentPublicInstance,
|
||||||
key: string,
|
key: string,
|
||||||
) {
|
) {
|
||||||
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 =
|
||||||
|
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)) {
|
||||||
watch(getter, handler as WatchCallback)
|
watch(getter, handler as WatchCallback, options)
|
||||||
} 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)) {
|
||||||
watch(getter, raw.bind(publicThis))
|
watch(getter, raw.bind(publicThis), options)
|
||||||
} 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))
|
||||||
|
@ -957,7 +985,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, extend(raw, options))
|
||||||
} 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,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<V = any> {
|
export interface DirectiveBinding<V = any> {
|
||||||
instance: ComponentPublicInstance | null
|
instance: ComponentPublicInstance | null
|
||||||
|
|
|
@ -3,16 +3,20 @@ import type { ComponentInternalInstance } from './component'
|
||||||
import { popWarningContext, pushWarningContext, warn } from './warning'
|
import { popWarningContext, pushWarningContext, warn } from './warning'
|
||||||
import { isFunction, isPromise } from '@vue/shared'
|
import { isFunction, isPromise } from '@vue/shared'
|
||||||
import { LifecycleHooks } from './enums'
|
import { LifecycleHooks } from './enums'
|
||||||
|
import { BaseWatchErrorCodes } 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,
|
||||||
|
@ -24,7 +28,9 @@ export enum ErrorCodes {
|
||||||
SCHEDULER,
|
SCHEDULER,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ErrorTypeStrings: Record<LifecycleHooks | ErrorCodes, string> = {
|
export type ErrorTypes = LifecycleHooks | ErrorCodes | BaseWatchErrorCodes
|
||||||
|
|
||||||
|
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',
|
||||||
|
@ -41,9 +47,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',
|
[BaseWatchErrorCodes.WATCH_GETTER]: 'watcher getter',
|
||||||
[ErrorCodes.WATCH_CALLBACK]: 'watcher callback',
|
[BaseWatchErrorCodes.WATCH_CALLBACK]: 'watcher callback',
|
||||||
[ErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function',
|
[BaseWatchErrorCodes.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',
|
||||||
|
@ -58,8 +64,6 @@ export const ErrorTypeStrings: Record<LifecycleHooks | ErrorCodes, string> = {
|
||||||
'Please open an issue at https://github.com/vuejs/core .',
|
'Please open an issue at https://github.com/vuejs/core .',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ErrorTypes = LifecycleHooks | ErrorCodes
|
|
||||||
|
|
||||||
export function callWithErrorHandling(
|
export function callWithErrorHandling(
|
||||||
fn: Function,
|
fn: Function,
|
||||||
instance: ComponentInternalInstance | null,
|
instance: ComponentInternalInstance | null,
|
||||||
|
|
|
@ -28,6 +28,7 @@ export {
|
||||||
// effect
|
// effect
|
||||||
effect,
|
effect,
|
||||||
stop,
|
stop,
|
||||||
|
onEffectCleanup,
|
||||||
ReactiveEffect,
|
ReactiveEffect,
|
||||||
// effect scope
|
// effect scope
|
||||||
effectScope,
|
effectScope,
|
||||||
|
|
|
@ -38,6 +38,7 @@ import {
|
||||||
isReservedProp,
|
isReservedProp,
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
|
type SchedulerFactory,
|
||||||
type SchedulerJob,
|
type SchedulerJob,
|
||||||
flushPostFlushCbs,
|
flushPostFlushCbs,
|
||||||
flushPreFlushCbs,
|
flushPreFlushCbs,
|
||||||
|
@ -281,6 +282,18 @@ export const queuePostRenderEffect = __FEATURE_SUSPENSE__
|
||||||
: queueEffectWithSuspense
|
: queueEffectWithSuspense
|
||||||
: queuePostFlushCb
|
: queuePostFlushCb
|
||||||
|
|
||||||
|
export const createPostRenderScheduler: SchedulerFactory =
|
||||||
|
instance => (job, effect, isInit) => {
|
||||||
|
if (isInit) {
|
||||||
|
queuePostRenderEffect(
|
||||||
|
effect.run.bind(effect),
|
||||||
|
instance && instance.suspense,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
queuePostRenderEffect(job, instance && instance.suspense)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The createRenderer function accepts two generic arguments:
|
* The createRenderer function accepts two generic arguments:
|
||||||
* HostNode and HostElement, corresponding to Node and Element types in the
|
* HostNode and HostElement, corresponding to Node and Element types in the
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
|
import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
|
||||||
import { type Awaited, NOOP, isArray } from '@vue/shared'
|
import { type Awaited, NOOP, isArray } from '@vue/shared'
|
||||||
import { type ComponentInternalInstance, getComponentName } from './component'
|
import { type ComponentInternalInstance, getComponentName } from './component'
|
||||||
|
import type { Scheduler } from '@vue/reactivity'
|
||||||
|
|
||||||
export interface SchedulerJob extends Function {
|
export interface SchedulerJob extends Function {
|
||||||
id?: number
|
id?: number
|
||||||
|
@ -287,3 +288,27 @@ function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SchedulerFactory = (
|
||||||
|
instance: ComponentInternalInstance | null,
|
||||||
|
) => Scheduler
|
||||||
|
|
||||||
|
export const createSyncScheduler: SchedulerFactory =
|
||||||
|
instance => (job, effect, isInit) => {
|
||||||
|
if (isInit) {
|
||||||
|
effect.run()
|
||||||
|
} else {
|
||||||
|
job()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPreScheduler: SchedulerFactory =
|
||||||
|
instance => (job, effect, isInit) => {
|
||||||
|
if (isInit) {
|
||||||
|
effect.run()
|
||||||
|
} else {
|
||||||
|
job.pre = true
|
||||||
|
if (instance) job.id = instance.uid
|
||||||
|
queueJob(job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue