import { effect, stop, isRef, Ref, ComputedRef, ReactiveEffectOptions, isReactive } from '@vue/reactivity' import { SchedulerJob, queuePreFlushCb } from './scheduler' import { EMPTY_OBJ, isObject, isArray, isFunction, isString, hasChanged, NOOP, remove, isMap, isSet, isPlainObject } from '@vue/shared' import { currentInstance, ComponentInternalInstance, isInSSRComponentSetup, recordInstanceBoundEffect } from './component' import { ErrorCodes, callWithErrorHandling, callWithAsyncErrorHandling } from './errorHandling' import { queuePostRenderEffect } from './renderer' import { warn } from './warning' import { DeprecationTypes } from './compat/compatConfig' import { checkCompatEnabled, isCompatEnabled } from './compat/compatConfig' import { ObjectWatchOptionItem } from './componentOptions' export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void export type WatchSource = Ref | ComputedRef | (() => T) export type WatchCallback = ( value: V, oldValue: OV, onInvalidate: InvalidateCbRegistrator ) => any type MapSources = { [K in keyof T]: T[K] extends WatchSource ? Immediate extends true ? (V | undefined) : V : T[K] extends object ? Immediate extends true ? (T[K] | undefined) : T[K] : never } type InvalidateCbRegistrator = (cb: () => void) => void export interface WatchOptionsBase { flush?: 'pre' | 'post' | 'sync' onTrack?: ReactiveEffectOptions['onTrack'] onTrigger?: ReactiveEffectOptions['onTrigger'] } export interface WatchOptions extends WatchOptionsBase { immediate?: Immediate deep?: boolean } export type WatchStopHandle = () => void // Simple effect. export function watchEffect( effect: WatchEffect, options?: WatchOptionsBase ): WatchStopHandle { return doWatch(effect, null, options) } // initial value for watchers to trigger on undefined initial values const INITIAL_WATCHER_VALUE = {} type MultiWatchSources = (WatchSource | object)[] // overload: array of multiple sources + cb export function watch< T extends MultiWatchSources, Immediate extends Readonly = false >( sources: [...T], cb: WatchCallback, MapSources>, options?: WatchOptions ): WatchStopHandle // overload: multiple sources w/ `as const` // watch([foo, bar] as const, () => {}) // somehow [...T] breaks when the type is readonly export function watch< T extends Readonly, Immediate extends Readonly = false >( source: T, cb: WatchCallback, MapSources>, options?: WatchOptions ): WatchStopHandle // overload: single source + cb export function watch = false>( source: WatchSource, cb: WatchCallback, options?: WatchOptions ): WatchStopHandle // overload: watching reactive object w/ cb export function watch< T extends object, Immediate extends Readonly = false >( source: T, cb: WatchCallback, options?: WatchOptions ): WatchStopHandle // implementation export function watch = false>( source: T | WatchSource, cb: any, options?: WatchOptions ): WatchStopHandle { if (__DEV__ && !isFunction(cb)) { warn( `\`watch(fn, options?)\` signature has been moved to a separate API. ` + `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + `supports \`watch(source, cb, options?) signature.` ) } return doWatch(source as any, cb, options) } function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ, instance = currentInstance ): WatchStopHandle { if (__DEV__ && !cb) { if (immediate !== undefined) { warn( `watch() "immediate" option is only respected when using the ` + `watch(source, callback, options?) signature.` ) } if (deep !== undefined) { warn( `watch() "deep" option is only respected when using the ` + `watch(source, callback, options?) signature.` ) } } const warnInvalidSource = (s: unknown) => { warn( `Invalid watch source: `, s, `A watch source can only be a getter/effect function, a ref, ` + `a reactive object, or an array of these types.` ) } let getter: () => any let forceTrigger = false let isMultiSource = false if (isRef(source)) { getter = () => (source as Ref).value forceTrigger = !!(source as Ref)._shallow } else if (isReactive(source)) { getter = () => source deep = true } else if (isArray(source)) { isMultiSource = true forceTrigger = source.some(isReactive) getter = () => source.map(s => { if (isRef(s)) { return s.value } else if (isReactive(s)) { return traverse(s) } else if (isFunction(s)) { return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER) } else { __DEV__ && warnInvalidSource(s) } }) } else if (isFunction(source)) { if (cb) { // getter with cb getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER) } else { // no cb -> simple effect getter = () => { if (instance && instance.isUnmounted) { return } if (cleanup) { cleanup() } return callWithAsyncErrorHandling( source, instance, ErrorCodes.WATCH_CALLBACK, [onInvalidate] ) } } } else { getter = NOOP __DEV__ && warnInvalidSource(source) } // 2.x array mutation watch compat if (__COMPAT__ && cb && !deep) { const baseGetter = getter getter = () => { const val = baseGetter() if ( isArray(val) && checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance) ) { traverse(val) } return val } } if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) } let cleanup: () => void let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => { cleanup = runner.options.onStop = () => { callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } } // in SSR there is no need to setup an actual effect, and it should be noop // unless it's eager if (__NODE_JS__ && isInSSRComponentSetup) { // we will also not call the invalidate callback (+ runner is not set up) onInvalidate = NOOP if (!cb) { getter() } else if (immediate) { callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ getter(), undefined, onInvalidate ]) } return NOOP } let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE const job: SchedulerJob = () => { if (!runner.active) { return } if (cb) { // watch(source, cb) const newValue = runner() if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) ) { // cleanup before running cb again if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, // pass undefined as the old value when it's changed for the first time oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onInvalidate ]) oldValue = newValue } } else { // watchEffect runner() } } // important: mark the job as a watcher callback so that scheduler knows // it is allowed to self-trigger (#1727) job.allowRecurse = !!cb let scheduler: ReactiveEffectOptions['scheduler'] 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' scheduler = () => { if (!instance || instance.isMounted) { queuePreFlushCb(job) } else { // with 'pre' option, the first call must happen before // the component is mounted so it is called synchronously. job() } } } const runner = effect(getter, { lazy: true, onTrack, onTrigger, scheduler }) recordInstanceBoundEffect(runner, instance) // initial run if (cb) { if (immediate) { job() } else { oldValue = runner() } } else if (flush === 'post') { queuePostRenderEffect(runner, instance && instance.suspense) } else { runner() } return () => { stop(runner) if (instance) { remove(instance.effects!, runner) } } } // this.$watch export function instanceWatch( this: ComponentInternalInstance, source: string | Function, value: WatchCallback | ObjectWatchOptionItem, options?: WatchOptions ): WatchStopHandle { const publicThis = this.proxy as any const getter = isString(source) ? source.includes('.') ? createPathGetter(publicThis, source) : () => publicThis[source] : source.bind(publicThis, publicThis) let cb if (isFunction(value)) { cb = value } else { cb = value.handler as Function options = value } return doWatch(getter, cb.bind(publicThis), options, this) } export function createPathGetter(ctx: any, path: string) { const segments = path.split('.') return () => { let cur = ctx for (let i = 0; i < segments.length && cur; i++) { cur = cur[segments[i]] } return cur } } function traverse(value: unknown, seen: Set = new Set()) { if (!isObject(value) || seen.has(value)) { return value } seen.add(value) if (isRef(value)) { traverse(value.value, seen) } else if (isArray(value)) { for (let i = 0; i < value.length; i++) { traverse(value[i], seen) } } else if (isSet(value) || isMap(value)) { value.forEach((v: any) => { traverse(v, seen) }) } else if (isPlainObject(value)) { for (const key in value) { traverse((value as any)[key], seen) } } return value }