Skip to content

Commit 205e5b5

Browse files
authored
feat(reactivity): base watch, getCurrentWatcher, and onWatcherCleanup (#9927)
1 parent 44973bb commit 205e5b5

File tree

9 files changed

+723
-324
lines changed

9 files changed

+723
-324
lines changed
+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import {
2+
EffectScope,
3+
type Ref,
4+
WatchErrorCodes,
5+
type WatchOptions,
6+
type WatchScheduler,
7+
onWatcherCleanup,
8+
ref,
9+
watch,
10+
} from '../src'
11+
12+
const queue: (() => void)[] = []
13+
14+
// a simple scheduler for testing purposes
15+
let isFlushPending = false
16+
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
17+
const nextTick = (fn?: () => any) =>
18+
fn ? resolvedPromise.then(fn) : resolvedPromise
19+
20+
const scheduler: WatchScheduler = (job, isFirstRun) => {
21+
if (isFirstRun) {
22+
job()
23+
} else {
24+
queue.push(job)
25+
flushJobs()
26+
}
27+
}
28+
29+
const flushJobs = () => {
30+
if (isFlushPending) return
31+
isFlushPending = true
32+
resolvedPromise.then(() => {
33+
queue.forEach(job => job())
34+
queue.length = 0
35+
isFlushPending = false
36+
})
37+
}
38+
39+
describe('watch', () => {
40+
test('effect', () => {
41+
let dummy: any
42+
const source = ref(0)
43+
watch(() => {
44+
dummy = source.value
45+
})
46+
expect(dummy).toBe(0)
47+
source.value++
48+
expect(dummy).toBe(1)
49+
})
50+
51+
test('with callback', () => {
52+
let dummy: any
53+
const source = ref(0)
54+
watch(source, () => {
55+
dummy = source.value
56+
})
57+
expect(dummy).toBe(undefined)
58+
source.value++
59+
expect(dummy).toBe(1)
60+
})
61+
62+
test('call option with error handling', () => {
63+
const onError = vi.fn()
64+
const call: WatchOptions['call'] = function call(fn, type, args) {
65+
if (Array.isArray(fn)) {
66+
fn.forEach(f => call(f, type, args))
67+
return
68+
}
69+
try {
70+
fn.apply(null, args)
71+
} catch (e) {
72+
onError(e, type)
73+
}
74+
}
75+
76+
watch(
77+
() => {
78+
throw 'oops in effect'
79+
},
80+
null,
81+
{ call },
82+
)
83+
84+
const source = ref(0)
85+
const effect = watch(
86+
source,
87+
() => {
88+
onWatcherCleanup(() => {
89+
throw 'oops in cleanup'
90+
})
91+
throw 'oops in watch'
92+
},
93+
{ call },
94+
)
95+
96+
expect(onError.mock.calls.length).toBe(1)
97+
expect(onError.mock.calls[0]).toMatchObject([
98+
'oops in effect',
99+
WatchErrorCodes.WATCH_CALLBACK,
100+
])
101+
102+
source.value++
103+
expect(onError.mock.calls.length).toBe(2)
104+
expect(onError.mock.calls[1]).toMatchObject([
105+
'oops in watch',
106+
WatchErrorCodes.WATCH_CALLBACK,
107+
])
108+
109+
effect!.stop()
110+
source.value++
111+
expect(onError.mock.calls.length).toBe(3)
112+
expect(onError.mock.calls[2]).toMatchObject([
113+
'oops in cleanup',
114+
WatchErrorCodes.WATCH_CLEANUP,
115+
])
116+
})
117+
118+
test('watch with onWatcherCleanup', async () => {
119+
let dummy = 0
120+
let source: Ref<number>
121+
const scope = new EffectScope()
122+
123+
scope.run(() => {
124+
source = ref(0)
125+
watch(onCleanup => {
126+
source.value
127+
128+
onCleanup(() => (dummy += 2))
129+
onWatcherCleanup(() => (dummy += 3))
130+
onWatcherCleanup(() => (dummy += 5))
131+
})
132+
})
133+
expect(dummy).toBe(0)
134+
135+
scope.run(() => {
136+
source.value++
137+
})
138+
expect(dummy).toBe(10)
139+
140+
scope.run(() => {
141+
source.value++
142+
})
143+
expect(dummy).toBe(20)
144+
145+
scope.stop()
146+
expect(dummy).toBe(30)
147+
})
148+
149+
test('nested calls to baseWatch and onWatcherCleanup', async () => {
150+
let calls: string[] = []
151+
let source: Ref<number>
152+
let copyist: Ref<number>
153+
const scope = new EffectScope()
154+
155+
scope.run(() => {
156+
source = ref(0)
157+
copyist = ref(0)
158+
// sync by default
159+
watch(
160+
() => {
161+
const current = (copyist.value = source.value)
162+
onWatcherCleanup(() => calls.push(`sync ${current}`))
163+
},
164+
null,
165+
{},
166+
)
167+
// with scheduler
168+
watch(
169+
() => {
170+
const current = copyist.value
171+
onWatcherCleanup(() => calls.push(`post ${current}`))
172+
},
173+
null,
174+
{ scheduler },
175+
)
176+
})
177+
178+
await nextTick()
179+
expect(calls).toEqual([])
180+
181+
scope.run(() => source.value++)
182+
expect(calls).toEqual(['sync 0'])
183+
await nextTick()
184+
expect(calls).toEqual(['sync 0', 'post 0'])
185+
calls.length = 0
186+
187+
scope.run(() => source.value++)
188+
expect(calls).toEqual(['sync 1'])
189+
await nextTick()
190+
expect(calls).toEqual(['sync 1', 'post 1'])
191+
calls.length = 0
192+
193+
scope.stop()
194+
expect(calls).toEqual(['sync 2', 'post 2'])
195+
})
196+
})

packages/reactivity/src/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,14 @@ export {
8080
} from './effectScope'
8181
export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations'
8282
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
83+
export {
84+
watch,
85+
getCurrentWatcher,
86+
traverse,
87+
onWatcherCleanup,
88+
WatchErrorCodes,
89+
type WatchOptions,
90+
type WatchScheduler,
91+
type WatchStopHandle,
92+
type WatchHandle,
93+
} from './watch'

0 commit comments

Comments
 (0)