2019-05-29 23:44:59 +08:00
|
|
|
import {
|
|
|
|
effect,
|
|
|
|
stop,
|
2019-08-16 10:02:53 -04:00
|
|
|
isRef,
|
|
|
|
Ref,
|
2019-10-14 12:15:09 -04:00
|
|
|
ComputedRef,
|
2019-05-29 23:44:59 +08:00
|
|
|
ReactiveEffectOptions
|
2019-06-11 23:50:28 +08:00
|
|
|
} from '@vue/reactivity'
|
2019-09-11 10:09:00 -04:00
|
|
|
import { queueJob } from './scheduler'
|
2019-10-23 10:53:43 -05:00
|
|
|
import {
|
|
|
|
EMPTY_OBJ,
|
|
|
|
isObject,
|
|
|
|
isArray,
|
|
|
|
isFunction,
|
|
|
|
isString,
|
2020-01-24 11:39:52 -05:00
|
|
|
hasChanged,
|
2020-02-18 13:52:59 -05:00
|
|
|
NOOP,
|
|
|
|
remove
|
2019-10-23 10:53:43 -05:00
|
|
|
} from '@vue/shared'
|
2019-09-11 10:09:00 -04:00
|
|
|
import {
|
|
|
|
currentInstance,
|
|
|
|
ComponentInternalInstance,
|
2020-02-14 00:13:54 -05:00
|
|
|
isInSSRComponentSetup,
|
|
|
|
recordInstanceBoundEffect
|
2019-09-11 10:09:00 -04:00
|
|
|
} from './component'
|
2019-08-30 15:05:39 -04:00
|
|
|
import {
|
2019-09-06 12:58:31 -04:00
|
|
|
ErrorCodes,
|
2019-08-30 15:05:39 -04:00
|
|
|
callWithErrorHandling,
|
|
|
|
callWithAsyncErrorHandling
|
|
|
|
} from './errorHandling'
|
2019-09-11 10:09:00 -04:00
|
|
|
import { onBeforeUnmount } from './apiLifecycle'
|
2019-11-02 12:18:35 -04:00
|
|
|
import { queuePostRenderEffect } from './renderer'
|
2019-12-18 11:54:12 -05:00
|
|
|
import { warn } from './warning'
|
2019-10-22 11:26:48 -04:00
|
|
|
|
2020-02-26 10:12:32 -05:00
|
|
|
export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void
|
2019-12-30 11:30:12 -05:00
|
|
|
|
|
|
|
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
|
|
|
|
|
2020-02-13 12:08:21 -05:00
|
|
|
export type WatchCallback<V = any, OV = any> = (
|
|
|
|
value: V,
|
|
|
|
oldValue: OV,
|
2020-02-26 10:12:32 -05:00
|
|
|
onInvalidate: InvalidateCbRegistrator
|
2019-10-22 11:26:48 -04:00
|
|
|
) => any
|
2019-05-29 23:44:59 +08:00
|
|
|
|
2019-12-30 11:30:12 -05:00
|
|
|
type MapSources<T> = {
|
|
|
|
[K in keyof T]: T[K] extends WatchSource<infer V> ? V : never
|
|
|
|
}
|
|
|
|
|
2020-02-17 23:14:19 -05:00
|
|
|
type MapOldSources<T, Immediate> = {
|
2020-02-13 12:08:21 -05:00
|
|
|
[K in keyof T]: T[K] extends WatchSource<infer V>
|
2020-02-17 23:14:19 -05:00
|
|
|
? Immediate extends true ? (V | undefined) : V
|
2020-02-13 12:08:21 -05:00
|
|
|
: never
|
|
|
|
}
|
|
|
|
|
2020-02-26 10:12:32 -05:00
|
|
|
type InvalidateCbRegistrator = (cb: () => void) => void
|
2019-12-30 11:30:12 -05:00
|
|
|
|
2020-04-27 13:33:57 -04:00
|
|
|
export interface WatchOptionsBase {
|
2019-05-29 23:44:59 +08:00
|
|
|
flush?: 'pre' | 'post' | 'sync'
|
|
|
|
onTrack?: ReactiveEffectOptions['onTrack']
|
|
|
|
onTrigger?: ReactiveEffectOptions['onTrigger']
|
|
|
|
}
|
|
|
|
|
2020-04-27 13:33:57 -04:00
|
|
|
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
|
2020-02-17 23:14:19 -05:00
|
|
|
immediate?: Immediate
|
|
|
|
deep?: boolean
|
|
|
|
}
|
|
|
|
|
2020-04-27 13:33:57 -04:00
|
|
|
export type WatchStopHandle = () => void
|
2019-08-18 22:49:08 -04:00
|
|
|
|
2019-05-29 23:44:59 +08:00
|
|
|
const invoke = (fn: Function) => fn()
|
|
|
|
|
2020-02-22 08:19:10 +01:00
|
|
|
// Simple effect.
|
|
|
|
export function watchEffect(
|
|
|
|
effect: WatchEffect,
|
2020-04-27 13:33:57 -04:00
|
|
|
options?: WatchOptionsBase
|
|
|
|
): WatchStopHandle {
|
2020-02-22 08:19:10 +01:00
|
|
|
return doWatch(effect, null, options)
|
|
|
|
}
|
|
|
|
|
2020-02-04 15:59:04 +01:00
|
|
|
// initial value for watchers to trigger on undefined initial values
|
|
|
|
const INITIAL_WATCHER_VALUE = {}
|
|
|
|
|
2020-02-25 18:57:41 -05:00
|
|
|
// overload #1: single source + cb
|
2020-02-17 23:14:19 -05:00
|
|
|
export function watch<T, Immediate extends Readonly<boolean> = false>(
|
2019-12-30 11:30:12 -05:00
|
|
|
source: WatchSource<T>,
|
2020-02-17 23:14:19 -05:00
|
|
|
cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
|
|
|
|
options?: WatchOptions<Immediate>
|
2020-04-27 13:33:57 -04:00
|
|
|
): WatchStopHandle
|
2019-08-18 22:49:08 -04:00
|
|
|
|
2020-02-25 18:57:41 -05:00
|
|
|
// overload #2: array of multiple sources + cb
|
2019-11-02 10:40:08 -04:00
|
|
|
// Readonly constraint helps the callback to correctly infer value types based
|
|
|
|
// on position in the source array. Otherwise the values will get a union type
|
|
|
|
// of all possible value types.
|
2020-02-13 12:08:21 -05:00
|
|
|
export function watch<
|
|
|
|
T extends Readonly<WatchSource<unknown>[]>,
|
2020-02-17 23:14:19 -05:00
|
|
|
Immediate extends Readonly<boolean> = false
|
2020-02-13 12:08:21 -05:00
|
|
|
>(
|
2019-08-18 22:49:08 -04:00
|
|
|
sources: T,
|
2020-02-17 23:14:19 -05:00
|
|
|
cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>,
|
|
|
|
options?: WatchOptions<Immediate>
|
2020-04-27 13:33:57 -04:00
|
|
|
): WatchStopHandle
|
2019-08-18 22:49:08 -04:00
|
|
|
|
|
|
|
// implementation
|
2019-10-08 22:48:24 +08:00
|
|
|
export function watch<T = any>(
|
2020-02-25 18:57:41 -05:00
|
|
|
source: WatchSource<T> | WatchSource<T>[],
|
|
|
|
cb: WatchCallback<T>,
|
2019-08-18 22:49:08 -04:00
|
|
|
options?: WatchOptions
|
2020-04-27 13:33:57 -04:00
|
|
|
): WatchStopHandle {
|
2020-02-25 18:57:41 -05:00
|
|
|
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.`
|
|
|
|
)
|
2019-08-18 22:49:08 -04:00
|
|
|
}
|
2020-02-25 18:57:41 -05:00
|
|
|
return doWatch(source, cb, options)
|
2019-08-18 22:49:08 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function doWatch(
|
2019-12-30 11:30:12 -05:00
|
|
|
source: WatchSource | WatchSource[] | WatchEffect,
|
|
|
|
cb: WatchCallback | null,
|
2020-02-17 23:14:19 -05:00
|
|
|
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
|
2020-04-27 13:33:57 -04:00
|
|
|
): WatchStopHandle {
|
2020-02-17 23:14:19 -05:00
|
|
|
if (__DEV__ && !cb) {
|
|
|
|
if (immediate !== undefined) {
|
|
|
|
warn(
|
|
|
|
`watch() "immediate" option is only respected when using the ` +
|
2020-02-25 01:03:02 +08:00
|
|
|
`watch(source, callback, options?) signature.`
|
2020-02-17 23:14:19 -05:00
|
|
|
)
|
|
|
|
}
|
|
|
|
if (deep !== undefined) {
|
|
|
|
warn(
|
|
|
|
`watch() "deep" option is only respected when using the ` +
|
2020-02-25 01:03:02 +08:00
|
|
|
`watch(source, callback, options?) signature.`
|
2020-02-17 23:14:19 -05:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-31 16:36:36 -04:00
|
|
|
const instance = currentInstance
|
2019-05-29 23:44:59 +08:00
|
|
|
|
2019-10-09 01:48:13 +08:00
|
|
|
let getter: () => any
|
2019-08-30 15:05:39 -04:00
|
|
|
if (isArray(source)) {
|
|
|
|
getter = () =>
|
2019-10-22 11:52:29 -04:00
|
|
|
source.map(
|
|
|
|
s =>
|
|
|
|
isRef(s)
|
|
|
|
? s.value
|
|
|
|
: callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
|
2019-08-30 15:05:39 -04:00
|
|
|
)
|
|
|
|
} else if (isRef(source)) {
|
|
|
|
getter = () => source.value
|
|
|
|
} else if (cb) {
|
|
|
|
// getter with cb
|
|
|
|
getter = () =>
|
2019-09-06 12:58:31 -04:00
|
|
|
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
|
2019-08-30 15:05:39 -04:00
|
|
|
} else {
|
|
|
|
// no cb -> simple effect
|
|
|
|
getter = () => {
|
2019-09-11 13:22:18 -04:00
|
|
|
if (instance && instance.isUnmounted) {
|
|
|
|
return
|
|
|
|
}
|
2019-08-30 15:05:39 -04:00
|
|
|
if (cleanup) {
|
|
|
|
cleanup()
|
|
|
|
}
|
|
|
|
return callWithErrorHandling(
|
|
|
|
source,
|
|
|
|
instance,
|
2019-09-06 12:58:31 -04:00
|
|
|
ErrorCodes.WATCH_CALLBACK,
|
2020-02-26 10:12:32 -05:00
|
|
|
[onInvalidate]
|
2019-08-30 15:05:39 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-25 01:03:02 +08:00
|
|
|
if (cb && deep) {
|
2019-08-30 15:05:39 -04:00
|
|
|
const baseGetter = getter
|
|
|
|
getter = () => traverse(baseGetter())
|
|
|
|
}
|
|
|
|
|
2020-03-23 11:08:22 -04:00
|
|
|
let cleanup: () => void
|
2020-02-26 10:12:32 -05:00
|
|
|
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
|
2019-10-30 11:29:08 -04:00
|
|
|
cleanup = runner.options.onStop = () => {
|
2019-09-06 12:58:31 -04:00
|
|
|
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
|
2019-08-30 15:05:39 -04:00
|
|
|
}
|
2019-06-06 15:19:04 +08:00
|
|
|
}
|
|
|
|
|
2020-02-17 23:14:19 -05:00
|
|
|
// in SSR there is no need to setup an actual effect, and it should be noop
|
|
|
|
// unless it's eager
|
|
|
|
if (__NODE_JS__ && isInSSRComponentSetup) {
|
|
|
|
if (!cb) {
|
|
|
|
getter()
|
|
|
|
} else if (immediate) {
|
|
|
|
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
|
|
|
|
getter(),
|
|
|
|
undefined,
|
2020-02-26 10:12:32 -05:00
|
|
|
onInvalidate
|
2020-02-17 23:14:19 -05:00
|
|
|
])
|
|
|
|
}
|
|
|
|
return NOOP
|
|
|
|
}
|
|
|
|
|
2020-02-04 15:59:04 +01:00
|
|
|
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
|
2019-05-29 23:44:59 +08:00
|
|
|
const applyCb = cb
|
|
|
|
? () => {
|
2019-09-11 13:22:18 -04:00
|
|
|
if (instance && instance.isUnmounted) {
|
|
|
|
return
|
|
|
|
}
|
2019-05-29 23:44:59 +08:00
|
|
|
const newValue = runner()
|
2019-10-23 10:53:43 -05:00
|
|
|
if (deep || hasChanged(newValue, oldValue)) {
|
2019-06-06 15:19:04 +08:00
|
|
|
// cleanup before running cb again
|
|
|
|
if (cleanup) {
|
|
|
|
cleanup()
|
2019-05-29 23:44:59 +08:00
|
|
|
}
|
2019-09-06 12:58:31 -04:00
|
|
|
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
|
2019-08-30 15:15:23 -04:00
|
|
|
newValue,
|
2020-02-04 15:59:04 +01:00
|
|
|
// pass undefined as the old value when it's changed for the first time
|
|
|
|
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
|
2020-02-26 10:12:32 -05:00
|
|
|
onInvalidate
|
2019-08-30 15:15:23 -04:00
|
|
|
])
|
2019-05-29 23:44:59 +08:00
|
|
|
oldValue = newValue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
: void 0
|
|
|
|
|
2019-09-11 13:22:18 -04:00
|
|
|
let scheduler: (job: () => any) => void
|
|
|
|
if (flush === 'sync') {
|
|
|
|
scheduler = invoke
|
|
|
|
} else if (flush === 'pre') {
|
|
|
|
scheduler = job => {
|
2020-03-18 18:14:51 -04:00
|
|
|
if (!instance || instance.isMounted) {
|
2019-09-11 13:22:18 -04:00
|
|
|
queueJob(job)
|
|
|
|
} else {
|
|
|
|
// with 'pre' option, the first call must happen before
|
|
|
|
// the component is mounted so it is called synchronously.
|
|
|
|
job()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2020-03-30 11:49:51 -04:00
|
|
|
scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
|
2019-09-11 13:22:18 -04:00
|
|
|
}
|
2019-08-19 14:44:52 -04:00
|
|
|
|
2019-05-29 23:44:59 +08:00
|
|
|
const runner = effect(getter, {
|
|
|
|
lazy: true,
|
|
|
|
// so it runs before component update effects in pre flush mode
|
|
|
|
computed: true,
|
2019-06-06 15:19:04 +08:00
|
|
|
onTrack,
|
|
|
|
onTrigger,
|
2019-08-19 14:44:52 -04:00
|
|
|
scheduler: applyCb ? () => scheduler(applyCb) : scheduler
|
2019-05-29 23:44:59 +08:00
|
|
|
})
|
|
|
|
|
2020-02-17 23:14:19 -05:00
|
|
|
recordInstanceBoundEffect(runner)
|
|
|
|
|
|
|
|
// initial run
|
|
|
|
if (applyCb) {
|
|
|
|
if (immediate) {
|
|
|
|
applyCb()
|
2019-08-19 14:44:52 -04:00
|
|
|
} else {
|
2020-02-17 23:14:19 -05:00
|
|
|
oldValue = runner()
|
2019-08-19 14:44:52 -04:00
|
|
|
}
|
2020-02-17 23:14:19 -05:00
|
|
|
} else {
|
|
|
|
runner()
|
2019-05-29 23:44:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
stop(runner)
|
2020-01-09 01:06:16 +08:00
|
|
|
if (instance) {
|
2020-02-18 13:52:59 -05:00
|
|
|
remove(instance.effects!, runner)
|
2020-01-09 01:06:16 +08:00
|
|
|
}
|
2019-05-29 23:44:59 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-04 11:36:27 -04:00
|
|
|
// this.$watch
|
|
|
|
export function instanceWatch(
|
2019-09-06 12:58:31 -04:00
|
|
|
this: ComponentInternalInstance,
|
2019-09-04 11:36:27 -04:00
|
|
|
source: string | Function,
|
|
|
|
cb: Function,
|
|
|
|
options?: WatchOptions
|
2020-04-27 13:33:57 -04:00
|
|
|
): WatchStopHandle {
|
2020-04-17 09:12:50 -04:00
|
|
|
const publicThis = this.proxy as any
|
|
|
|
const getter = isString(source)
|
|
|
|
? () => publicThis[source]
|
|
|
|
: source.bind(publicThis)
|
|
|
|
const stop = watch(getter, cb.bind(publicThis), options)
|
2019-09-11 10:09:00 -04:00
|
|
|
onBeforeUnmount(stop, this)
|
2019-09-04 11:36:27 -04:00
|
|
|
return stop
|
|
|
|
}
|
|
|
|
|
2019-10-22 11:26:48 -04:00
|
|
|
function traverse(value: unknown, seen: Set<unknown> = new Set()) {
|
2019-05-29 23:44:59 +08:00
|
|
|
if (!isObject(value) || seen.has(value)) {
|
2020-04-17 14:55:41 +01:00
|
|
|
return value
|
|
|
|
}
|
2019-05-29 23:44:59 +08:00
|
|
|
seen.add(value)
|
|
|
|
if (isArray(value)) {
|
|
|
|
for (let i = 0; i < value.length; i++) {
|
|
|
|
traverse(value[i], seen)
|
|
|
|
}
|
2019-08-27 15:01:01 -04:00
|
|
|
} else if (value instanceof Map) {
|
2019-10-05 22:44:02 +08:00
|
|
|
value.forEach((v, key) => {
|
2019-08-27 15:01:01 -04:00
|
|
|
// to register mutation dep for existing keys
|
|
|
|
traverse(value.get(key), seen)
|
|
|
|
})
|
|
|
|
} else if (value instanceof Set) {
|
2019-10-05 22:44:02 +08:00
|
|
|
value.forEach(v => {
|
2019-05-29 23:44:59 +08:00
|
|
|
traverse(v, seen)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
for (const key in value) {
|
|
|
|
traverse(value[key], seen)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return value
|
|
|
|
}
|