--- /dev/null
+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'])
+ })
+})
--- /dev/null
+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
+}
onScopeDispose,
} from './effectScope'
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
+export {
+ baseWatch,
+ onEffectCleanup,
+ traverse,
+ BaseWatchErrorCodes,
+ type BaseWatchOptions,
+ type Scheduler,
+} from './baseWatch'
defineComponent,
getCurrentInstance,
nextTick,
+ onEffectCleanup,
reactive,
ref,
watch,
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 () => {
const count = ref(0)
const count2 = ref(0)
import {
+ type BaseWatchErrorCodes,
+ type BaseWatchOptions,
type ComputedRef,
type DebuggerOptions,
- type EffectScheduler,
- ReactiveEffect,
- ReactiveFlags,
type Ref,
+ baseWatch,
getCurrentScope,
- isReactive,
- isRef,
- isShallow,
} from '@vue/reactivity'
-import { type SchedulerJob, queueJob } from './scheduler'
+import {
+ type SchedulerFactory,
+ createPreScheduler,
+ createSyncScheduler,
+} from './scheduler'
import {
EMPTY_OBJ,
NOOP,
extend,
- hasChanged,
- isArray,
isFunction,
- isMap,
- isObject,
- isPlainObject,
- isSet,
isString,
remove,
} from '@vue/shared'
setCurrentInstance,
unsetCurrentInstance,
} from './component'
-import {
- ErrorCodes,
- callWithAsyncErrorHandling,
- callWithErrorHandling,
-} from './errorHandling'
-import { queuePostRenderEffect } from './renderer'
+import { handleError as handleErrorWithInstance } from './errorHandling'
+import { createPostRenderScheduler } 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'
)
}
-// initial value for watchers to trigger on undefined initial values
-const INITIAL_WATCHER_VALUE = {}
-
type MultiWatchSources = (WatchSource<unknown> | object)[]
// overload: single source + cb
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(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
- {
- immediate,
- deep,
- flush,
- once,
- onTrack,
- onTrigger,
- }: WatchOptions = EMPTY_OBJ,
+ options: WatchOptions = EMPTY_OBJ,
): WatchStopHandle {
- if (cb && once) {
- const _cb = cb
- cb = (...args) => {
- _cb(...args)
- unwatch()
- }
- }
+ const { immediate, deep, flush, once } = options
// TODO remove in 3.5
if (__DEV__ && deep !== void 0 && typeof deep === 'number') {
}
}
- 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) =>
- 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())
- }
+ const extendOptions: BaseWatchOptions = {}
- let cleanup: (() => void) | undefined
- let onCleanup: OnCleanup = (fn: () => void) => {
- cleanup = effect.onStop = () => {
- callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
- cleanup = effect.onStop = undefined
- }
- }
+ if (__DEV__) extendOptions.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
+ extendOptions.once = true
} else {
- return NOOP
- }
- }
-
- 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)) ||
- (__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()
+ return NOOP
}
}
- // important: mark the job as a watcher callback so that scheduler knows
- // it is allowed to self-trigger (#1727)
- job.allowRecurse = !!cb
-
- let scheduler: EffectScheduler
- if (flush === 'sync') {
- scheduler = job as any // the scheduler function gets called directly
- } else if (flush === 'post') {
- scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
- } else {
- // default: 'pre'
- job.pre = true
- if (instance) job.id = instance.uid
- scheduler = () => queueJob(job)
- }
+ const instance = currentInstance
+ extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) =>
+ handleErrorWithInstance(err, instance, type)
+ extendOptions.scheduler = getScheduler(flush)(instance)
- const effect = new ReactiveEffect(getter, NOOP, scheduler)
+ let effect = baseWatch(source, cb, extend({}, options, extendOptions))
const scope = getCurrentScope()
- const unwatch = () => {
- effect.stop()
- if (scope) {
- 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()
- }
+ const unwatch = !effect
+ ? NOOP
+ : () => {
+ effect!.stop()
+ if (scope) {
+ remove(scope.effects, effect)
+ }
+ }
if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
return unwatch
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
-}
-import type {
- Component,
- ComponentInternalInstance,
- ComponentInternalOptions,
- ConcreteComponent,
- InternalRenderFunction,
- SetupContext,
+import {
+ type Component,
+ type ComponentInternalInstance,
+ type ComponentInternalOptions,
+ type ConcreteComponent,
+ type InternalRenderFunction,
+ type SetupContext,
+ currentInstance,
} from './component'
import {
type Data,
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,
) {
- const getter = key.includes('.')
+ let getter = key.includes('.')
? createPathGetter(publicThis, 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)) {
const handler = ctx[raw]
if (isFunction(handler)) {
- watch(getter, handler as WatchCallback)
+ watch(getter, handler as WatchCallback, options)
} else if (__DEV__) {
warn(`Invalid watch handler specified by key "${raw}"`, handler)
}
} else if (isFunction(raw)) {
- watch(getter, raw.bind(publicThis))
+ watch(getter, raw.bind(publicThis), options)
} 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, extend(raw, options))
} 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<V = any> {
instance: ComponentPublicInstance | null
import { popWarningContext, pushWarningContext, warn } from './warning'
import { isFunction, isPromise } from '@vue/shared'
import { LifecycleHooks } from './enums'
+import { BaseWatchErrorCodes } 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,
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.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',
+ [BaseWatchErrorCodes.WATCH_GETTER]: 'watcher getter',
+ [BaseWatchErrorCodes.WATCH_CALLBACK]: 'watcher callback',
+ [BaseWatchErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function',
[ErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler',
[ErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler',
[ErrorCodes.VNODE_HOOK]: 'vnode hook',
'Please open an issue at https://github.com/vuejs/core .',
}
-export type ErrorTypes = LifecycleHooks | ErrorCodes
-
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
// effect
effect,
stop,
+ onEffectCleanup,
ReactiveEffect,
// effect scope
effectScope,
isReservedProp,
} from '@vue/shared'
import {
+ type SchedulerFactory,
type SchedulerJob,
flushPostFlushCbs,
flushPreFlushCbs,
: queueEffectWithSuspense
: 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:
* HostNode and HostElement, corresponding to Node and Element types in the
import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
import { type Awaited, NOOP, isArray } from '@vue/shared'
import { type ComponentInternalInstance, getComponentName } from './component'
+import type { Scheduler } from '@vue/reactivity'
export interface SchedulerJob extends Function {
id?: number
}
}
}
+
+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)
+ }
+ }