Skip to content

Commit 9571ede

Browse files
committed
refactor(watch): adjsut watch API behavior
BREAKING CHANGE: `watch` behavior has been adjusted. - When using the `watch(source, callback, options?)` signature, the callback now fires lazily by default (consistent with 2.x behavior). Note that the `watch(effect, options?)` signature is still eager, since it must invoke the `effect` immediately to collect dependencies. - The `lazy` option has been replaced by the opposite `immediate` option, which defaults to `false`. (It's ignored when using the effect signature) - Due to the above changes, the `watch` option in Options API now behaves exactly the same as 2.x. - When using the effect signature or `{ immediate: true }`, the intital execution is now performed synchronously instead of deferred until the component is mounted. This is necessary for certain use cases to work properly with `async setup()` and Suspense. The side effect of this is the immediate watcher invocation will no longer have access to the mounted DOM. However, the watcher can be initiated inside `onMounted` to retain previous behavior.
1 parent d9d63f2 commit 9571ede

File tree

7 files changed

+176
-143
lines changed

7 files changed

+176
-143
lines changed

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

+14-26
Original file line numberDiff line numberDiff line change
@@ -149,30 +149,24 @@ describe('api: options', () => {
149149

150150
function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
151151
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
152+
expect(spy).toHaveReturnedWith(ctx)
152153
}
153154

154-
assertCall(spyA, 0, [1, undefined])
155-
assertCall(spyB, 0, [2, undefined])
156-
assertCall(spyC, 0, [{ qux: 3 }, undefined])
157-
expect(spyA).toHaveReturnedWith(ctx)
158-
expect(spyB).toHaveReturnedWith(ctx)
159-
expect(spyC).toHaveReturnedWith(ctx)
160-
161155
ctx.foo++
162156
await nextTick()
163-
expect(spyA).toHaveBeenCalledTimes(2)
164-
assertCall(spyA, 1, [2, 1])
157+
expect(spyA).toHaveBeenCalledTimes(1)
158+
assertCall(spyA, 0, [2, 1])
165159

166160
ctx.bar++
167161
await nextTick()
168-
expect(spyB).toHaveBeenCalledTimes(2)
169-
assertCall(spyB, 1, [3, 2])
162+
expect(spyB).toHaveBeenCalledTimes(1)
163+
assertCall(spyB, 0, [3, 2])
170164

171165
ctx.baz.qux++
172166
await nextTick()
173-
expect(spyC).toHaveBeenCalledTimes(2)
167+
expect(spyC).toHaveBeenCalledTimes(1)
174168
// new and old objects have same identity
175-
assertCall(spyC, 1, [{ qux: 4 }, { qux: 4 }])
169+
assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
176170
})
177171

178172
test('watch array', async () => {
@@ -218,30 +212,24 @@ describe('api: options', () => {
218212

219213
function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
220214
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
215+
expect(spy).toHaveReturnedWith(ctx)
221216
}
222217

223-
assertCall(spyA, 0, [1, undefined])
224-
assertCall(spyB, 0, [2, undefined])
225-
assertCall(spyC, 0, [{ qux: 3 }, undefined])
226-
expect(spyA).toHaveReturnedWith(ctx)
227-
expect(spyB).toHaveReturnedWith(ctx)
228-
expect(spyC).toHaveReturnedWith(ctx)
229-
230218
ctx.foo++
231219
await nextTick()
232-
expect(spyA).toHaveBeenCalledTimes(2)
233-
assertCall(spyA, 1, [2, 1])
220+
expect(spyA).toHaveBeenCalledTimes(1)
221+
assertCall(spyA, 0, [2, 1])
234222

235223
ctx.bar++
236224
await nextTick()
237-
expect(spyB).toHaveBeenCalledTimes(2)
238-
assertCall(spyB, 1, [3, 2])
225+
expect(spyB).toHaveBeenCalledTimes(1)
226+
assertCall(spyB, 0, [3, 2])
239227

240228
ctx.baz.qux++
241229
await nextTick()
242-
expect(spyC).toHaveBeenCalledTimes(2)
230+
expect(spyC).toHaveBeenCalledTimes(1)
243231
// new and old objects have same identity
244-
assertCall(spyC, 1, [{ qux: 4 }, { qux: 4 }])
232+
assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
245233
})
246234

247235
test('provide/inject', () => {

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

+46-66
Original file line numberDiff line numberDiff line change
@@ -13,47 +13,19 @@ import { mockWarn } from '@vue/shared'
1313
describe('api: watch', () => {
1414
mockWarn()
1515

16-
it('basic usage', async () => {
16+
it('watch(effect)', async () => {
1717
const state = reactive({ count: 0 })
1818
let dummy
1919
watch(() => {
2020
dummy = state.count
2121
})
22-
await nextTick()
2322
expect(dummy).toBe(0)
2423

2524
state.count++
2625
await nextTick()
2726
expect(dummy).toBe(1)
2827
})
2928

30-
it('triggers when initial value is null', async () => {
31-
const state = ref(null)
32-
const spy = jest.fn()
33-
watch(() => state.value, spy)
34-
await nextTick()
35-
expect(spy).toHaveBeenCalled()
36-
})
37-
38-
it('triggers when initial value is undefined', async () => {
39-
const state = ref()
40-
const spy = jest.fn()
41-
watch(() => state.value, spy)
42-
await nextTick()
43-
expect(spy).toHaveBeenCalled()
44-
state.value = 3
45-
await nextTick()
46-
expect(spy).toHaveBeenCalledTimes(2)
47-
// testing if undefined can trigger the watcher
48-
state.value = undefined
49-
await nextTick()
50-
expect(spy).toHaveBeenCalledTimes(3)
51-
// it shouldn't trigger if the same value is set
52-
state.value = undefined
53-
await nextTick()
54-
expect(spy).toHaveBeenCalledTimes(3)
55-
})
56-
5729
it('watching single source: getter', async () => {
5830
const state = reactive({ count: 0 })
5931
let dummy
@@ -68,9 +40,6 @@ describe('api: watch', () => {
6840
}
6941
}
7042
)
71-
await nextTick()
72-
expect(dummy).toMatchObject([0, undefined])
73-
7443
state.count++
7544
await nextTick()
7645
expect(dummy).toMatchObject([1, 0])
@@ -87,9 +56,6 @@ describe('api: watch', () => {
8756
prevCount + 1
8857
}
8958
})
90-
await nextTick()
91-
expect(dummy).toMatchObject([0, undefined])
92-
9359
count.value++
9460
await nextTick()
9561
expect(dummy).toMatchObject([1, 0])
@@ -107,9 +73,6 @@ describe('api: watch', () => {
10773
prevCount + 1
10874
}
10975
})
110-
await nextTick()
111-
expect(dummy).toMatchObject([1, undefined])
112-
11376
count.value++
11477
await nextTick()
11578
expect(dummy).toMatchObject([2, 1])
@@ -127,8 +90,6 @@ describe('api: watch', () => {
12790
vals.concat(1)
12891
oldVals.concat(1)
12992
})
130-
await nextTick()
131-
expect(dummy).toMatchObject([[1, 1, 2], []])
13293

13394
state.count++
13495
count.value++
@@ -149,8 +110,6 @@ describe('api: watch', () => {
149110
count + 1
150111
oldStatus === true
151112
})
152-
await nextTick()
153-
expect(dummy).toMatchObject([[1, false], []])
154113

155114
state.count++
156115
status.value = true
@@ -164,7 +123,6 @@ describe('api: watch', () => {
164123
const stop = watch(() => {
165124
dummy = state.count
166125
})
167-
await nextTick()
168126
expect(dummy).toBe(0)
169127

170128
stop()
@@ -174,15 +132,14 @@ describe('api: watch', () => {
174132
expect(dummy).toBe(0)
175133
})
176134

177-
it('cleanup registration (basic)', async () => {
135+
it('cleanup registration (effect)', async () => {
178136
const state = reactive({ count: 0 })
179137
const cleanup = jest.fn()
180138
let dummy
181139
const stop = watch(onCleanup => {
182140
onCleanup(cleanup)
183141
dummy = state.count
184142
})
185-
await nextTick()
186143
expect(dummy).toBe(0)
187144

188145
state.count++
@@ -202,22 +159,30 @@ describe('api: watch', () => {
202159
onCleanup(cleanup)
203160
dummy = count
204161
})
162+
163+
count.value++
205164
await nextTick()
206-
expect(dummy).toBe(0)
165+
expect(cleanup).toHaveBeenCalledTimes(0)
166+
expect(dummy).toBe(1)
207167

208168
count.value++
209169
await nextTick()
210170
expect(cleanup).toHaveBeenCalledTimes(1)
211-
expect(dummy).toBe(1)
171+
expect(dummy).toBe(2)
212172

213173
stop()
214174
expect(cleanup).toHaveBeenCalledTimes(2)
215175
})
216176

217-
it('flush timing: post', async () => {
177+
it('flush timing: post (default)', async () => {
218178
const count = ref(0)
179+
let callCount = 0
219180
const assertion = jest.fn(count => {
220-
expect(serializeInner(root)).toBe(`${count}`)
181+
callCount++
182+
// on mount, the watcher callback should be called before DOM render
183+
// on update, should be called after the count is updated
184+
const expectedDOM = callCount === 1 ? `` : `${count}`
185+
expect(serializeInner(root)).toBe(expectedDOM)
221186
})
222187

223188
const Comp = {
@@ -230,7 +195,6 @@ describe('api: watch', () => {
230195
}
231196
const root = nodeOps.createElement('div')
232197
render(h(Comp), root)
233-
await nextTick()
234198
expect(assertion).toHaveBeenCalledTimes(1)
235199

236200
count.value++
@@ -270,7 +234,6 @@ describe('api: watch', () => {
270234
}
271235
const root = nodeOps.createElement('div')
272236
render(h(Comp), root)
273-
await nextTick()
274237
expect(assertion).toHaveBeenCalledTimes(1)
275238

276239
count.value++
@@ -313,7 +276,6 @@ describe('api: watch', () => {
313276
}
314277
const root = nodeOps.createElement('div')
315278
render(h(Comp), root)
316-
await nextTick()
317279
expect(assertion).toHaveBeenCalledTimes(1)
318280

319281
count.value++
@@ -346,9 +308,6 @@ describe('api: watch', () => {
346308
{ deep: true }
347309
)
348310

349-
await nextTick()
350-
expect(dummy).toEqual([0, 1, 1, true])
351-
352311
state.nested.count++
353312
await nextTick()
354313
expect(dummy).toEqual([1, 1, 1, true])
@@ -369,32 +328,53 @@ describe('api: watch', () => {
369328
expect(dummy).toEqual([1, 2, 2, false])
370329
})
371330

372-
it('lazy', async () => {
331+
it('immediate', async () => {
373332
const count = ref(0)
374333
const cb = jest.fn()
375-
watch(count, cb, { lazy: true })
376-
await nextTick()
377-
expect(cb).not.toHaveBeenCalled()
334+
watch(count, cb, { immediate: true })
335+
expect(cb).toHaveBeenCalledTimes(1)
378336
count.value++
379337
await nextTick()
380-
expect(cb).toHaveBeenCalled()
338+
expect(cb).toHaveBeenCalledTimes(2)
381339
})
382340

383-
it('ignore lazy option when using simple callback', async () => {
341+
it('immediate: triggers when initial value is null', async () => {
342+
const state = ref(null)
343+
const spy = jest.fn()
344+
watch(() => state.value, spy, { immediate: true })
345+
expect(spy).toHaveBeenCalled()
346+
})
347+
348+
it('immediate: triggers when initial value is undefined', async () => {
349+
const state = ref()
350+
const spy = jest.fn()
351+
watch(() => state.value, spy, { immediate: true })
352+
expect(spy).toHaveBeenCalled()
353+
state.value = 3
354+
await nextTick()
355+
expect(spy).toHaveBeenCalledTimes(2)
356+
// testing if undefined can trigger the watcher
357+
state.value = undefined
358+
await nextTick()
359+
expect(spy).toHaveBeenCalledTimes(3)
360+
// it shouldn't trigger if the same value is set
361+
state.value = undefined
362+
await nextTick()
363+
expect(spy).toHaveBeenCalledTimes(3)
364+
})
365+
366+
it('warn immediate option when using effect signature', async () => {
384367
const count = ref(0)
385368
let dummy
386369
// @ts-ignore
387370
watch(
388371
() => {
389372
dummy = count.value
390373
},
391-
{ lazy: true }
374+
{ immediate: false }
392375
)
393-
expect(dummy).toBeUndefined()
394-
expect(`lazy option is only respected`).toHaveBeenWarned()
395-
396-
await nextTick()
397376
expect(dummy).toBe(0)
377+
expect(`"immediate" option is only respected`).toHaveBeenWarned()
398378

399379
count.value++
400380
await nextTick()

0 commit comments

Comments
 (0)