Skip to content

Commit ceb174b

Browse files
committed
Revert "refactor(runtime-core): reduce the abstraction of base watcher"
This reverts commit 9e4c584.
1 parent 6ea6a0d commit ceb174b

File tree

5 files changed

+189
-195
lines changed

5 files changed

+189
-195
lines changed

packages/reactivity/__tests__/watch.spec.ts

+76
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,40 @@ import {
33
type Ref,
44
WatchErrorCodes,
55
type WatchOptions,
6+
type WatchScheduler,
67
computed,
78
onWatcherCleanup,
89
ref,
910
watch,
1011
} from '../src'
1112

13+
const queue: (() => void)[] = []
14+
15+
// a simple scheduler for testing purposes
16+
let isFlushPending = false
17+
const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
18+
const nextTick = (fn?: () => any) =>
19+
fn ? resolvedPromise.then(fn) : resolvedPromise
20+
21+
const scheduler: WatchScheduler = (job, isFirstRun) => {
22+
if (isFirstRun) {
23+
job()
24+
} else {
25+
queue.push(job)
26+
flushJobs()
27+
}
28+
}
29+
30+
const flushJobs = () => {
31+
if (isFlushPending) return
32+
isFlushPending = true
33+
resolvedPromise.then(() => {
34+
queue.forEach(job => job())
35+
queue.length = 0
36+
isFlushPending = false
37+
})
38+
}
39+
1240
describe('watch', () => {
1341
test('effect', () => {
1442
let dummy: any
@@ -119,6 +147,54 @@ describe('watch', () => {
119147
expect(dummy).toBe(30)
120148
})
121149

150+
test('nested calls to baseWatch and onWatcherCleanup', async () => {
151+
let calls: string[] = []
152+
let source: Ref<number>
153+
let copyist: Ref<number>
154+
const scope = new EffectScope()
155+
156+
scope.run(() => {
157+
source = ref(0)
158+
copyist = ref(0)
159+
// sync by default
160+
watch(
161+
() => {
162+
const current = (copyist.value = source.value)
163+
onWatcherCleanup(() => calls.push(`sync ${current}`))
164+
},
165+
null,
166+
{},
167+
)
168+
// with scheduler
169+
watch(
170+
() => {
171+
const current = copyist.value
172+
onWatcherCleanup(() => calls.push(`post ${current}`))
173+
},
174+
null,
175+
{ scheduler },
176+
)
177+
})
178+
179+
await nextTick()
180+
expect(calls).toEqual([])
181+
182+
scope.run(() => source.value++)
183+
expect(calls).toEqual(['sync 0'])
184+
await nextTick()
185+
expect(calls).toEqual(['sync 0', 'post 0'])
186+
calls.length = 0
187+
188+
scope.run(() => source.value++)
189+
expect(calls).toEqual(['sync 1'])
190+
await nextTick()
191+
expect(calls).toEqual(['sync 1', 'post 1'])
192+
calls.length = 0
193+
194+
scope.stop()
195+
expect(calls).toEqual(['sync 2', 'post 2'])
196+
})
197+
122198
test('once option should be ignored by simple watch', async () => {
123199
let dummy: any
124200
const source = ref(0)

packages/reactivity/src/index.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,8 @@ export {
9090
traverse,
9191
onWatcherCleanup,
9292
WatchErrorCodes,
93-
/**
94-
* @internal
95-
*/
96-
WatcherEffect,
9793
type WatchOptions,
94+
type WatchScheduler,
9895
type WatchStopHandle,
9996
type WatchHandle,
10097
type WatchEffect,

packages/reactivity/src/watch.ts

+72-51
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ export interface WatchOptions<Immediate = boolean> extends DebuggerOptions {
4848
immediate?: Immediate
4949
deep?: boolean | number
5050
once?: boolean
51+
scheduler?: WatchScheduler
5152
onWarn?: (msg: string, ...args: any[]) => void
53+
/**
54+
* @internal
55+
*/
56+
augmentJob?: (job: (...args: any[]) => void) => void
5257
/**
5358
* @internal
5459
*/
@@ -70,6 +75,8 @@ export interface WatchHandle extends WatchStopHandle {
7075
// initial value for watchers to trigger on undefined initial values
7176
const INITIAL_WATCHER_VALUE = {}
7277

78+
export type WatchScheduler = (job: () => void, isFirstRun: boolean) => void
79+
7380
let activeWatcher: WatcherEffect | undefined = undefined
7481

7582
/**
@@ -110,7 +117,7 @@ export function onWatcherCleanup(
110117
}
111118
}
112119

113-
export class WatcherEffect extends ReactiveEffect {
120+
class WatcherEffect extends ReactiveEffect {
114121
forceTrigger: boolean
115122
isMultiSource: boolean
116123
oldValue: any
@@ -122,7 +129,8 @@ export class WatcherEffect extends ReactiveEffect {
122129
public cb?: WatchCallback<any, any> | null | undefined,
123130
public options: WatchOptions = EMPTY_OBJ,
124131
) {
125-
const { deep, once, call, onWarn } = options
132+
const { immediate, deep, once, scheduler, augmentJob, call, onWarn } =
133+
options
126134

127135
let getter: () => any
128136
let forceTrigger = false
@@ -208,53 +216,79 @@ export class WatcherEffect extends ReactiveEffect {
208216
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
209217
: INITIAL_WATCHER_VALUE
210218

219+
const job = this.scheduler.bind(this)
220+
221+
if (augmentJob) {
222+
augmentJob(job)
223+
}
224+
225+
if (scheduler) {
226+
this.scheduler = () => scheduler(job, false)
227+
}
228+
211229
if (__DEV__) {
212230
this.onTrack = options.onTrack
213231
this.onTrigger = options.onTrigger
214232
}
233+
234+
// initial run
235+
if (cb) {
236+
if (immediate) {
237+
job()
238+
} else {
239+
this.oldValue = this.run()
240+
}
241+
} else if (scheduler) {
242+
scheduler(job, true)
243+
} else {
244+
this.run()
245+
}
215246
}
216247

217248
scheduler(): void {
218249
if (!this.dirty) {
219250
return
220251
}
221-
const newValue = this.run()
222-
if (!this.cb) {
223-
return
224-
}
225-
const { deep, call } = this.options
226-
if (
227-
deep ||
228-
this.forceTrigger ||
229-
(this.isMultiSource
230-
? (newValue as any[]).some((v, i) => hasChanged(v, this.oldValue[i]))
231-
: hasChanged(newValue, this.oldValue))
232-
) {
233-
// cleanup before running cb again
234-
if (this.cleanups) {
235-
cleanup(this, this.cleanups)
236-
}
237-
const currentWatcher = activeWatcher
238-
activeWatcher = this
239-
try {
240-
const args = [
241-
newValue,
242-
// pass undefined as the old value when it's changed for the first time
243-
this.oldValue === INITIAL_WATCHER_VALUE
244-
? undefined
245-
: this.isMultiSource && this.oldValue[0] === INITIAL_WATCHER_VALUE
246-
? []
247-
: this.oldValue,
248-
this.boundCleanup,
249-
]
250-
call
251-
? call(this.cb, WatchErrorCodes.WATCH_CALLBACK, args)
252-
: // @ts-expect-error
253-
this.cb(...args)
254-
this.oldValue = newValue
255-
} finally {
256-
activeWatcher = currentWatcher
252+
if (this.cb) {
253+
// watch(source, cb)
254+
const newValue = this.run()
255+
const { deep, call } = this.options
256+
if (
257+
deep ||
258+
this.forceTrigger ||
259+
(this.isMultiSource
260+
? (newValue as any[]).some((v, i) => hasChanged(v, this.oldValue[i]))
261+
: hasChanged(newValue, this.oldValue))
262+
) {
263+
// cleanup before running cb again
264+
if (this.cleanups) {
265+
cleanup(this, this.cleanups)
266+
}
267+
const currentWatcher = activeWatcher
268+
activeWatcher = this
269+
try {
270+
const args = [
271+
newValue,
272+
// pass undefined as the old value when it's changed for the first time
273+
this.oldValue === INITIAL_WATCHER_VALUE
274+
? undefined
275+
: this.isMultiSource && this.oldValue[0] === INITIAL_WATCHER_VALUE
276+
? []
277+
: this.oldValue,
278+
this.boundCleanup,
279+
]
280+
call
281+
? call(this.cb, WatchErrorCodes.WATCH_CALLBACK, args)
282+
: // @ts-expect-error
283+
this.cb(...args)
284+
this.oldValue = newValue
285+
} finally {
286+
activeWatcher = currentWatcher
287+
}
257288
}
289+
} else {
290+
// watchEffect
291+
this.run()
258292
}
259293
}
260294
}
@@ -284,23 +318,10 @@ export function watch(
284318
options: WatchOptions = EMPTY_OBJ,
285319
): WatchHandle {
286320
const effect = new WatcherEffect(source, cb, options)
287-
288-
// initial run
289-
if (cb) {
290-
if (options.immediate) {
291-
effect.scheduler()
292-
} else {
293-
effect.oldValue = effect.run()
294-
}
295-
} else {
296-
effect.run()
297-
}
298-
299321
const stop = effect.stop.bind(effect) as WatchHandle
300322
stop.pause = effect.pause.bind(effect)
301323
stop.resume = effect.resume.bind(effect)
302324
stop.stop = stop
303-
304325
return stop
305326
}
306327

packages/runtime-core/__tests__/apiWatch.spec.ts

-48
Original file line numberDiff line numberDiff line change
@@ -505,54 +505,6 @@ describe('api: watch', () => {
505505
expect(cleanupWatch).toHaveBeenCalledTimes(2)
506506
})
507507

508-
it('nested calls to baseWatch and onWatcherCleanup', async () => {
509-
let calls: string[] = []
510-
let source: Ref<number>
511-
let copyist: Ref<number>
512-
const scope = effectScope()
513-
514-
scope.run(() => {
515-
source = ref(0)
516-
copyist = ref(0)
517-
// sync flush
518-
watch(
519-
() => {
520-
const current = (copyist.value = source.value)
521-
onWatcherCleanup(() => calls.push(`sync ${current}`))
522-
},
523-
null,
524-
{ flush: 'sync' },
525-
)
526-
// post flush
527-
watch(
528-
() => {
529-
const current = copyist.value
530-
onWatcherCleanup(() => calls.push(`post ${current}`))
531-
},
532-
null,
533-
{ flush: 'post' },
534-
)
535-
})
536-
537-
await nextTick()
538-
expect(calls).toEqual([])
539-
540-
scope.run(() => source.value++)
541-
expect(calls).toEqual(['sync 0'])
542-
await nextTick()
543-
expect(calls).toEqual(['sync 0', 'post 0'])
544-
calls.length = 0
545-
546-
scope.run(() => source.value++)
547-
expect(calls).toEqual(['sync 1'])
548-
await nextTick()
549-
expect(calls).toEqual(['sync 1', 'post 1'])
550-
calls.length = 0
551-
552-
scope.stop()
553-
expect(calls).toEqual(['sync 2', 'post 2'])
554-
})
555-
556508
it('flush timing: pre (default)', async () => {
557509
const count = ref(0)
558510
const count2 = ref(0)

0 commit comments

Comments
 (0)