2019-05-29 15:44:59 +00:00
|
|
|
import {
|
|
|
|
effect,
|
|
|
|
stop,
|
2019-08-16 14:02:53 +00:00
|
|
|
isRef,
|
|
|
|
Ref,
|
2019-10-14 16:15:09 +00:00
|
|
|
ComputedRef,
|
2019-05-29 15:44:59 +00:00
|
|
|
ReactiveEffectOptions
|
2019-06-11 15:50:28 +00:00
|
|
|
} from '@vue/reactivity'
|
2019-09-11 14:09:00 +00:00
|
|
|
import { queueJob } from './scheduler'
|
2019-10-23 15:53:43 +00:00
|
|
|
import {
|
|
|
|
EMPTY_OBJ,
|
|
|
|
isObject,
|
|
|
|
isArray,
|
|
|
|
isFunction,
|
|
|
|
isString,
|
|
|
|
hasChanged
|
|
|
|
} from '@vue/shared'
|
2019-08-20 13:38:00 +00:00
|
|
|
import { recordEffect } from './apiReactivity'
|
2019-09-11 14:09:00 +00:00
|
|
|
import {
|
|
|
|
currentInstance,
|
|
|
|
ComponentInternalInstance,
|
|
|
|
currentSuspense
|
|
|
|
} from './component'
|
2019-08-30 19:05:39 +00:00
|
|
|
import {
|
2019-09-06 16:58:31 +00:00
|
|
|
ErrorCodes,
|
2019-08-30 19:05:39 +00:00
|
|
|
callWithErrorHandling,
|
|
|
|
callWithAsyncErrorHandling
|
|
|
|
} from './errorHandling'
|
2019-09-11 14:09:00 +00:00
|
|
|
import { onBeforeUnmount } from './apiLifecycle'
|
|
|
|
import { queuePostRenderEffect } from './createRenderer'
|
2019-10-22 15:26:48 +00:00
|
|
|
|
|
|
|
export type WatchHandler<T = any> = (
|
|
|
|
value: T,
|
|
|
|
oldValue: T,
|
|
|
|
onCleanup: CleanupRegistrator
|
|
|
|
) => any
|
2019-05-29 15:44:59 +00:00
|
|
|
|
|
|
|
export interface WatchOptions {
|
|
|
|
lazy?: boolean
|
|
|
|
flush?: 'pre' | 'post' | 'sync'
|
|
|
|
deep?: boolean
|
|
|
|
onTrack?: ReactiveEffectOptions['onTrack']
|
|
|
|
onTrigger?: ReactiveEffectOptions['onTrigger']
|
|
|
|
}
|
|
|
|
|
2019-08-19 02:49:08 +00:00
|
|
|
type StopHandle = () => void
|
|
|
|
|
2019-10-14 16:15:09 +00:00
|
|
|
type WatcherSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
|
2019-08-19 02:49:08 +00:00
|
|
|
|
|
|
|
type MapSources<T> = {
|
|
|
|
[K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
|
|
|
|
}
|
|
|
|
|
2019-09-05 20:09:30 +00:00
|
|
|
export type CleanupRegistrator = (invalidate: () => void) => void
|
2019-08-19 02:49:08 +00:00
|
|
|
|
|
|
|
type SimpleEffect = (onCleanup: CleanupRegistrator) => void
|
2019-06-07 06:55:38 +00:00
|
|
|
|
2019-05-29 15:44:59 +00:00
|
|
|
const invoke = (fn: Function) => fn()
|
|
|
|
|
2019-09-11 14:09:00 +00:00
|
|
|
// overload #1: simple effect
|
2019-08-19 02:49:08 +00:00
|
|
|
export function watch(effect: SimpleEffect, options?: WatchOptions): StopHandle
|
|
|
|
|
2019-09-11 14:09:00 +00:00
|
|
|
// overload #2: single source + cb
|
2019-05-29 15:44:59 +00:00
|
|
|
export function watch<T>(
|
2019-08-19 02:49:08 +00:00
|
|
|
source: WatcherSource<T>,
|
2019-10-08 14:48:24 +00:00
|
|
|
cb: WatchHandler<T>,
|
2019-08-19 02:49:08 +00:00
|
|
|
options?: WatchOptions
|
|
|
|
): StopHandle
|
|
|
|
|
2019-09-11 14:09:00 +00:00
|
|
|
// overload #3: array of multiple sources + cb
|
2019-10-22 15:52:29 +00:00
|
|
|
export function watch<T extends Readonly<WatcherSource<unknown>[]>>(
|
2019-08-19 02:49:08 +00:00
|
|
|
sources: T,
|
2019-10-22 15:26:48 +00:00
|
|
|
cb: WatchHandler<MapSources<T>>,
|
2019-08-19 02:49:08 +00:00
|
|
|
options?: WatchOptions
|
|
|
|
): StopHandle
|
|
|
|
|
|
|
|
// implementation
|
2019-10-08 14:48:24 +00:00
|
|
|
export function watch<T = any>(
|
|
|
|
effectOrSource: WatcherSource<T> | WatcherSource<T>[] | SimpleEffect,
|
|
|
|
cbOrOptions?: WatchHandler<T> | WatchOptions,
|
2019-08-19 02:49:08 +00:00
|
|
|
options?: WatchOptions
|
|
|
|
): StopHandle {
|
2019-10-06 15:27:09 +00:00
|
|
|
if (isFunction(cbOrOptions)) {
|
2019-08-19 02:49:08 +00:00
|
|
|
// effect callback as 2nd argument - this is a source watcher
|
2019-10-06 15:27:09 +00:00
|
|
|
return doWatch(effectOrSource, cbOrOptions, options)
|
2019-08-19 02:49:08 +00:00
|
|
|
} else {
|
|
|
|
// 2nd argument is either missing or an options object
|
|
|
|
// - this is a simple effect watcher
|
2019-10-06 15:27:09 +00:00
|
|
|
return doWatch(effectOrSource, null, cbOrOptions)
|
2019-08-19 02:49:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function doWatch(
|
|
|
|
source: WatcherSource | WatcherSource[] | SimpleEffect,
|
2019-10-22 15:26:48 +00:00
|
|
|
cb: WatchHandler | null,
|
2019-08-19 02:49:08 +00:00
|
|
|
{ lazy, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
|
|
|
|
): StopHandle {
|
2019-08-31 20:36:36 +00:00
|
|
|
const instance = currentInstance
|
2019-09-11 14:09:00 +00:00
|
|
|
const suspense = currentSuspense
|
2019-05-29 15:44:59 +00:00
|
|
|
|
2019-10-08 17:48:13 +00:00
|
|
|
let getter: () => any
|
2019-08-30 19:05:39 +00:00
|
|
|
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)
|
2019-08-30 19:05:39 +00:00
|
|
|
)
|
|
|
|
} 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)
|
2019-08-30 19:05:39 +00:00
|
|
|
} else {
|
|
|
|
// no cb -> simple effect
|
|
|
|
getter = () => {
|
2019-09-11 17:22:18 +00:00
|
|
|
if (instance && instance.isUnmounted) {
|
|
|
|
return
|
|
|
|
}
|
2019-08-30 19:05:39 +00:00
|
|
|
if (cleanup) {
|
|
|
|
cleanup()
|
|
|
|
}
|
|
|
|
return callWithErrorHandling(
|
|
|
|
source,
|
|
|
|
instance,
|
2019-09-06 16:58:31 +00:00
|
|
|
ErrorCodes.WATCH_CALLBACK,
|
2019-08-30 19:05:39 +00:00
|
|
|
[registerCleanup]
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (deep) {
|
|
|
|
const baseGetter = getter
|
|
|
|
getter = () => traverse(baseGetter())
|
|
|
|
}
|
|
|
|
|
|
|
|
let cleanup: Function
|
2019-08-19 02:49:08 +00:00
|
|
|
const registerCleanup: CleanupRegistrator = (fn: () => void) => {
|
2019-06-06 07:19:04 +00:00
|
|
|
// TODO wrap the cleanup fn for error handling
|
2019-08-30 19:05:39 +00:00
|
|
|
cleanup = runner.onStop = () => {
|
2019-09-06 16:58:31 +00:00
|
|
|
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
|
2019-08-30 19:05:39 +00:00
|
|
|
}
|
2019-06-06 07:19:04 +00:00
|
|
|
}
|
|
|
|
|
2019-08-27 02:47:38 +00:00
|
|
|
let oldValue = isArray(source) ? [] : undefined
|
2019-05-29 15:44:59 +00:00
|
|
|
const applyCb = cb
|
|
|
|
? () => {
|
2019-09-11 17:22:18 +00:00
|
|
|
if (instance && instance.isUnmounted) {
|
|
|
|
return
|
|
|
|
}
|
2019-05-29 15:44:59 +00:00
|
|
|
const newValue = runner()
|
2019-10-23 15:53:43 +00:00
|
|
|
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,
|
|
|
|
oldValue,
|
|
|
|
registerCleanup
|
|
|
|
])
|
2019-05-29 15:44:59 +00:00
|
|
|
oldValue = newValue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
: void 0
|
|
|
|
|
2019-09-11 17:22:18 +00:00
|
|
|
let scheduler: (job: () => any) => void
|
|
|
|
if (flush === 'sync') {
|
|
|
|
scheduler = invoke
|
|
|
|
} else if (flush === 'pre') {
|
|
|
|
scheduler = job => {
|
|
|
|
if (!instance || instance.vnode.el != null) {
|
|
|
|
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, 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
|
|
|
})
|
|
|
|
|
2019-06-06 07:19:04 +00:00
|
|
|
if (!lazy) {
|
2019-08-19 18:44:52 +00:00
|
|
|
if (applyCb) {
|
|
|
|
scheduler(applyCb)
|
|
|
|
} else {
|
|
|
|
scheduler(runner)
|
|
|
|
}
|
2019-05-29 15:44:59 +00:00
|
|
|
} else {
|
|
|
|
oldValue = runner()
|
|
|
|
}
|
|
|
|
|
|
|
|
recordEffect(runner)
|
|
|
|
return () => {
|
|
|
|
stop(runner)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
2019-10-12 14:30:21 +00:00
|
|
|
): StopHandle {
|
2019-10-08 16:43:13 +00:00
|
|
|
const ctx = this.renderProxy!
|
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)
|
2019-09-11 14:09:00 +00:00
|
|
|
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) {
|
2019-10-05 14:44:02 +00:00
|
|
|
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) {
|
2019-10-05 14:44:02 +00:00
|
|
|
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
|
|
|
|
}
|