forked from vuejs/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f1fe01e
commit d8682e8
Showing
4 changed files
with
440 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,397 @@ | ||
import { | ||
EMPTY_OBJ, | ||
isObject, | ||
isArray, | ||
isFunction, | ||
hasChanged, | ||
NOOP, | ||
isMap, | ||
isSet, | ||
isPlainObject, | ||
isPromise | ||
} from '@vue/shared' | ||
import { warn } from './warning' | ||
import { ComputedRef } from './computed' | ||
import { ReactiveFlags } from './constants' | ||
import { DebuggerOptions, ReactiveEffect, EffectScheduler } from './effect' | ||
import { isShallow, isReactive } from './reactive' | ||
import { Ref, isRef } from './ref' | ||
|
||
// contexts where user provided function may be executed, in addition to | ||
// lifecycle hooks. | ||
export enum BaseWatchErrorCodes { | ||
WATCH_GETTER = 'BaseWatchErrorCodes_WATCH_GETTER', | ||
WATCH_CALLBACK = 'BaseWatchErrorCodes_WATCH_CALLBACK', | ||
WATCH_CLEANUP = 'BaseWatchErrorCodes_WATCH_CLEANUP' | ||
} | ||
|
||
export interface SchedulerJob extends Function { | ||
id?: number | ||
pre?: boolean | ||
active?: boolean | ||
computed?: boolean | ||
/** | ||
* Indicates whether the effect is allowed to recursively trigger itself | ||
* when managed by the scheduler. | ||
* | ||
* By default, a job cannot trigger itself because some built-in method calls, | ||
* e.g. Array.prototype.push actually performs reads as well (#1740) which | ||
* can lead to confusing infinite loops. | ||
* The allowed cases are component update functions and watch callbacks. | ||
* Component update functions may update child component props, which in turn | ||
* trigger flush: "pre" watch callbacks that mutates state that the parent | ||
* relies on (#1801). Watch callbacks doesn't track its dependencies so if it | ||
* triggers itself again, it's likely intentional and it is the user's | ||
* responsibility to perform recursive state mutation that eventually | ||
* stabilizes (#1727). | ||
*/ | ||
allowRecurse?: boolean | ||
} | ||
|
||
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 OnCleanup = (cleanupFn: () => void) => void | ||
|
||
export interface BaseWatchOptions<Immediate = boolean> extends DebuggerOptions { | ||
immediate?: Immediate | ||
deep?: boolean | ||
once?: boolean | ||
scheduler?: Scheduler | ||
handlerError?: HandleError | ||
handlerWarn?: HandleWarn | ||
} | ||
|
||
export type WatchStopHandle = () => void | ||
|
||
// initial value for watchers to trigger on undefined initial values | ||
const INITIAL_WATCHER_VALUE = {} | ||
|
||
export type Scheduler = (context: { | ||
effect: ReactiveEffect | ||
job: SchedulerJob | ||
isInit: boolean | ||
}) => void | ||
|
||
const DEFAULT_SCHEDULER: Scheduler = ({ job }) => job() | ||
|
||
export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void | ||
|
||
const DEFAULT_HANDLE_ERROR: HandleError = (err: unknown) => { | ||
throw err | ||
} | ||
|
||
export type HandleWarn = (msg: string, ...args: any[]) => void | ||
|
||
const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap() | ||
let activeEffect: ReactiveEffect | undefined = undefined | ||
|
||
export function onEffectCleanup(cleanupFn: () => void) { | ||
if (activeEffect) { | ||
const cleanups = | ||
cleanupMap.get(activeEffect) || | ||
cleanupMap.set(activeEffect, []).get(activeEffect)! | ||
cleanups.push(cleanupFn) | ||
} | ||
} | ||
|
||
export function baseWatch( | ||
source: WatchSource | WatchSource[] | WatchEffect | object, | ||
cb: WatchCallback | null, | ||
{ | ||
immediate, | ||
deep, | ||
once, | ||
onTrack, | ||
onTrigger, | ||
scheduler = DEFAULT_SCHEDULER, | ||
handlerError = DEFAULT_HANDLE_ERROR, | ||
handlerWarn = warn | ||
}: BaseWatchOptions = EMPTY_OBJ | ||
): WatchStopHandle { | ||
if (cb && once) { | ||
const _cb = cb | ||
cb = (...args) => { | ||
_cb(...args) | ||
unwatch() | ||
} | ||
} | ||
|
||
const warnInvalidSource = (s: unknown) => { | ||
handlerWarn( | ||
`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.` | ||
) | ||
} | ||
|
||
let getter: () => any | ||
let forceTrigger = false | ||
let isMultiSource = false | ||
|
||
if (isRef(source)) { | ||
getter = () => source.value | ||
forceTrigger = isShallow(source) | ||
} else if (isReactive(source)) { | ||
getter = () => 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 callWithErrorHandling( | ||
s, | ||
handlerError, | ||
BaseWatchErrorCodes.WATCH_GETTER | ||
) | ||
} else { | ||
__DEV__ && warnInvalidSource(s) | ||
} | ||
}) | ||
} else if (isFunction(source)) { | ||
if (cb) { | ||
// getter with cb | ||
getter = () => | ||
callWithErrorHandling( | ||
source, | ||
handlerError, | ||
BaseWatchErrorCodes.WATCH_GETTER | ||
) | ||
} else { | ||
// no cb -> simple effect | ||
getter = () => { | ||
// TODO: move to scheduler | ||
// if (instance && instance.isUnmounted) { | ||
// return | ||
// } | ||
if (cleanup) { | ||
cleanup() | ||
} | ||
const currentEffect = activeEffect | ||
activeEffect = effect | ||
try { | ||
return callWithAsyncErrorHandling( | ||
source, | ||
handlerError, | ||
BaseWatchErrorCodes.WATCH_CALLBACK, | ||
[onEffectCleanup] | ||
) | ||
} finally { | ||
activeEffect = currentEffect | ||
} | ||
} | ||
} | ||
} else { | ||
getter = NOOP | ||
__DEV__ && warnInvalidSource(source) | ||
} | ||
|
||
if (cb && deep) { | ||
const baseGetter = getter | ||
getter = () => traverse(baseGetter()) | ||
} | ||
|
||
// TODO: support SSR | ||
// in SSR there is no need to setup an actual effect, and it should be noop | ||
// unless it's eager or sync flush | ||
// let ssrCleanup: (() => void)[] | undefined | ||
// if (__SSR__ && isInSSRComponentSetup) { | ||
// // we will also not call the invalidate callback (+ runner is not set up) | ||
// onCleanup = NOOP | ||
// if (!cb) { | ||
// getter() | ||
// } else if (immediate) { | ||
// callWithAsyncErrorHandling(cb, handlerError, BaseWatchErrorCodes.WATCH_CALLBACK, [ | ||
// getter(), | ||
// isMultiSource ? [] : undefined, | ||
// onCleanup | ||
// ]) | ||
// } | ||
// if (flush === 'sync') { | ||
// const ctx = useSSRContext()! | ||
// ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []) | ||
// } else { | ||
// return NOOP | ||
// } | ||
// } | ||
|
||
let oldValue: any = isMultiSource | ||
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) | ||
: INITIAL_WATCHER_VALUE | ||
const job: SchedulerJob = () => { | ||
if (!effect.active || !effect.dirty) { | ||
return | ||
} | ||
if (cb) { | ||
// watch(source, cb) | ||
const newValue = effect.run() | ||
if ( | ||
deep || | ||
forceTrigger || | ||
(isMultiSource | ||
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i])) | ||
: hasChanged(newValue, oldValue)) | ||
) { | ||
// cleanup before running cb again | ||
if (cleanup) { | ||
cleanup() | ||
} | ||
const currentEffect = activeEffect | ||
activeEffect = effect | ||
try { | ||
callWithAsyncErrorHandling( | ||
cb, | ||
handlerError, | ||
BaseWatchErrorCodes.WATCH_CALLBACK, | ||
[ | ||
newValue, | ||
// pass undefined as the old value when it's changed for the first time | ||
oldValue === INITIAL_WATCHER_VALUE | ||
? undefined | ||
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE | ||
? [] | ||
: oldValue, | ||
onEffectCleanup | ||
] | ||
) | ||
oldValue = newValue | ||
} finally { | ||
activeEffect = currentEffect | ||
} | ||
} | ||
} else { | ||
// watchEffect | ||
effect.run() | ||
} | ||
} | ||
|
||
// important: mark the job as a watcher callback so that scheduler knows | ||
// it is allowed to self-trigger (#1727) | ||
job.allowRecurse = !!cb | ||
|
||
let effectScheduler: EffectScheduler = () => | ||
scheduler({ | ||
effect, | ||
job, | ||
isInit: false | ||
}) | ||
|
||
const effect = new ReactiveEffect(getter, NOOP, effectScheduler) | ||
|
||
const cleanup = (effect.onStop = () => { | ||
const cleanups = cleanupMap.get(effect) | ||
if (cleanups) { | ||
cleanups.forEach(cleanup => cleanup()) | ||
cleanupMap.delete(effect) | ||
} | ||
}) | ||
|
||
const unwatch = () => { | ||
effect.stop() | ||
// TODO: move to doWatch | ||
// if (instance && instance.scope) { | ||
// remove(instance.scope.effects!, effect) | ||
// } | ||
} | ||
|
||
if (__DEV__) { | ||
effect.onTrack = onTrack | ||
effect.onTrigger = onTrigger | ||
} | ||
|
||
// initial run | ||
if (cb) { | ||
if (immediate) { | ||
job() | ||
} else { | ||
oldValue = effect.run() | ||
} | ||
} else { | ||
scheduler({ | ||
effect, | ||
job, | ||
isInit: true | ||
}) | ||
} | ||
|
||
return unwatch | ||
} | ||
|
||
export function traverse(value: unknown, seen?: Set<unknown>) { | ||
if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) { | ||
return value | ||
} | ||
seen = seen || new Set() | ||
if (seen.has(value)) { | ||
return value | ||
} | ||
seen.add(value) | ||
if (isRef(value)) { | ||
traverse(value.value, seen) | ||
} else if (isArray(value)) { | ||
for (let i = 0; i < value.length; i++) { | ||
traverse(value[i], seen) | ||
} | ||
} else if (isSet(value) || isMap(value)) { | ||
value.forEach((v: any) => { | ||
traverse(v, seen) | ||
}) | ||
} else if (isPlainObject(value)) { | ||
for (const key in value) { | ||
traverse(value[key], seen) | ||
} | ||
} | ||
return value | ||
} | ||
|
||
export function callWithErrorHandling( | ||
fn: Function, | ||
handleError: HandleError, | ||
type: BaseWatchErrorCodes, | ||
args?: unknown[] | ||
) { | ||
let res | ||
try { | ||
res = args ? fn(...args) : fn() | ||
} catch (err) { | ||
handleError(err, type) | ||
} | ||
return res | ||
} | ||
|
||
export function callWithAsyncErrorHandling( | ||
fn: Function | Function[], | ||
handleError: HandleError, | ||
type: BaseWatchErrorCodes, | ||
args?: unknown[] | ||
): any[] { | ||
if (isFunction(fn)) { | ||
const res = callWithErrorHandling(fn, handleError, type, args) | ||
if (res && isPromise(res)) { | ||
res.catch(err => { | ||
handleError(err, type) | ||
}) | ||
} | ||
return res | ||
} | ||
|
||
const values = [] | ||
for (let i = 0; i < fn.length; i++) { | ||
values.push(callWithAsyncErrorHandling(fn[i], handleError, type, args)) | ||
} | ||
return values | ||
} |
Oops, something went wrong.