| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- 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<T = any> = Ref<T, any> | ComputedRef<T> | (() => T)
- export type WatchCallback<V = any, OV = any> = (
- value: V,
- oldValue: OV,
- onCleanup: OnCleanup,
- ) => any
- export 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.
- * @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) => {
- _cb(...args)
- 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 (
- 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,
- ]
- oldValue = newValue
- call
- ? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args)
- : // @ts-expect-error
- cb!(...args)
- } 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()
- }
- 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, number>,
- ): 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
- }
|