e7300eb479
fix #3068
418 lines
11 KiB
TypeScript
418 lines
11 KiB
TypeScript
import {
|
|
effect,
|
|
stop,
|
|
isRef,
|
|
Ref,
|
|
ComputedRef,
|
|
ReactiveEffectOptions,
|
|
isReactive
|
|
} from '@vue/reactivity'
|
|
import { SchedulerJob, queuePreFlushCb } from './scheduler'
|
|
import {
|
|
EMPTY_OBJ,
|
|
isObject,
|
|
isArray,
|
|
isFunction,
|
|
isString,
|
|
hasChanged,
|
|
NOOP,
|
|
remove,
|
|
isMap,
|
|
isSet,
|
|
isPlainObject
|
|
} from '@vue/shared'
|
|
import {
|
|
currentInstance,
|
|
ComponentInternalInstance,
|
|
isInSSRComponentSetup,
|
|
recordInstanceBoundEffect
|
|
} 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'
|
|
|
|
export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void
|
|
|
|
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
|
|
|
|
export type WatchCallback<V = any, OV = any> = (
|
|
value: V,
|
|
oldValue: OV,
|
|
onInvalidate: InvalidateCbRegistrator
|
|
) => 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 InvalidateCbRegistrator = (cb: () => void) => void
|
|
|
|
export interface WatchOptionsBase {
|
|
flush?: 'pre' | 'post' | 'sync'
|
|
onTrack?: ReactiveEffectOptions['onTrack']
|
|
onTrigger?: ReactiveEffectOptions['onTrigger']
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// 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,
|
|
instance = currentInstance
|
|
): 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.`
|
|
)
|
|
}
|
|
|
|
let getter: () => any
|
|
let forceTrigger = false
|
|
let isMultiSource = false
|
|
|
|
if (isRef(source)) {
|
|
getter = () => (source as Ref).value
|
|
forceTrigger = !!(source as Ref)._shallow
|
|
} else if (isReactive(source)) {
|
|
getter = () => source
|
|
deep = true
|
|
} else if (isArray(source)) {
|
|
isMultiSource = true
|
|
forceTrigger = source.some(isReactive)
|
|
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, [
|
|
instance && (instance.proxy as any)
|
|
])
|
|
} else {
|
|
__DEV__ && warnInvalidSource(s)
|
|
}
|
|
})
|
|
} else if (isFunction(source)) {
|
|
if (cb) {
|
|
// getter with cb
|
|
getter = () =>
|
|
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER, [
|
|
instance && (instance.proxy as any)
|
|
])
|
|
} else {
|
|
// no cb -> simple effect
|
|
getter = () => {
|
|
if (instance && instance.isUnmounted) {
|
|
return
|
|
}
|
|
if (cleanup) {
|
|
cleanup()
|
|
}
|
|
return callWithAsyncErrorHandling(
|
|
source,
|
|
instance,
|
|
ErrorCodes.WATCH_CALLBACK,
|
|
[onInvalidate]
|
|
)
|
|
}
|
|
}
|
|
} 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
|
|
let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
|
|
cleanup = runner.options.onStop = () => {
|
|
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
|
|
}
|
|
}
|
|
|
|
// in SSR there is no need to setup an actual effect, and it should be noop
|
|
// unless it's eager
|
|
if (__NODE_JS__ && isInSSRComponentSetup) {
|
|
// we will also not call the invalidate callback (+ runner is not set up)
|
|
onInvalidate = NOOP
|
|
if (!cb) {
|
|
getter()
|
|
} else if (immediate) {
|
|
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
|
|
getter(),
|
|
undefined,
|
|
onInvalidate
|
|
])
|
|
}
|
|
return NOOP
|
|
}
|
|
|
|
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
|
|
const job: SchedulerJob = () => {
|
|
if (!runner.active) {
|
|
return
|
|
}
|
|
if (cb) {
|
|
// watch(source, cb)
|
|
const newValue = runner()
|
|
if (
|
|
deep ||
|
|
forceTrigger ||
|
|
(isMultiSource
|
|
? (newValue as any[]).some((v, i) =>
|
|
hasChanged(v, (oldValue as any[])[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 : oldValue,
|
|
onInvalidate
|
|
])
|
|
oldValue = newValue
|
|
}
|
|
} else {
|
|
// watchEffect
|
|
runner()
|
|
}
|
|
}
|
|
|
|
// important: mark the job as a watcher callback so that scheduler knows
|
|
// it is allowed to self-trigger (#1727)
|
|
job.allowRecurse = !!cb
|
|
|
|
let scheduler: ReactiveEffectOptions['scheduler']
|
|
if (flush === 'sync') {
|
|
scheduler = job
|
|
} else if (flush === 'post') {
|
|
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
|
|
} else {
|
|
// default: 'pre'
|
|
scheduler = () => {
|
|
if (!instance || instance.isMounted) {
|
|
queuePreFlushCb(job)
|
|
} else {
|
|
// with 'pre' option, the first call must happen before
|
|
// the component is mounted so it is called synchronously.
|
|
job()
|
|
}
|
|
}
|
|
}
|
|
|
|
const runner = effect(getter, {
|
|
lazy: true,
|
|
onTrack,
|
|
onTrigger,
|
|
scheduler
|
|
})
|
|
|
|
recordInstanceBoundEffect(runner, instance)
|
|
|
|
// initial run
|
|
if (cb) {
|
|
if (immediate) {
|
|
job()
|
|
} else {
|
|
oldValue = runner()
|
|
}
|
|
} else if (flush === 'post') {
|
|
queuePostRenderEffect(runner, instance && instance.suspense)
|
|
} else {
|
|
runner()
|
|
}
|
|
|
|
return () => {
|
|
stop(runner)
|
|
if (instance) {
|
|
remove(instance.effects!, runner)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
let cb
|
|
if (isFunction(value)) {
|
|
cb = value
|
|
} else {
|
|
cb = value.handler as Function
|
|
options = value
|
|
}
|
|
return doWatch(getter, cb.bind(publicThis), options, this)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
function traverse(value: unknown, seen: Set<unknown> = new Set()) {
|
|
if (!isObject(value) || 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 as any)[key], seen)
|
|
}
|
|
}
|
|
return value
|
|
}
|