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.
This commit is contained in:
parent
d9d63f21b1
commit
9571ede84b
@ -149,30 +149,24 @@ describe('api: options', () => {
|
|||||||
|
|
||||||
function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
|
function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
|
||||||
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
|
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
|
||||||
|
expect(spy).toHaveReturnedWith(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertCall(spyA, 0, [1, undefined])
|
|
||||||
assertCall(spyB, 0, [2, undefined])
|
|
||||||
assertCall(spyC, 0, [{ qux: 3 }, undefined])
|
|
||||||
expect(spyA).toHaveReturnedWith(ctx)
|
|
||||||
expect(spyB).toHaveReturnedWith(ctx)
|
|
||||||
expect(spyC).toHaveReturnedWith(ctx)
|
|
||||||
|
|
||||||
ctx.foo++
|
ctx.foo++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(spyA).toHaveBeenCalledTimes(2)
|
expect(spyA).toHaveBeenCalledTimes(1)
|
||||||
assertCall(spyA, 1, [2, 1])
|
assertCall(spyA, 0, [2, 1])
|
||||||
|
|
||||||
ctx.bar++
|
ctx.bar++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(spyB).toHaveBeenCalledTimes(2)
|
expect(spyB).toHaveBeenCalledTimes(1)
|
||||||
assertCall(spyB, 1, [3, 2])
|
assertCall(spyB, 0, [3, 2])
|
||||||
|
|
||||||
ctx.baz.qux++
|
ctx.baz.qux++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(spyC).toHaveBeenCalledTimes(2)
|
expect(spyC).toHaveBeenCalledTimes(1)
|
||||||
// new and old objects have same identity
|
// new and old objects have same identity
|
||||||
assertCall(spyC, 1, [{ qux: 4 }, { qux: 4 }])
|
assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('watch array', async () => {
|
test('watch array', async () => {
|
||||||
@ -218,30 +212,24 @@ describe('api: options', () => {
|
|||||||
|
|
||||||
function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
|
function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
|
||||||
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
|
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
|
||||||
|
expect(spy).toHaveReturnedWith(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertCall(spyA, 0, [1, undefined])
|
|
||||||
assertCall(spyB, 0, [2, undefined])
|
|
||||||
assertCall(spyC, 0, [{ qux: 3 }, undefined])
|
|
||||||
expect(spyA).toHaveReturnedWith(ctx)
|
|
||||||
expect(spyB).toHaveReturnedWith(ctx)
|
|
||||||
expect(spyC).toHaveReturnedWith(ctx)
|
|
||||||
|
|
||||||
ctx.foo++
|
ctx.foo++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(spyA).toHaveBeenCalledTimes(2)
|
expect(spyA).toHaveBeenCalledTimes(1)
|
||||||
assertCall(spyA, 1, [2, 1])
|
assertCall(spyA, 0, [2, 1])
|
||||||
|
|
||||||
ctx.bar++
|
ctx.bar++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(spyB).toHaveBeenCalledTimes(2)
|
expect(spyB).toHaveBeenCalledTimes(1)
|
||||||
assertCall(spyB, 1, [3, 2])
|
assertCall(spyB, 0, [3, 2])
|
||||||
|
|
||||||
ctx.baz.qux++
|
ctx.baz.qux++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(spyC).toHaveBeenCalledTimes(2)
|
expect(spyC).toHaveBeenCalledTimes(1)
|
||||||
// new and old objects have same identity
|
// new and old objects have same identity
|
||||||
assertCall(spyC, 1, [{ qux: 4 }, { qux: 4 }])
|
assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('provide/inject', () => {
|
test('provide/inject', () => {
|
||||||
|
@ -13,13 +13,12 @@ import { mockWarn } from '@vue/shared'
|
|||||||
describe('api: watch', () => {
|
describe('api: watch', () => {
|
||||||
mockWarn()
|
mockWarn()
|
||||||
|
|
||||||
it('basic usage', async () => {
|
it('watch(effect)', async () => {
|
||||||
const state = reactive({ count: 0 })
|
const state = reactive({ count: 0 })
|
||||||
let dummy
|
let dummy
|
||||||
watch(() => {
|
watch(() => {
|
||||||
dummy = state.count
|
dummy = state.count
|
||||||
})
|
})
|
||||||
await nextTick()
|
|
||||||
expect(dummy).toBe(0)
|
expect(dummy).toBe(0)
|
||||||
|
|
||||||
state.count++
|
state.count++
|
||||||
@ -27,33 +26,6 @@ describe('api: watch', () => {
|
|||||||
expect(dummy).toBe(1)
|
expect(dummy).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('triggers when initial value is null', async () => {
|
|
||||||
const state = ref(null)
|
|
||||||
const spy = jest.fn()
|
|
||||||
watch(() => state.value, spy)
|
|
||||||
await nextTick()
|
|
||||||
expect(spy).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('triggers when initial value is undefined', async () => {
|
|
||||||
const state = ref()
|
|
||||||
const spy = jest.fn()
|
|
||||||
watch(() => state.value, spy)
|
|
||||||
await nextTick()
|
|
||||||
expect(spy).toHaveBeenCalled()
|
|
||||||
state.value = 3
|
|
||||||
await nextTick()
|
|
||||||
expect(spy).toHaveBeenCalledTimes(2)
|
|
||||||
// testing if undefined can trigger the watcher
|
|
||||||
state.value = undefined
|
|
||||||
await nextTick()
|
|
||||||
expect(spy).toHaveBeenCalledTimes(3)
|
|
||||||
// it shouldn't trigger if the same value is set
|
|
||||||
state.value = undefined
|
|
||||||
await nextTick()
|
|
||||||
expect(spy).toHaveBeenCalledTimes(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('watching single source: getter', async () => {
|
it('watching single source: getter', async () => {
|
||||||
const state = reactive({ count: 0 })
|
const state = reactive({ count: 0 })
|
||||||
let dummy
|
let dummy
|
||||||
@ -68,9 +40,6 @@ describe('api: watch', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
await nextTick()
|
|
||||||
expect(dummy).toMatchObject([0, undefined])
|
|
||||||
|
|
||||||
state.count++
|
state.count++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(dummy).toMatchObject([1, 0])
|
expect(dummy).toMatchObject([1, 0])
|
||||||
@ -87,9 +56,6 @@ describe('api: watch', () => {
|
|||||||
prevCount + 1
|
prevCount + 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await nextTick()
|
|
||||||
expect(dummy).toMatchObject([0, undefined])
|
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(dummy).toMatchObject([1, 0])
|
expect(dummy).toMatchObject([1, 0])
|
||||||
@ -107,9 +73,6 @@ describe('api: watch', () => {
|
|||||||
prevCount + 1
|
prevCount + 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await nextTick()
|
|
||||||
expect(dummy).toMatchObject([1, undefined])
|
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(dummy).toMatchObject([2, 1])
|
expect(dummy).toMatchObject([2, 1])
|
||||||
@ -127,8 +90,6 @@ describe('api: watch', () => {
|
|||||||
vals.concat(1)
|
vals.concat(1)
|
||||||
oldVals.concat(1)
|
oldVals.concat(1)
|
||||||
})
|
})
|
||||||
await nextTick()
|
|
||||||
expect(dummy).toMatchObject([[1, 1, 2], []])
|
|
||||||
|
|
||||||
state.count++
|
state.count++
|
||||||
count.value++
|
count.value++
|
||||||
@ -149,8 +110,6 @@ describe('api: watch', () => {
|
|||||||
count + 1
|
count + 1
|
||||||
oldStatus === true
|
oldStatus === true
|
||||||
})
|
})
|
||||||
await nextTick()
|
|
||||||
expect(dummy).toMatchObject([[1, false], []])
|
|
||||||
|
|
||||||
state.count++
|
state.count++
|
||||||
status.value = true
|
status.value = true
|
||||||
@ -164,7 +123,6 @@ describe('api: watch', () => {
|
|||||||
const stop = watch(() => {
|
const stop = watch(() => {
|
||||||
dummy = state.count
|
dummy = state.count
|
||||||
})
|
})
|
||||||
await nextTick()
|
|
||||||
expect(dummy).toBe(0)
|
expect(dummy).toBe(0)
|
||||||
|
|
||||||
stop()
|
stop()
|
||||||
@ -174,7 +132,7 @@ describe('api: watch', () => {
|
|||||||
expect(dummy).toBe(0)
|
expect(dummy).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cleanup registration (basic)', async () => {
|
it('cleanup registration (effect)', async () => {
|
||||||
const state = reactive({ count: 0 })
|
const state = reactive({ count: 0 })
|
||||||
const cleanup = jest.fn()
|
const cleanup = jest.fn()
|
||||||
let dummy
|
let dummy
|
||||||
@ -182,7 +140,6 @@ describe('api: watch', () => {
|
|||||||
onCleanup(cleanup)
|
onCleanup(cleanup)
|
||||||
dummy = state.count
|
dummy = state.count
|
||||||
})
|
})
|
||||||
await nextTick()
|
|
||||||
expect(dummy).toBe(0)
|
expect(dummy).toBe(0)
|
||||||
|
|
||||||
state.count++
|
state.count++
|
||||||
@ -202,22 +159,30 @@ describe('api: watch', () => {
|
|||||||
onCleanup(cleanup)
|
onCleanup(cleanup)
|
||||||
dummy = count
|
dummy = count
|
||||||
})
|
})
|
||||||
|
|
||||||
|
count.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(dummy).toBe(0)
|
expect(cleanup).toHaveBeenCalledTimes(0)
|
||||||
|
expect(dummy).toBe(1)
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(cleanup).toHaveBeenCalledTimes(1)
|
expect(cleanup).toHaveBeenCalledTimes(1)
|
||||||
expect(dummy).toBe(1)
|
expect(dummy).toBe(2)
|
||||||
|
|
||||||
stop()
|
stop()
|
||||||
expect(cleanup).toHaveBeenCalledTimes(2)
|
expect(cleanup).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('flush timing: post', async () => {
|
it('flush timing: post (default)', async () => {
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
|
let callCount = 0
|
||||||
const assertion = jest.fn(count => {
|
const assertion = jest.fn(count => {
|
||||||
expect(serializeInner(root)).toBe(`${count}`)
|
callCount++
|
||||||
|
// on mount, the watcher callback should be called before DOM render
|
||||||
|
// on update, should be called after the count is updated
|
||||||
|
const expectedDOM = callCount === 1 ? `` : `${count}`
|
||||||
|
expect(serializeInner(root)).toBe(expectedDOM)
|
||||||
})
|
})
|
||||||
|
|
||||||
const Comp = {
|
const Comp = {
|
||||||
@ -230,7 +195,6 @@ describe('api: watch', () => {
|
|||||||
}
|
}
|
||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
await nextTick()
|
|
||||||
expect(assertion).toHaveBeenCalledTimes(1)
|
expect(assertion).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
@ -270,7 +234,6 @@ describe('api: watch', () => {
|
|||||||
}
|
}
|
||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
await nextTick()
|
|
||||||
expect(assertion).toHaveBeenCalledTimes(1)
|
expect(assertion).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
@ -313,7 +276,6 @@ describe('api: watch', () => {
|
|||||||
}
|
}
|
||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
await nextTick()
|
|
||||||
expect(assertion).toHaveBeenCalledTimes(1)
|
expect(assertion).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
@ -346,9 +308,6 @@ describe('api: watch', () => {
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
await nextTick()
|
|
||||||
expect(dummy).toEqual([0, 1, 1, true])
|
|
||||||
|
|
||||||
state.nested.count++
|
state.nested.count++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(dummy).toEqual([1, 1, 1, true])
|
expect(dummy).toEqual([1, 1, 1, true])
|
||||||
@ -369,18 +328,42 @@ describe('api: watch', () => {
|
|||||||
expect(dummy).toEqual([1, 2, 2, false])
|
expect(dummy).toEqual([1, 2, 2, false])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('lazy', async () => {
|
it('immediate', async () => {
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
const cb = jest.fn()
|
const cb = jest.fn()
|
||||||
watch(count, cb, { lazy: true })
|
watch(count, cb, { immediate: true })
|
||||||
await nextTick()
|
expect(cb).toHaveBeenCalledTimes(1)
|
||||||
expect(cb).not.toHaveBeenCalled()
|
|
||||||
count.value++
|
count.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(cb).toHaveBeenCalled()
|
expect(cb).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('ignore lazy option when using simple callback', async () => {
|
it('immediate: triggers when initial value is null', async () => {
|
||||||
|
const state = ref(null)
|
||||||
|
const spy = jest.fn()
|
||||||
|
watch(() => state.value, spy, { immediate: true })
|
||||||
|
expect(spy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('immediate: triggers when initial value is undefined', async () => {
|
||||||
|
const state = ref()
|
||||||
|
const spy = jest.fn()
|
||||||
|
watch(() => state.value, spy, { immediate: true })
|
||||||
|
expect(spy).toHaveBeenCalled()
|
||||||
|
state.value = 3
|
||||||
|
await nextTick()
|
||||||
|
expect(spy).toHaveBeenCalledTimes(2)
|
||||||
|
// testing if undefined can trigger the watcher
|
||||||
|
state.value = undefined
|
||||||
|
await nextTick()
|
||||||
|
expect(spy).toHaveBeenCalledTimes(3)
|
||||||
|
// it shouldn't trigger if the same value is set
|
||||||
|
state.value = undefined
|
||||||
|
await nextTick()
|
||||||
|
expect(spy).toHaveBeenCalledTimes(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('warn immediate option when using effect signature', async () => {
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
let dummy
|
let dummy
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -388,13 +371,10 @@ describe('api: watch', () => {
|
|||||||
() => {
|
() => {
|
||||||
dummy = count.value
|
dummy = count.value
|
||||||
},
|
},
|
||||||
{ lazy: true }
|
{ immediate: false }
|
||||||
)
|
)
|
||||||
expect(dummy).toBeUndefined()
|
|
||||||
expect(`lazy option is only respected`).toHaveBeenWarned()
|
|
||||||
|
|
||||||
await nextTick()
|
|
||||||
expect(dummy).toBe(0)
|
expect(dummy).toBe(0)
|
||||||
|
expect(`"immediate" option is only respected`).toHaveBeenWarned()
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
@ -164,8 +164,14 @@ describe('Suspense', () => {
|
|||||||
deps.push(p.then(() => Promise.resolve()))
|
deps.push(p.then(() => Promise.resolve()))
|
||||||
|
|
||||||
watch(() => {
|
watch(() => {
|
||||||
|
calls.push('immediate effect')
|
||||||
|
})
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
watch(count, v => {
|
||||||
calls.push('watch callback')
|
calls.push('watch callback')
|
||||||
})
|
})
|
||||||
|
count.value++ // trigger the watcher now
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
calls.push('mounted')
|
calls.push('mounted')
|
||||||
@ -193,18 +199,24 @@ describe('Suspense', () => {
|
|||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
|
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
|
||||||
expect(calls).toEqual([])
|
expect(calls).toEqual([`immediate effect`])
|
||||||
|
|
||||||
await Promise.all(deps)
|
await Promise.all(deps)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(serializeInner(root)).toBe(`<div>async</div>`)
|
expect(serializeInner(root)).toBe(`<div>async</div>`)
|
||||||
expect(calls).toEqual([`watch callback`, `mounted`])
|
expect(calls).toEqual([`immediate effect`, `watch callback`, `mounted`])
|
||||||
|
|
||||||
// effects inside an already resolved suspense should happen at normal timing
|
// effects inside an already resolved suspense should happen at normal timing
|
||||||
toggle.value = false
|
toggle.value = false
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
await nextTick()
|
||||||
expect(serializeInner(root)).toBe(`<!---->`)
|
expect(serializeInner(root)).toBe(`<!---->`)
|
||||||
expect(calls).toEqual([`watch callback`, `mounted`, 'unmounted'])
|
expect(calls).toEqual([
|
||||||
|
`immediate effect`,
|
||||||
|
`watch callback`,
|
||||||
|
`mounted`,
|
||||||
|
'unmounted'
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('content update before suspense resolve', async () => {
|
test('content update before suspense resolve', async () => {
|
||||||
@ -254,8 +266,14 @@ describe('Suspense', () => {
|
|||||||
deps.push(p)
|
deps.push(p)
|
||||||
|
|
||||||
watch(() => {
|
watch(() => {
|
||||||
|
calls.push('immediate effect')
|
||||||
|
})
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
watch(count, () => {
|
||||||
calls.push('watch callback')
|
calls.push('watch callback')
|
||||||
})
|
})
|
||||||
|
count.value++ // trigger the watcher now
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
calls.push('mounted')
|
calls.push('mounted')
|
||||||
@ -283,7 +301,7 @@ describe('Suspense', () => {
|
|||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
|
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
|
||||||
expect(calls).toEqual([])
|
expect(calls).toEqual(['immediate effect'])
|
||||||
|
|
||||||
// remove the async dep before it's resolved
|
// remove the async dep before it's resolved
|
||||||
toggle.value = false
|
toggle.value = false
|
||||||
@ -294,8 +312,8 @@ describe('Suspense', () => {
|
|||||||
await Promise.all(deps)
|
await Promise.all(deps)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(serializeInner(root)).toBe(`<!---->`)
|
expect(serializeInner(root)).toBe(`<!---->`)
|
||||||
// should discard effects
|
// should discard effects (except for immediate ones)
|
||||||
expect(calls).toEqual([])
|
expect(calls).toEqual(['immediate effect'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('unmount suspense after resolve', async () => {
|
test('unmount suspense after resolve', async () => {
|
||||||
|
@ -241,7 +241,7 @@ describe('error handling', () => {
|
|||||||
expect(fn).toHaveBeenCalledWith(err, 'ref function')
|
expect(fn).toHaveBeenCalledWith(err, 'ref function')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('in watch (simple usage)', () => {
|
test('in watch (effect)', () => {
|
||||||
const err = new Error('foo')
|
const err = new Error('foo')
|
||||||
const fn = jest.fn()
|
const fn = jest.fn()
|
||||||
|
|
||||||
@ -268,7 +268,7 @@ describe('error handling', () => {
|
|||||||
expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
|
expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('in watch getter', () => {
|
test('in watch (getter)', () => {
|
||||||
const err = new Error('foo')
|
const err = new Error('foo')
|
||||||
const fn = jest.fn()
|
const fn = jest.fn()
|
||||||
|
|
||||||
@ -298,7 +298,7 @@ describe('error handling', () => {
|
|||||||
expect(fn).toHaveBeenCalledWith(err, 'watcher getter')
|
expect(fn).toHaveBeenCalledWith(err, 'watcher getter')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('in watch callback', () => {
|
test('in watch (callback)', async () => {
|
||||||
const err = new Error('foo')
|
const err = new Error('foo')
|
||||||
const fn = jest.fn()
|
const fn = jest.fn()
|
||||||
|
|
||||||
@ -312,10 +312,11 @@ describe('error handling', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
const Child = {
|
const Child = {
|
||||||
setup() {
|
setup() {
|
||||||
watch(
|
watch(
|
||||||
() => 1,
|
() => count.value,
|
||||||
() => {
|
() => {
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
@ -325,6 +326,9 @@ describe('error handling', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(h(Comp), nodeOps.createElement('div'))
|
render(h(Comp), nodeOps.createElement('div'))
|
||||||
|
|
||||||
|
count.value++
|
||||||
|
await nextTick()
|
||||||
expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
|
expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -47,22 +47,25 @@ type MapSources<T> = {
|
|||||||
[K in keyof T]: T[K] extends WatchSource<infer V> ? V : never
|
[K in keyof T]: T[K] extends WatchSource<infer V> ? V : never
|
||||||
}
|
}
|
||||||
|
|
||||||
type MapOldSources<T, Lazy> = {
|
type MapOldSources<T, Immediate> = {
|
||||||
[K in keyof T]: T[K] extends WatchSource<infer V>
|
[K in keyof T]: T[K] extends WatchSource<infer V>
|
||||||
? Lazy extends true ? V : (V | undefined)
|
? Immediate extends true ? (V | undefined) : V
|
||||||
: never
|
: never
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CleanupRegistrator = (invalidate: () => void) => void
|
export type CleanupRegistrator = (invalidate: () => void) => void
|
||||||
|
|
||||||
export interface WatchOptions<Lazy = boolean> {
|
export interface BaseWatchOptions {
|
||||||
lazy?: Lazy
|
|
||||||
flush?: 'pre' | 'post' | 'sync'
|
flush?: 'pre' | 'post' | 'sync'
|
||||||
deep?: boolean
|
|
||||||
onTrack?: ReactiveEffectOptions['onTrack']
|
onTrack?: ReactiveEffectOptions['onTrack']
|
||||||
onTrigger?: ReactiveEffectOptions['onTrigger']
|
onTrigger?: ReactiveEffectOptions['onTrigger']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WatchOptions<Immediate = boolean> extends BaseWatchOptions {
|
||||||
|
immediate?: Immediate
|
||||||
|
deep?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type StopHandle = () => void
|
export type StopHandle = () => void
|
||||||
|
|
||||||
const invoke = (fn: Function) => fn()
|
const invoke = (fn: Function) => fn()
|
||||||
@ -73,14 +76,14 @@ const INITIAL_WATCHER_VALUE = {}
|
|||||||
// overload #1: simple effect
|
// overload #1: simple effect
|
||||||
export function watch(
|
export function watch(
|
||||||
effect: WatchEffect,
|
effect: WatchEffect,
|
||||||
options?: WatchOptions<false>
|
options?: BaseWatchOptions
|
||||||
): StopHandle
|
): StopHandle
|
||||||
|
|
||||||
// overload #2: single source + cb
|
// overload #2: single source + cb
|
||||||
export function watch<T, Lazy extends Readonly<boolean> = false>(
|
export function watch<T, Immediate extends Readonly<boolean> = false>(
|
||||||
source: WatchSource<T>,
|
source: WatchSource<T>,
|
||||||
cb: WatchCallback<T, Lazy extends true ? T : (T | undefined)>,
|
cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
|
||||||
options?: WatchOptions<Lazy>
|
options?: WatchOptions<Immediate>
|
||||||
): StopHandle
|
): StopHandle
|
||||||
|
|
||||||
// overload #3: array of multiple sources + cb
|
// overload #3: array of multiple sources + cb
|
||||||
@ -89,11 +92,11 @@ export function watch<T, Lazy extends Readonly<boolean> = false>(
|
|||||||
// of all possible value types.
|
// of all possible value types.
|
||||||
export function watch<
|
export function watch<
|
||||||
T extends Readonly<WatchSource<unknown>[]>,
|
T extends Readonly<WatchSource<unknown>[]>,
|
||||||
Lazy extends Readonly<boolean> = false
|
Immediate extends Readonly<boolean> = false
|
||||||
>(
|
>(
|
||||||
sources: T,
|
sources: T,
|
||||||
cb: WatchCallback<MapSources<T>, MapOldSources<T, Lazy>>,
|
cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>,
|
||||||
options?: WatchOptions<Lazy>
|
options?: WatchOptions<Immediate>
|
||||||
): StopHandle
|
): StopHandle
|
||||||
|
|
||||||
// implementation
|
// implementation
|
||||||
@ -102,15 +105,11 @@ export function watch<T = any>(
|
|||||||
cbOrOptions?: WatchCallback<T> | WatchOptions,
|
cbOrOptions?: WatchCallback<T> | WatchOptions,
|
||||||
options?: WatchOptions
|
options?: WatchOptions
|
||||||
): StopHandle {
|
): StopHandle {
|
||||||
if (isInSSRComponentSetup && !(options && options.flush === 'sync')) {
|
if (isFunction(cbOrOptions)) {
|
||||||
// component watchers during SSR are no-op
|
// watch(source, cb)
|
||||||
return NOOP
|
|
||||||
} else if (isFunction(cbOrOptions)) {
|
|
||||||
// effect callback as 2nd argument - this is a source watcher
|
|
||||||
return doWatch(effectOrSource, cbOrOptions, options)
|
return doWatch(effectOrSource, cbOrOptions, options)
|
||||||
} else {
|
} else {
|
||||||
// 2nd argument is either missing or an options object
|
// watch(effect)
|
||||||
// - this is a simple effect watcher
|
|
||||||
return doWatch(effectOrSource, null, cbOrOptions)
|
return doWatch(effectOrSource, null, cbOrOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,8 +117,23 @@ export function watch<T = any>(
|
|||||||
function doWatch(
|
function doWatch(
|
||||||
source: WatchSource | WatchSource[] | WatchEffect,
|
source: WatchSource | WatchSource[] | WatchEffect,
|
||||||
cb: WatchCallback | null,
|
cb: WatchCallback | null,
|
||||||
{ lazy, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
|
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
|
||||||
): StopHandle {
|
): StopHandle {
|
||||||
|
if (__DEV__ && !cb) {
|
||||||
|
if (immediate !== undefined) {
|
||||||
|
warn(
|
||||||
|
`watch() "immediate" option is only respected when using the ` +
|
||||||
|
`watch(source, callback) signature.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (deep !== undefined) {
|
||||||
|
warn(
|
||||||
|
`watch() "deep" option is only respected when using the ` +
|
||||||
|
`watch(source, callback) signature.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const instance = currentInstance
|
const instance = currentInstance
|
||||||
const suspense = currentSuspense
|
const suspense = currentSuspense
|
||||||
|
|
||||||
@ -168,6 +182,21 @@ function doWatch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// in SSR there is no need to setup an actual effect, and it should be noop
|
||||||
|
// unless it's eager
|
||||||
|
if (__NODE_JS__ && isInSSRComponentSetup) {
|
||||||
|
if (!cb) {
|
||||||
|
getter()
|
||||||
|
} else if (immediate) {
|
||||||
|
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
|
||||||
|
getter(),
|
||||||
|
undefined,
|
||||||
|
registerCleanup
|
||||||
|
])
|
||||||
|
}
|
||||||
|
return NOOP
|
||||||
|
}
|
||||||
|
|
||||||
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
|
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
|
||||||
const applyCb = cb
|
const applyCb = cb
|
||||||
? () => {
|
? () => {
|
||||||
@ -219,23 +248,19 @@ function doWatch(
|
|||||||
scheduler: applyCb ? () => scheduler(applyCb) : scheduler
|
scheduler: applyCb ? () => scheduler(applyCb) : scheduler
|
||||||
})
|
})
|
||||||
|
|
||||||
if (lazy && cb) {
|
recordInstanceBoundEffect(runner)
|
||||||
oldValue = runner()
|
|
||||||
} else {
|
// initial run
|
||||||
if (__DEV__ && lazy && !cb) {
|
|
||||||
warn(
|
|
||||||
`watch() lazy option is only respected when using the ` +
|
|
||||||
`watch(getter, callback) signature.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (applyCb) {
|
if (applyCb) {
|
||||||
scheduler(applyCb)
|
if (immediate) {
|
||||||
|
applyCb()
|
||||||
} else {
|
} else {
|
||||||
scheduler(runner)
|
oldValue = runner()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
runner()
|
||||||
}
|
}
|
||||||
|
|
||||||
recordInstanceBoundEffect(runner)
|
|
||||||
return () => {
|
return () => {
|
||||||
stop(runner)
|
stop(runner)
|
||||||
if (instance) {
|
if (instance) {
|
||||||
|
@ -135,8 +135,7 @@ const KeepAliveImpl = {
|
|||||||
([include, exclude]) => {
|
([include, exclude]) => {
|
||||||
include && pruneCache(name => matches(include, name))
|
include && pruneCache(name => matches(include, name))
|
||||||
exclude && pruneCache(name => matches(exclude, name))
|
exclude && pruneCache(name => matches(exclude, name))
|
||||||
},
|
}
|
||||||
{ lazy: true }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
@ -25,22 +25,39 @@ watch([source, source2, source3] as const, (values, oldValues) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// lazy watcher will have consistent types for oldValue.
|
// lazy watcher will have consistent types for oldValue.
|
||||||
|
watch(source, (value, oldValue) => {
|
||||||
|
expectType<string>(value)
|
||||||
|
expectType<string>(oldValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([source, source2, source3], (values, oldValues) => {
|
||||||
|
expectType<(string | number)[]>(values)
|
||||||
|
expectType<(string | number)[]>(oldValues)
|
||||||
|
})
|
||||||
|
|
||||||
|
// const array
|
||||||
|
watch([source, source2, source3] as const, (values, oldValues) => {
|
||||||
|
expectType<Readonly<[string, string, number]>>(values)
|
||||||
|
expectType<Readonly<[string, string, number]>>(oldValues)
|
||||||
|
})
|
||||||
|
|
||||||
|
// source + immediate: true
|
||||||
watch(
|
watch(
|
||||||
source,
|
source,
|
||||||
(value, oldValue) => {
|
(value, oldValue) => {
|
||||||
expectType<string>(value)
|
expectType<string>(value)
|
||||||
expectType<string>(oldValue)
|
expectType<string | undefined>(oldValue)
|
||||||
},
|
},
|
||||||
{ lazy: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[source, source2, source3],
|
[source, source2, source3],
|
||||||
(values, oldValues) => {
|
(values, oldValues) => {
|
||||||
expectType<(string | number)[]>(values)
|
expectType<(string | number)[]>(values)
|
||||||
expectType<(string | number)[]>(oldValues)
|
expectType<(string | number | undefined)[]>(oldValues)
|
||||||
},
|
},
|
||||||
{ lazy: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// const array
|
// const array
|
||||||
@ -48,7 +65,9 @@ watch(
|
|||||||
[source, source2, source3] as const,
|
[source, source2, source3] as const,
|
||||||
(values, oldValues) => {
|
(values, oldValues) => {
|
||||||
expectType<Readonly<[string, string, number]>>(values)
|
expectType<Readonly<[string, string, number]>>(values)
|
||||||
expectType<Readonly<[string, string, number]>>(oldValues)
|
expectType<
|
||||||
|
Readonly<[string | undefined, string | undefined, number | undefined]>
|
||||||
|
>(oldValues)
|
||||||
},
|
},
|
||||||
{ lazy: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user