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, } export type WatchEffect = (onCleanup: OnCleanup) => void export type WatchSource = Ref | ComputedRef | (() => T) export type WatchCallback = ( value: V, oldValue: OV, onCleanup: OnCleanup, ) => any export type OnCleanup = (cleanupFn: () => void) => void export interface WatchOptions 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 void)[]> = new WeakMap() let activeWatcher: ReactiveEffect | undefined = undefined /** * Returns the current active effect if there is one. */ export function getCurrentWatcher(): ReactiveEffect | 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. * @param failSilently - if `true`, will not throw warning when called without * an active effect. * @param owner - The effect that this cleanup function should be attached to. * By default, the current active effect. */ 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) } const scope = getCurrentScope() const watchHandle: WatchHandle = () => { effect.stop() if (scope && scope.active) { remove(scope.effects, effect) } } if (once && cb) { const _cb = cb cb = (...args) => { try { return _cb(...args) } finally { watchHandle() } } } 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 ( immediateFirstRun || deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i])) : hasChanged(newValue, oldValue)) ) { const currentWatcher = activeWatcher pauseTracking() try { // cleanup before running cb again if (cleanup) { cleanup() } activeWatcher = effect 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, ] oldValue = newValue call ? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args) : // @ts-expect-error cb!(...args) } finally { activeWatcher = currentWatcher resetTracking() } } } 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() } 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?: Map, ): unknown { if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) { return value } seen = seen || new Map() if ((seen.get(value) || 0) >= depth) { return value } seen.set(value, depth) 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 }