Skip to content

Commit 7a410ad

Browse files
committed
refactor(runtime-core): reduce the abstraction of base watcher
1 parent 281e1f9 commit 7a410ad

File tree

5 files changed

+189
-189
lines changed

5 files changed

+189
-189
lines changed

packages/reactivity/__tests__/watch.spec.ts

Lines changed: 0 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,12 @@ import {
33
type Ref,
44
WatchErrorCodes,
55
type WatchOptions,
6-
type WatchScheduler,
76
computed,
87
onWatcherCleanup,
98
ref,
109
watch,
1110
} from '../src'
1211

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-
4012
describe('watch', () => {
4113
test('effect', () => {
4214
let dummy: any
@@ -147,54 +119,6 @@ describe('watch', () => {
147119
expect(dummy).toBe(30)
148120
})
149121

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-
198122
test('once option should be ignored by simple watch', async () => {
199123
let dummy: any
200124
const source = ref(0)

packages/reactivity/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,11 @@ export {
9090
traverse,
9191
onWatcherCleanup,
9292
WatchErrorCodes,
93+
/**
94+
* @internal
95+
*/
96+
WatcherEffect,
9397
type WatchOptions,
94-
type WatchScheduler,
9598
type WatchStopHandle,
9699
type WatchHandle,
97100
type WatchEffect,

packages/reactivity/src/watch.ts

Lines changed: 51 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,7 @@ export interface WatchOptions<Immediate = boolean> extends DebuggerOptions {
4848
immediate?: Immediate
4949
deep?: boolean | number
5050
once?: boolean
51-
scheduler?: WatchScheduler
5251
onWarn?: (msg: string, ...args: any[]) => void
53-
/**
54-
* @internal
55-
*/
56-
augmentJob?: (job: (...args: any[]) => void) => void
5752
/**
5853
* @internal
5954
*/
@@ -75,8 +70,6 @@ export interface WatchHandle extends WatchStopHandle {
7570
// initial value for watchers to trigger on undefined initial values
7671
const INITIAL_WATCHER_VALUE = {}
7772

78-
export type WatchScheduler = (job: () => void, isFirstRun: boolean) => void
79-
8073
let activeWatcher: WatcherEffect | undefined = undefined
8174

8275
/**
@@ -117,7 +110,7 @@ export function onWatcherCleanup(
117110
}
118111
}
119112

120-
class WatcherEffect extends ReactiveEffect {
113+
export class WatcherEffect extends ReactiveEffect {
121114
forceTrigger: boolean
122115
isMultiSource: boolean
123116
oldValue: any
@@ -129,8 +122,7 @@ class WatcherEffect extends ReactiveEffect {
129122
public cb?: WatchCallback<any, any> | null | undefined,
130123
public options: WatchOptions = EMPTY_OBJ,
131124
) {
132-
const { immediate, deep, once, scheduler, augmentJob, call, onWarn } =
133-
options
125+
const { deep, once, call, onWarn } = options
134126

135127
let getter: () => any
136128
let forceTrigger = false
@@ -216,79 +208,53 @@ class WatcherEffect extends ReactiveEffect {
216208
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
217209
: INITIAL_WATCHER_VALUE
218210

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-
229211
if (__DEV__) {
230212
this.onTrack = options.onTrack
231213
this.onTrigger = options.onTrigger
232214
}
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-
}
246215
}
247216

248217
scheduler(): void {
249218
if (!this.dirty) {
250219
return
251220
}
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-
}
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
288257
}
289-
} else {
290-
// watchEffect
291-
this.run()
292258
}
293259
}
294260
}
@@ -318,10 +284,23 @@ export function watch(
318284
options: WatchOptions = EMPTY_OBJ,
319285
): WatchHandle {
320286
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+
321299
const stop = effect.stop.bind(effect) as WatchHandle
322300
stop.pause = effect.pause.bind(effect)
323301
stop.resume = effect.resume.bind(effect)
324302
stop.stop = stop
303+
325304
return stop
326305
}
327306

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,54 @@ 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+
508556
it('flush timing: pre (default)', async () => {
509557
const count = ref(0)
510558
const count2 = ref(0)

0 commit comments

Comments
 (0)