--- /dev/null
+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'])
+ })
+})
} from './effectScope'
export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations'
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
+export {
+ watch,
+ getCurrentWatcher,
+ traverse,
+ onWatcherCleanup,
+ WatchErrorCodes,
+ type WatchOptions,
+ type WatchScheduler,
+ type WatchStopHandle,
+ type WatchHandle,
+} from './watch'
--- /dev/null
+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
+}
getCurrentInstance,
nextTick,
onErrorCaptured,
+ onWatcherCleanup,
reactive,
ref,
watch,
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 () => {
const count = ref(0)
const count2 = ref(0)
import {
+ type WatchOptions as BaseWatchOptions,
type ComputedRef,
type DebuggerOptions,
- EffectFlags,
- type EffectScheduler,
- ReactiveEffect,
- ReactiveFlags,
type ReactiveMarker,
type Ref,
- getCurrentScope,
- isReactive,
- isRef,
- isShallow,
+ type WatchHandle,
+ watch as baseWatch,
} from '@vue/reactivity'
import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler'
-import {
- EMPTY_OBJ,
- NOOP,
- extend,
- hasChanged,
- isArray,
- isFunction,
- isMap,
- isObject,
- isPlainObject,
- isSet,
- isString,
- remove,
-} from '@vue/shared'
+import { EMPTY_OBJ, NOOP, extend, isFunction, isString } from '@vue/shared'
import {
type ComponentInternalInstance,
currentInstance,
isInSSRComponentSetup,
setCurrentInstance,
} from './component'
-import {
- ErrorCodes,
- callWithAsyncErrorHandling,
- callWithErrorHandling,
-} from './errorHandling'
+import { callWithAsyncErrorHandling } from './errorHandling'
import { queuePostRenderEffect } from './renderer'
import { warn } from './warning'
-import { DeprecationTypes } from './compat/compatConfig'
-import { checkCompatEnabled, isCompatEnabled } from './compat/compatConfig'
import type { ObjectWatchOptionItem } from './componentOptions'
import { useSSRContext } from './helpers/useSsrContext'
+export type { WatchHandle, WatchStopHandle } from '@vue/reactivity'
+
export type WatchEffect = (onCleanup: OnCleanup) => void
export type WatchSource<T = any> = Ref<T, any> | ComputedRef<T> | (() => T)
once?: boolean
}
-export type WatchStopHandle = () => void
-
-export interface WatchHandle extends WatchStopHandle {
- pause: () => void
- resume: () => void
- stop: () => void
-}
-
// Simple effect.
export function watchEffect(
effect: WatchEffect,
export function watchPostEffect(
effect: WatchEffect,
options?: DebuggerOptions,
-): WatchStopHandle {
+): WatchHandle {
return doWatch(
effect,
null,
export function watchSyncEffect(
effect: WatchEffect,
options?: DebuggerOptions,
-): WatchStopHandle {
+): WatchHandle {
return doWatch(
effect,
null,
)
}
-// initial value for watchers to trigger on undefined initial values
-const INITIAL_WATCHER_VALUE = {}
-
export type MultiWatchSources = (WatchSource<unknown> | object)[]
// overload: single source + cb
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
- {
- immediate,
- deep,
- flush,
- once,
- onTrack,
- onTrigger,
- }: WatchOptions = EMPTY_OBJ,
+ options: WatchOptions = EMPTY_OBJ,
): WatchHandle {
- if (cb && once) {
- const _cb = cb
- cb = (...args) => {
- _cb(...args)
- watchHandle()
- }
- }
+ const { immediate, deep, flush, once } = options
if (__DEV__ && !cb) {
if (immediate !== undefined) {
}
}
- const warnInvalidSource = (s: unknown) => {
- warn(
- `Invalid watch source: `,
- s,
- `A watch source can only be a getter/effect function, a ref, ` +
- `a reactive object, or an array of these types.`,
- )
- }
-
- const instance = currentInstance
- 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)
- }
+ const baseWatchOptions: BaseWatchOptions = extend({}, options)
- let cleanup: (() => void) | undefined
- let onCleanup: OnCleanup = (fn: () => void) => {
- cleanup = effect.onStop = () => {
- callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
- cleanup = effect.onStop = undefined
- }
- }
+ if (__DEV__) baseWatchOptions.onWarn = warn
- // 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
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') {
const ctx = useSSRContext()!
ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
+ } else if (!cb || immediate) {
+ // immediately watch or watchEffect
+ baseWatchOptions.once = true
} else {
- const watchHandle: WatchHandle = () => {}
- watchHandle.stop = NOOP
- watchHandle.resume = NOOP
- watchHandle.pause = NOOP
- return watchHandle
+ return {
+ stop: NOOP,
+ resume: NOOP,
+ pause: NOOP,
+ } as WatchHandle
}
}
- let oldValue: any = isMultiSource
- ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
- : INITIAL_WATCHER_VALUE
- const job: SchedulerJob = (immediateFirstRun?: boolean) => {
- if (
- !(effect.flags & EffectFlags.ACTIVE) ||
- (!effect.dirty && !immediateFirstRun)
- ) {
- return
+ const instance = currentInstance
+ baseWatchOptions.call = (fn, type, args) =>
+ callWithAsyncErrorHandling(fn, instance, type, args)
+
+ // scheduler
+ let isPre = false
+ if (flush === 'post') {
+ baseWatchOptions.scheduler = job => {
+ 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 {
+ queueJob(job)
}
- } else {
- // watchEffect
- effect.run()
}
}
- // important: mark the job as a watcher callback so that scheduler knows
- // it is allowed to self-trigger (#1727)
- if (cb) job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
-
- const effect = new ReactiveEffect(getter)
-
- 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
- if (instance) {
- job.id = instance.uid
- job.i = instance
+ baseWatchOptions.augmentJob = (job: SchedulerJob) => {
+ // important: mark the job as a watcher callback so that scheduler knows
+ // it is allowed to self-trigger (#1727)
+ if (cb) {
+ job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
}
- scheduler = () => queueJob(job)
- }
- effect.scheduler = scheduler
-
- const scope = getCurrentScope()
- const watchHandle: WatchHandle = () => {
- effect.stop()
- if (scope) {
- remove(scope.effects, effect)
+ if (isPre) {
+ job.flags! |= SchedulerJobFlags.PRE
+ if (instance) {
+ job.id = instance.uid
+ ;(job as SchedulerJob).i = instance
+ }
}
}
- watchHandle.pause = effect.pause.bind(effect)
- 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()
- }
+ const watchHandle = baseWatch(source, cb, baseWatchOptions)
if (__SSR__ && ssrCleanup) ssrCleanup.push(watchHandle)
return watchHandle
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
-}
-import type {
- Component,
- ComponentInternalInstance,
- ComponentInternalOptions,
- ConcreteComponent,
- Data,
- InternalRenderFunction,
- SetupContext,
+import {
+ type Component,
+ type ComponentInternalInstance,
+ type ComponentInternalOptions,
+ type ConcreteComponent,
+ type Data,
+ type InternalRenderFunction,
+ type SetupContext,
+ currentInstance,
} from './component'
import {
type LooseRequired,
isPromise,
isString,
} from '@vue/shared'
-import { type Ref, isRef } from '@vue/reactivity'
+import { type Ref, getCurrentScope, isRef, traverse } from '@vue/reactivity'
import { computed } from './apiComputed'
import {
type WatchCallback,
import type { VNodeChild } from './vnode'
import { callWithAsyncErrorHandling } from './errorHandling'
import { deepMergeData } from './compat/data'
-import { DeprecationTypes } from './compat/compatConfig'
+import { DeprecationTypes, checkCompatEnabled } from './compat/compatConfig'
import {
type CompatConfig,
isCompatEnabled,
publicThis: ComponentPublicInstance,
key: string,
): void {
- const getter = key.includes('.')
+ let getter = key.includes('.')
? createPathGetter(publicThis, 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)) {
const handler = ctx[raw]
if (isFunction(handler)) {
- watch(getter, handler as WatchCallback)
+ if (__COMPAT__) {
+ watch(getter, handler as WatchCallback, options)
+ } else {
+ watch(getter, handler as WatchCallback)
+ }
} else if (__DEV__) {
warn(`Invalid watch handler specified by key "${raw}"`, handler)
}
} else if (isFunction(raw)) {
- watch(getter, raw.bind(publicThis))
+ if (__COMPAT__) {
+ watch(getter, raw.bind(publicThis), options)
+ } else {
+ watch(getter, raw.bind(publicThis))
+ }
} else if (isObject(raw)) {
if (isArray(raw)) {
raw.forEach(r => createWatcher(r, ctx, publicThis, key))
? raw.handler.bind(publicThis)
: (ctx[raw.handler] as WatchCallback)
if (isFunction(handler)) {
- watch(getter, handler, raw)
+ watch(getter, handler, __COMPAT__ ? extend(raw, options) : raw)
} else if (__DEV__) {
warn(`Invalid watch handler specified by key "${raw.handler}"`, handler)
}
import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
import type { ComponentPublicInstance } from './componentPublicInstance'
import { mapCompatDirectiveHook } from './compat/customDirective'
-import { pauseTracking, resetTracking } from '@vue/reactivity'
-import { traverse } from './apiWatch'
+import { pauseTracking, resetTracking, traverse } from '@vue/reactivity'
export interface DirectiveBinding<
Value = any,
import { popWarningContext, pushWarningContext, warn } from './warning'
import { EMPTY_OBJ, isArray, isFunction, isPromise } from '@vue/shared'
import { LifecycleHooks } from './enums'
+import { WatchErrorCodes } from '@vue/reactivity'
// contexts where user provided function may be executed, in addition to
// lifecycle hooks.
export enum ErrorCodes {
SETUP_FUNCTION,
RENDER_FUNCTION,
- WATCH_GETTER,
- WATCH_CALLBACK,
- WATCH_CLEANUP,
- NATIVE_EVENT_HANDLER,
+ // The error codes for the watch have been transferred to the reactivity
+ // package along with baseWatch to maintain code compatibility. Hence,
+ // it is essential to keep these values unchanged.
+ // WATCH_GETTER,
+ // WATCH_CALLBACK,
+ // WATCH_CLEANUP,
+ NATIVE_EVENT_HANDLER = 5,
COMPONENT_EVENT_HANDLER,
VNODE_HOOK,
DIRECTIVE_HOOK,
APP_UNMOUNT_CLEANUP,
}
-export const ErrorTypeStrings: Record<LifecycleHooks | ErrorCodes, string> = {
+export const ErrorTypeStrings: Record<ErrorTypes, string> = {
[LifecycleHooks.SERVER_PREFETCH]: 'serverPrefetch hook',
[LifecycleHooks.BEFORE_CREATE]: 'beforeCreate hook',
[LifecycleHooks.CREATED]: 'created hook',
[LifecycleHooks.RENDER_TRIGGERED]: 'renderTriggered hook',
[ErrorCodes.SETUP_FUNCTION]: 'setup function',
[ErrorCodes.RENDER_FUNCTION]: 'render function',
- [ErrorCodes.WATCH_GETTER]: 'watcher getter',
- [ErrorCodes.WATCH_CALLBACK]: 'watcher callback',
- [ErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function',
+ [WatchErrorCodes.WATCH_GETTER]: 'watcher getter',
+ [WatchErrorCodes.WATCH_CALLBACK]: 'watcher callback',
+ [WatchErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function',
[ErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler',
[ErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler',
[ErrorCodes.VNODE_HOOK]: 'vnode hook',
[ErrorCodes.APP_UNMOUNT_CLEANUP]: 'app unmount cleanup function',
}
-export type ErrorTypes = LifecycleHooks | ErrorCodes
+export type ErrorTypes = LifecycleHooks | ErrorCodes | WatchErrorCodes
export function callWithErrorHandling(
fn: Function,
// effect
effect,
stop,
+ getCurrentWatcher,
+ onWatcherCleanup,
ReactiveEffect,
// effect scope
effectScope,