import { isRef, Ref } from './reactivity/ref'
import { ComputedRef } from './reactivity/computed'
import { isReactive, isShallow } from './reactivity/reactive'
import {
warn,
noop,
isArray,
isFunction,
emptyObject,
hasChanged,
isServerRendering,
invokeWithErrorHandling
} from 'core/util'
import { currentInstance } from './currentInstance'
import { traverse } from 'core/observer/traverse'
import Watcher from '../core/observer/watcher'
import { queueWatcher } from '../core/observer/scheduler'
import { DebuggerOptions } from './debug'
const WATCHER = `watcher`
const WATCHER_CB = `${WATCHER} callback`
const WATCHER_GETTER = `${WATCHER} getter`
const WATCHER_CLEANUP = `${WATCHER} cleanup`
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
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__
? { ...options, flush: 'post' }
: { flush: 'post' }) as WatchOptionsBase
)
}
export function watchSyncEffect(
effect: WatchEffect,
options?: DebuggerOptions
) {
return doWatch(
effect,
null,
(__DEV__
? { ...options, flush: 'sync' }
: { flush: 'sync' }) as WatchOptionsBase
)
}
const INITIAL_WATCHER_VALUE = {}
type MultiWatchSources = (WatchSource<unknown> | object)[]
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__ && typeof cb !== 'function') {
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 = 'pre',
onTrack,
onTrigger
}: WatchOptions = emptyObject
): 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 = currentInstance
const call = (fn: Function, type: string, args: any[] | null = null) =>
invokeWithErrorHandling(fn, null, args, instance, type)
let getter: () => any
let forceTrigger = false
let isMultiSource = false
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
getter = () => {
;(source as any).__ob__.dep.depend()
return 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 call(s, WATCHER_GETTER)
} else {
__DEV__ && warnInvalidSource(s)
}
})
} else if (isFunction(source)) {
if (cb) {
getter = () => call(source, WATCHER_GETTER)
} else {
getter = () => {
if (instance && instance._isDestroyed) {
return
}
if (cleanup) {
cleanup()
}
return call(source, WATCHER, [onCleanup])
}
}
} else {
getter = noop
__DEV__ && warnInvalidSource(source)
}
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = watcher.onStop = () => {
call(fn, WATCHER_CLEANUP)
}
}
if (isServerRendering()) {
onCleanup = noop
if (!cb) {
getter()
} else if (immediate) {
call(cb, WATCHER_CB, [
getter(),
isMultiSource ? [] : undefined,
onCleanup
])
}
return noop
}
const watcher = new Watcher(currentInstance, getter, noop, {
lazy: true
})
watcher.noRecurse = !cb
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
watcher.run = () => {
if (!watcher.active) {
return
}
if (cb) {
const newValue = watcher.get()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue))
) {
if (cleanup) {
cleanup()
}
call(cb, WATCHER_CB, [
newValue,
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
])
oldValue = newValue
}
} else {
watcher.get()
}
}
if (flush === 'sync') {
watcher.update = watcher.run
} else if (flush === 'post') {
watcher.post = true
watcher.update = () => queueWatcher(watcher)
} else {
watcher.update = () => {
if (instance && instance === currentInstance && !instance._isMounted) {
const buffer = instance._preWatchers || (instance._preWatchers = [])
if (buffer.indexOf(watcher) < 0) buffer.push(watcher)
} else {
queueWatcher(watcher)
}
}
}
if (__DEV__) {
watcher.onTrack = onTrack
watcher.onTrigger = onTrigger
}
if (cb) {
if (immediate) {
watcher.run()
} else {
oldValue = watcher.get()
}
} else if (flush === 'post' && instance) {
instance.$once('hook:mounted', () => watcher.get())
} else {
watcher.get()
}
return () => {
watcher.teardown()
}
}