| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- import {
- isRef,
- isShallow,
- Ref,
- ComputedRef,
- ReactiveEffect,
- isReactive,
- ReactiveFlags,
- EffectScheduler,
- DebuggerOptions,
- getCurrentScope
- } from '@vue/reactivity'
- import { SchedulerJob, queueJob } from './scheduler'
- import {
- EMPTY_OBJ,
- isObject,
- isArray,
- isFunction,
- isString,
- hasChanged,
- NOOP,
- remove,
- isMap,
- isSet,
- isPlainObject,
- extend
- } from '@vue/shared'
- import {
- currentInstance,
- ComponentInternalInstance,
- isInSSRComponentSetup,
- setCurrentInstance,
- unsetCurrentInstance
- } 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'
- import { useSSRContext } from '@vue/runtime-core'
- 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 MapSources<T, Immediate> = {
- [K in keyof T]: T[K] extends WatchSource<infer V>
- ? Immediate extends true
- ? V | undefined
- : V
- : T[K] extends object
- ? Immediate extends true
- ? T[K] | undefined
- : T[K]
- : never
- }
- 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
- }
- 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: 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: multiple sources w/ `as const`
- // watch([foo, bar] as const, () => {})
- // somehow [...T] breaks when the type is readonly
- export function watch<
- T extends Readonly<MultiWatchSources>,
- Immediate extends Readonly<boolean> = false
- >(
- source: T,
- cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
- options?: WatchOptions<Immediate>
- ): WatchStopHandle
- // overload: single source + cb
- export function watch<T, Immediate extends Readonly<boolean> = false>(
- source: WatchSource<T>,
- cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
- 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, Immediate extends true ? T | undefined : T>,
- 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, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
- ): 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.`
- )
- }
- const instance =
- getCurrentScope() === currentInstance?.scope ? currentInstance : null
- // const instance = currentInstance
- let getter: () => any
- let forceTrigger = false
- let isMultiSource = false
- if (isRef(source)) {
- getter = () => source.value
- forceTrigger = isShallow(source)
- } else if (isReactive(source)) {
- getter = () => source
- deep = 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 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,
- [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) {
- 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, scheduler)
- 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()
- }
- const unwatch = () => {
- effect.stop()
- if (instance && instance.scope) {
- remove(instance.scope.effects!, effect)
- }
- }
- 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 cur = currentInstance
- setCurrentInstance(this)
- const res = doWatch(getter, cb.bind(publicThis), options)
- if (cur) {
- setCurrentInstance(cur)
- } else {
- unsetCurrentInstance()
- }
- 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, seen?: Set<unknown>) {
- if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
- return value
- }
- seen = seen || new Set()
- if (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[key], seen)
- }
- }
- return value
- }
|