import { effect, stop, isRef, Ref, ComputedRef, ReactiveEffectOptions } from '@vue/reactivity' import { queueJob } from './scheduler' import { EMPTY_OBJ, isObject, isArray, isFunction, isString, hasChanged } from '@vue/shared' import { recordEffect } from './apiReactivity' import { currentInstance, ComponentInternalInstance, currentSuspense, Data } from './component' import { ErrorCodes, callWithErrorHandling, callWithAsyncErrorHandling } from './errorHandling' import { onBeforeUnmount } from './apiLifecycle' import { queuePostRenderEffect } from './renderer' import { warn } from './warning' export type WatchEffect = (onCleanup: CleanupRegistrator) => void export type WatchSource = Ref | ComputedRef | (() => T) export type WatchCallback = ( value: T, oldValue: T, onCleanup: CleanupRegistrator ) => any type MapSources = { [K in keyof T]: T[K] extends WatchSource ? V : never } export type CleanupRegistrator = (invalidate: () => void) => void export interface WatchOptions { lazy?: boolean flush?: 'pre' | 'post' | 'sync' deep?: boolean onTrack?: ReactiveEffectOptions['onTrack'] onTrigger?: ReactiveEffectOptions['onTrigger'] } export type StopHandle = () => void const invoke = (fn: Function) => fn() // overload #1: simple effect export function watch(effect: WatchEffect, options?: WatchOptions): StopHandle // overload #2: single source + cb export function watch( source: WatchSource, cb: WatchCallback, options?: WatchOptions ): StopHandle // overload #3: array of multiple sources + cb // Readonly constraint helps the callback to correctly infer value types based // on position in the source array. Otherwise the values will get a union type // of all possible value types. export function watch[]>>( sources: T, cb: WatchCallback>, options?: WatchOptions ): StopHandle // implementation export function watch( effectOrSource: WatchSource | WatchSource[] | WatchEffect, cbOrOptions?: WatchCallback | WatchOptions, options?: WatchOptions ): StopHandle { if (isFunction(cbOrOptions)) { // effect callback as 2nd argument - this is a source watcher return doWatch(effectOrSource, cbOrOptions, options) } else { // 2nd argument is either missing or an options object // - this is a simple effect watcher return doWatch(effectOrSource, null, cbOrOptions) } } function doWatch( source: WatchSource | WatchSource[] | WatchEffect, cb: WatchCallback | null, { lazy, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): StopHandle { const instance = currentInstance const suspense = currentSuspense let getter: () => any if (isArray(source)) { getter = () => source.map( s => isRef(s) ? s.value : callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER) ) } else if (isRef(source)) { getter = () => source.value } else 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 callWithErrorHandling( source, instance, ErrorCodes.WATCH_CALLBACK, [registerCleanup] ) } } if (deep) { const baseGetter = getter getter = () => traverse(baseGetter()) } let cleanup: Function const registerCleanup: CleanupRegistrator = (fn: () => void) => { cleanup = runner.options.onStop = () => { callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } } let oldValue = isArray(source) ? [] : undefined const applyCb = cb ? () => { if (instance && instance.isUnmounted) { return } const newValue = runner() if (deep || hasChanged(newValue, oldValue)) { // cleanup before running cb again if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, oldValue, registerCleanup ]) oldValue = newValue } } : void 0 let scheduler: (job: () => any) => void if (flush === 'sync') { scheduler = invoke } else if (flush === 'pre') { scheduler = job => { if (!instance || instance.vnode.el != null) { queueJob(job) } else { // with 'pre' option, the first call must happen before // the component is mounted so it is called synchronously. job() } } } else { scheduler = job => { queuePostRenderEffect(job, suspense) } } const runner = effect(getter, { lazy: true, // so it runs before component update effects in pre flush mode computed: true, onTrack, onTrigger, scheduler: applyCb ? () => scheduler(applyCb) : scheduler }) if (lazy && cb) { oldValue = runner() } else { if (__DEV__ && lazy && !cb) { warn( `watch() lazy option is only respected when using the ` + `watch(getter, callback) signature.` ) } if (applyCb) { scheduler(applyCb) } else { scheduler(runner) } } recordEffect(runner) return () => { stop(runner) if (instance) { const effects = instance.effects! const index = effects.indexOf(runner) if (index > -1) { effects.splice(index, 1) } } } } // this.$watch export function instanceWatch( this: ComponentInternalInstance, source: string | Function, cb: Function, options?: WatchOptions ): StopHandle { const ctx = this.proxy as Data const getter = isString(source) ? () => ctx[source] : source.bind(ctx) const stop = watch(getter, cb.bind(ctx), options) onBeforeUnmount(stop, this) return stop } function traverse(value: unknown, seen: Set = new Set()) { if (!isObject(value) || seen.has(value)) { return } seen.add(value) if (isArray(value)) { for (let i = 0; i < value.length; i++) { traverse(value[i], seen) } } else if (value instanceof Map) { value.forEach((v, key) => { // to register mutation dep for existing keys traverse(value.get(key), seen) }) } else if (value instanceof Set) { value.forEach(v => { traverse(v, seen) }) } else { for (const key in value) { traverse(value[key], seen) } } return value }