Skip to content

Commit d8682e8

Browse files
committed
feat: initial code of baseWatch
1 parent f1fe01e commit d8682e8

File tree

4 files changed

+440
-30
lines changed

4 files changed

+440
-30
lines changed

packages/reactivity/src/baseWatch.ts

Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
import {
2+
EMPTY_OBJ,
3+
isObject,
4+
isArray,
5+
isFunction,
6+
hasChanged,
7+
NOOP,
8+
isMap,
9+
isSet,
10+
isPlainObject,
11+
isPromise
12+
} from '@vue/shared'
13+
import { warn } from './warning'
14+
import { ComputedRef } from './computed'
15+
import { ReactiveFlags } from './constants'
16+
import { DebuggerOptions, ReactiveEffect, EffectScheduler } from './effect'
17+
import { isShallow, isReactive } from './reactive'
18+
import { Ref, isRef } from './ref'
19+
20+
// contexts where user provided function may be executed, in addition to
21+
// lifecycle hooks.
22+
export enum BaseWatchErrorCodes {
23+
WATCH_GETTER = 'BaseWatchErrorCodes_WATCH_GETTER',
24+
WATCH_CALLBACK = 'BaseWatchErrorCodes_WATCH_CALLBACK',
25+
WATCH_CLEANUP = 'BaseWatchErrorCodes_WATCH_CLEANUP'
26+
}
27+
28+
export interface SchedulerJob extends Function {
29+
id?: number
30+
pre?: boolean
31+
active?: boolean
32+
computed?: boolean
33+
/**
34+
* Indicates whether the effect is allowed to recursively trigger itself
35+
* when managed by the scheduler.
36+
*
37+
* By default, a job cannot trigger itself because some built-in method calls,
38+
* e.g. Array.prototype.push actually performs reads as well (#1740) which
39+
* can lead to confusing infinite loops.
40+
* The allowed cases are component update functions and watch callbacks.
41+
* Component update functions may update child component props, which in turn
42+
* trigger flush: "pre" watch callbacks that mutates state that the parent
43+
* relies on (#1801). Watch callbacks doesn't track its dependencies so if it
44+
* triggers itself again, it's likely intentional and it is the user's
45+
* responsibility to perform recursive state mutation that eventually
46+
* stabilizes (#1727).
47+
*/
48+
allowRecurse?: boolean
49+
}
50+
51+
export type WatchEffect = (onCleanup: OnCleanup) => void
52+
53+
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
54+
55+
export type WatchCallback<V = any, OV = any> = (
56+
value: V,
57+
oldValue: OV,
58+
onCleanup: OnCleanup
59+
) => any
60+
61+
type OnCleanup = (cleanupFn: () => void) => void
62+
63+
export interface BaseWatchOptions<Immediate = boolean> extends DebuggerOptions {
64+
immediate?: Immediate
65+
deep?: boolean
66+
once?: boolean
67+
scheduler?: Scheduler
68+
handlerError?: HandleError
69+
handlerWarn?: HandleWarn
70+
}
71+
72+
export type WatchStopHandle = () => void
73+
74+
// initial value for watchers to trigger on undefined initial values
75+
const INITIAL_WATCHER_VALUE = {}
76+
77+
export type Scheduler = (context: {
78+
effect: ReactiveEffect
79+
job: SchedulerJob
80+
isInit: boolean
81+
}) => void
82+
83+
const DEFAULT_SCHEDULER: Scheduler = ({ job }) => job()
84+
85+
export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void
86+
87+
const DEFAULT_HANDLE_ERROR: HandleError = (err: unknown) => {
88+
throw err
89+
}
90+
91+
export type HandleWarn = (msg: string, ...args: any[]) => void
92+
93+
const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()
94+
let activeEffect: ReactiveEffect | undefined = undefined
95+
96+
export function onEffectCleanup(cleanupFn: () => void) {
97+
if (activeEffect) {
98+
const cleanups =
99+
cleanupMap.get(activeEffect) ||
100+
cleanupMap.set(activeEffect, []).get(activeEffect)!
101+
cleanups.push(cleanupFn)
102+
}
103+
}
104+
105+
export function baseWatch(
106+
source: WatchSource | WatchSource[] | WatchEffect | object,
107+
cb: WatchCallback | null,
108+
{
109+
immediate,
110+
deep,
111+
once,
112+
onTrack,
113+
onTrigger,
114+
scheduler = DEFAULT_SCHEDULER,
115+
handlerError = DEFAULT_HANDLE_ERROR,
116+
handlerWarn = warn
117+
}: BaseWatchOptions = EMPTY_OBJ
118+
): WatchStopHandle {
119+
if (cb && once) {
120+
const _cb = cb
121+
cb = (...args) => {
122+
_cb(...args)
123+
unwatch()
124+
}
125+
}
126+
127+
const warnInvalidSource = (s: unknown) => {
128+
handlerWarn(
129+
`Invalid watch source: `,
130+
s,
131+
`A watch source can only be a getter/effect function, a ref, ` +
132+
`a reactive object, or an array of these types.`
133+
)
134+
}
135+
136+
let getter: () => any
137+
let forceTrigger = false
138+
let isMultiSource = false
139+
140+
if (isRef(source)) {
141+
getter = () => source.value
142+
forceTrigger = isShallow(source)
143+
} else if (isReactive(source)) {
144+
getter = () => source
145+
deep = true
146+
} else if (isArray(source)) {
147+
isMultiSource = true
148+
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
149+
getter = () =>
150+
source.map(s => {
151+
if (isRef(s)) {
152+
return s.value
153+
} else if (isReactive(s)) {
154+
return traverse(s)
155+
} else if (isFunction(s)) {
156+
return callWithErrorHandling(
157+
s,
158+
handlerError,
159+
BaseWatchErrorCodes.WATCH_GETTER
160+
)
161+
} else {
162+
__DEV__ && warnInvalidSource(s)
163+
}
164+
})
165+
} else if (isFunction(source)) {
166+
if (cb) {
167+
// getter with cb
168+
getter = () =>
169+
callWithErrorHandling(
170+
source,
171+
handlerError,
172+
BaseWatchErrorCodes.WATCH_GETTER
173+
)
174+
} else {
175+
// no cb -> simple effect
176+
getter = () => {
177+
// TODO: move to scheduler
178+
// if (instance && instance.isUnmounted) {
179+
// return
180+
// }
181+
if (cleanup) {
182+
cleanup()
183+
}
184+
const currentEffect = activeEffect
185+
activeEffect = effect
186+
try {
187+
return callWithAsyncErrorHandling(
188+
source,
189+
handlerError,
190+
BaseWatchErrorCodes.WATCH_CALLBACK,
191+
[onEffectCleanup]
192+
)
193+
} finally {
194+
activeEffect = currentEffect
195+
}
196+
}
197+
}
198+
} else {
199+
getter = NOOP
200+
__DEV__ && warnInvalidSource(source)
201+
}
202+
203+
if (cb && deep) {
204+
const baseGetter = getter
205+
getter = () => traverse(baseGetter())
206+
}
207+
208+
// TODO: support SSR
209+
// in SSR there is no need to setup an actual effect, and it should be noop
210+
// unless it's eager or sync flush
211+
// let ssrCleanup: (() => void)[] | undefined
212+
// if (__SSR__ && isInSSRComponentSetup) {
213+
// // we will also not call the invalidate callback (+ runner is not set up)
214+
// onCleanup = NOOP
215+
// if (!cb) {
216+
// getter()
217+
// } else if (immediate) {
218+
// callWithAsyncErrorHandling(cb, handlerError, BaseWatchErrorCodes.WATCH_CALLBACK, [
219+
// getter(),
220+
// isMultiSource ? [] : undefined,
221+
// onCleanup
222+
// ])
223+
// }
224+
// if (flush === 'sync') {
225+
// const ctx = useSSRContext()!
226+
// ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
227+
// } else {
228+
// return NOOP
229+
// }
230+
// }
231+
232+
let oldValue: any = isMultiSource
233+
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
234+
: INITIAL_WATCHER_VALUE
235+
const job: SchedulerJob = () => {
236+
if (!effect.active || !effect.dirty) {
237+
return
238+
}
239+
if (cb) {
240+
// watch(source, cb)
241+
const newValue = effect.run()
242+
if (
243+
deep ||
244+
forceTrigger ||
245+
(isMultiSource
246+
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
247+
: hasChanged(newValue, oldValue))
248+
) {
249+
// cleanup before running cb again
250+
if (cleanup) {
251+
cleanup()
252+
}
253+
const currentEffect = activeEffect
254+
activeEffect = effect
255+
try {
256+
callWithAsyncErrorHandling(
257+
cb,
258+
handlerError,
259+
BaseWatchErrorCodes.WATCH_CALLBACK,
260+
[
261+
newValue,
262+
// pass undefined as the old value when it's changed for the first time
263+
oldValue === INITIAL_WATCHER_VALUE
264+
? undefined
265+
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
266+
? []
267+
: oldValue,
268+
onEffectCleanup
269+
]
270+
)
271+
oldValue = newValue
272+
} finally {
273+
activeEffect = currentEffect
274+
}
275+
}
276+
} else {
277+
// watchEffect
278+
effect.run()
279+
}
280+
}
281+
282+
// important: mark the job as a watcher callback so that scheduler knows
283+
// it is allowed to self-trigger (#1727)
284+
job.allowRecurse = !!cb
285+
286+
let effectScheduler: EffectScheduler = () =>
287+
scheduler({
288+
effect,
289+
job,
290+
isInit: false
291+
})
292+
293+
const effect = new ReactiveEffect(getter, NOOP, effectScheduler)
294+
295+
const cleanup = (effect.onStop = () => {
296+
const cleanups = cleanupMap.get(effect)
297+
if (cleanups) {
298+
cleanups.forEach(cleanup => cleanup())
299+
cleanupMap.delete(effect)
300+
}
301+
})
302+
303+
const unwatch = () => {
304+
effect.stop()
305+
// TODO: move to doWatch
306+
// if (instance && instance.scope) {
307+
// remove(instance.scope.effects!, effect)
308+
// }
309+
}
310+
311+
if (__DEV__) {
312+
effect.onTrack = onTrack
313+
effect.onTrigger = onTrigger
314+
}
315+
316+
// initial run
317+
if (cb) {
318+
if (immediate) {
319+
job()
320+
} else {
321+
oldValue = effect.run()
322+
}
323+
} else {
324+
scheduler({
325+
effect,
326+
job,
327+
isInit: true
328+
})
329+
}
330+
331+
return unwatch
332+
}
333+
334+
export function traverse(value: unknown, seen?: Set<unknown>) {
335+
if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
336+
return value
337+
}
338+
seen = seen || new Set()
339+
if (seen.has(value)) {
340+
return value
341+
}
342+
seen.add(value)
343+
if (isRef(value)) {
344+
traverse(value.value, seen)
345+
} else if (isArray(value)) {
346+
for (let i = 0; i < value.length; i++) {
347+
traverse(value[i], seen)
348+
}
349+
} else if (isSet(value) || isMap(value)) {
350+
value.forEach((v: any) => {
351+
traverse(v, seen)
352+
})
353+
} else if (isPlainObject(value)) {
354+
for (const key in value) {
355+
traverse(value[key], seen)
356+
}
357+
}
358+
return value
359+
}
360+
361+
export function callWithErrorHandling(
362+
fn: Function,
363+
handleError: HandleError,
364+
type: BaseWatchErrorCodes,
365+
args?: unknown[]
366+
) {
367+
let res
368+
try {
369+
res = args ? fn(...args) : fn()
370+
} catch (err) {
371+
handleError(err, type)
372+
}
373+
return res
374+
}
375+
376+
export function callWithAsyncErrorHandling(
377+
fn: Function | Function[],
378+
handleError: HandleError,
379+
type: BaseWatchErrorCodes,
380+
args?: unknown[]
381+
): any[] {
382+
if (isFunction(fn)) {
383+
const res = callWithErrorHandling(fn, handleError, type, args)
384+
if (res && isPromise(res)) {
385+
res.catch(err => {
386+
handleError(err, type)
387+
})
388+
}
389+
return res
390+
}
391+
392+
const values = []
393+
for (let i = 0; i < fn.length; i++) {
394+
values.push(callWithAsyncErrorHandling(fn[i], handleError, type, args))
395+
}
396+
return values
397+
}

0 commit comments

Comments
 (0)