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

164 lines
4.1 KiB
TypeScript
Raw Normal View History

2019-05-29 23:44:59 +08:00
import {
effect,
stop,
isRef,
Ref,
2019-05-29 23:44:59 +08:00
ReactiveEffectOptions
} from '@vue/reactivity'
2019-05-29 23:44:59 +08:00
import { queueJob, queuePostFlushCb } from './scheduler'
import { EMPTY_OBJ, isObject, isArray, isFunction } from '@vue/shared'
2019-08-20 21:38:00 +08:00
import { recordEffect } from './apiReactivity'
2019-05-29 23:44:59 +08:00
export interface WatchOptions {
lazy?: boolean
flush?: 'pre' | 'post' | 'sync'
deep?: boolean
onTrack?: ReactiveEffectOptions['onTrack']
onTrigger?: ReactiveEffectOptions['onTrigger']
}
2019-08-19 10:49:08 +08:00
type StopHandle = () => void
type WatcherSource<T = any> = Ref<T> | (() => T)
type MapSources<T> = {
[K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
}
type CleanupRegistrator = (invalidate: () => void) => void
type SimpleEffect = (onCleanup: CleanupRegistrator) => void
2019-05-29 23:44:59 +08:00
const invoke = (fn: Function) => fn()
2019-08-19 10:49:08 +08:00
export function watch(effect: SimpleEffect, options?: WatchOptions): StopHandle
2019-05-29 23:44:59 +08:00
export function watch<T>(
2019-08-19 10:49:08 +08:00
source: WatcherSource<T>,
cb: (newValue: T, oldValue: T, onCleanup: CleanupRegistrator) => any,
options?: WatchOptions
): StopHandle
export function watch<T extends WatcherSource<unknown>[]>(
sources: T,
cb: (
newValues: MapSources<T>,
oldValues: MapSources<T>,
onCleanup: CleanupRegistrator
) => any,
options?: WatchOptions
): StopHandle
// implementation
export function watch(
effectOrSource:
| WatcherSource<unknown>
| WatcherSource<unknown>[]
| SimpleEffect,
effectOrOptions?:
| ((value: any, oldValue: any, onCleanup: CleanupRegistrator) => any)
| WatchOptions,
options?: WatchOptions
): StopHandle {
if (isFunction(effectOrOptions)) {
2019-08-19 10:49:08 +08:00
// effect callback as 2nd argument - this is a source watcher
return doWatch(effectOrSource, effectOrOptions, options)
} else {
// 2nd argument is either missing or an options object
// - this is a simple effect watcher
return doWatch(effectOrSource, null, effectOrOptions)
}
}
function doWatch(
source: WatcherSource | WatcherSource[] | SimpleEffect,
cb:
| ((newValue: any, oldValue: any, onCleanup: CleanupRegistrator) => any)
| null,
{ lazy, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): StopHandle {
const baseGetter = isArray(source)
? () => source.map(s => (isRef(s) ? s.value : s()))
: isRef(source)
? () => source.value
2019-08-27 10:47:38 +08:00
: () => {
if (cleanup) {
cleanup()
}
return source(registerCleanup)
}
2019-06-06 15:19:04 +08:00
const getter = deep ? () => traverse(baseGetter()) : baseGetter
2019-05-29 23:44:59 +08:00
2019-06-06 13:04:49 +08:00
let cleanup: any
2019-08-19 10:49:08 +08:00
const registerCleanup: CleanupRegistrator = (fn: () => void) => {
2019-06-06 15:19:04 +08:00
// TODO wrap the cleanup fn for error handling
cleanup = runner.onStop = fn
}
2019-08-27 10:47:38 +08:00
let oldValue = isArray(source) ? [] : undefined
2019-05-29 23:44:59 +08:00
const applyCb = cb
? () => {
const newValue = runner()
2019-06-06 15:19:04 +08:00
if (deep || newValue !== oldValue) {
// cleanup before running cb again
if (cleanup) {
cleanup()
2019-05-29 23:44:59 +08:00
}
2019-06-06 15:19:04 +08:00
// TODO handle error (including ASYNC)
try {
cb(newValue, oldValue, registerCleanup)
} catch (e) {}
2019-05-29 23:44:59 +08:00
oldValue = newValue
}
}
: void 0
2019-08-20 02:44:52 +08:00
const scheduler =
flush === 'sync' ? invoke : flush === 'pre' ? queueJob : queuePostFlushCb
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-20 02:44:52 +08:00
scheduler: applyCb ? () => scheduler(applyCb) : scheduler
2019-05-29 23:44:59 +08:00
})
2019-06-06 15:19:04 +08:00
if (!lazy) {
2019-08-20 02:44:52 +08:00
if (applyCb) {
scheduler(applyCb)
} else {
scheduler(runner)
}
2019-05-29 23:44:59 +08:00
} else {
oldValue = runner()
}
recordEffect(runner)
return () => {
stop(runner)
}
}
function traverse(value: any, seen: Set<any> = new Set()) {
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)
}
} else if (value instanceof Map || value instanceof Set) {
;(value as any).forEach((v: any) => {
traverse(v, seen)
})
} else {
for (const key in value) {
traverse(value[key], seen)
}
}
return value
}