vue3-yuanma/packages/runtime-core/src/apiWatch.ts

311 lines
7.6 KiB
TypeScript
Raw Normal View History

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