| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- import {
- type ComputedRef,
- type DebuggerOptions,
- type EffectScheduler,
- ReactiveEffect,
- ReactiveFlags,
- type ReactiveMarker,
- type Ref,
- getCurrentScope,
- isReactive,
- isRef,
- isShallow,
- } from '@vue/reactivity'
- import { type SchedulerJob, queueJob } from './scheduler'
- import {
- EMPTY_OBJ,
- NOOP,
- extend,
- hasChanged,
- isArray,
- isFunction,
- isMap,
- isObject,
- isPlainObject,
- isSet,
- isString,
- remove,
- } from '@vue/shared'
- import {
- type ComponentInternalInstance,
- currentInstance,
- isInSSRComponentSetup,
- setCurrentInstance,
- } from './component'
- import {
- ErrorCodes,
- callWithAsyncErrorHandling,
- callWithErrorHandling,
- } from './errorHandling'
- import { queuePostRenderEffect } 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'
- export type WatchEffect = (onCleanup: OnCleanup) => void
- export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
- export type WatchCallback<V = any, OV = any> = (
- value: V,
- oldValue: OV,
- onCleanup: OnCleanup,
- ) => any
- type MaybeUndefined<T, I> = I extends true ? T | undefined : T
- type MapSources<T, Immediate> = {
- [K in keyof T]: T[K] extends WatchSource<infer V>
- ? MaybeUndefined<V, Immediate>
- : T[K] extends object
- ? MaybeUndefined<T[K], Immediate>
- : never
- }
- export type OnCleanup = (cleanupFn: () => void) => void
- export interface WatchOptionsBase extends DebuggerOptions {
- flush?: 'pre' | 'post' | 'sync'
- }
- export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
- immediate?: Immediate
- deep?: boolean
- once?: boolean
- }
- export type WatchStopHandle = () => void
- // Simple effect.
- export function watchEffect(
- effect: WatchEffect,
- options?: WatchOptionsBase,
- ): WatchStopHandle {
- return doWatch(effect, null, options)
- }
- export function watchPostEffect(
- effect: WatchEffect,
- options?: DebuggerOptions,
- ) {
- return doWatch(
- effect,
- null,
- __DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },
- )
- }
- export function watchSyncEffect(
- effect: WatchEffect,
- options?: DebuggerOptions,
- ) {
- return doWatch(
- effect,
- null,
- __DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' },
- )
- }
- // initial value for watchers to trigger on undefined initial values
- const INITIAL_WATCHER_VALUE = {}
- type MultiWatchSources = (WatchSource<unknown> | object)[]
- // overload: single source + cb
- export function watch<T, Immediate extends Readonly<boolean> = false>(
- source: WatchSource<T>,
- cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
- options?: WatchOptions<Immediate>,
- ): WatchStopHandle
- // overload: reactive array or tuple of multiple sources + cb
- export function watch<
- T extends Readonly<MultiWatchSources>,
- Immediate extends Readonly<boolean> = false,
- >(
- sources: readonly [...T] | T,
- cb: [T] extends [ReactiveMarker]
- ? WatchCallback<T, MaybeUndefined<T, Immediate>>
- : WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
- options?: WatchOptions<Immediate>,
- ): WatchStopHandle
- // overload: array of multiple sources + cb
- export function watch<
- T extends MultiWatchSources,
- Immediate extends Readonly<boolean> = false,
- >(
- sources: [...T],
- cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
- options?: WatchOptions<Immediate>,
- ): WatchStopHandle
- // overload: watching reactive object w/ cb
- export function watch<
- T extends object,
- Immediate extends Readonly<boolean> = false,
- >(
- source: T,
- cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
- options?: WatchOptions<Immediate>,
- ): WatchStopHandle
- // implementation
- export function watch<T = any, Immediate extends Readonly<boolean> = false>(
- source: T | WatchSource<T>,
- cb: any,
- options?: WatchOptions<Immediate>,
- ): 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,
- once,
- onTrack,
- onTrigger,
- }: WatchOptions = EMPTY_OBJ,
- ): WatchStopHandle {
- if (cb && once) {
- const _cb = cb
- cb = (...args) => {
- _cb(...args)
- unwatch()
- }
- }
- // TODO remove in 3.5
- if (__DEV__ && deep !== void 0 && typeof deep === 'number') {
- warn(
- `watch() "deep" option with number value will be used as watch depth in future versions. ` +
- `Please use a boolean instead to avoid potential breakage.`,
- )
- }
- 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.`,
- )
- }
- if (once !== undefined) {
- warn(
- `watch() "once" 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.`,
- )
- }
- 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())
- }
- let cleanup: (() => void) | undefined
- let onCleanup: OnCleanup = (fn: () => void) => {
- cleanup = effect.onStop = () => {
- callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
- cleanup = effect.onStop = undefined
- }
- }
- // 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 {
- 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()
- }
- }
- // 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 effect = new ReactiveEffect(getter, NOOP, scheduler)
- 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()
- }
- if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
- return unwatch
- }
- // 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
- }
- const reset = setCurrentInstance(this)
- const res = doWatch(getter, cb.bind(publicThis), options)
- reset()
- return res
- }
- 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
- }
- }
- export function traverse(
- value: unknown,
- depth = Infinity,
- seen?: Set<unknown>,
- ) {
- if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
- return value
- }
- seen = seen || new Set()
- if (seen.has(value)) {
- return value
- }
- seen.add(value)
- 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
- }
|