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:
Evan You 2020-02-17 23:14:19 -05:00
parent d9d63f21b1
commit 9571ede84b
7 changed files with 176 additions and 143 deletions

View File

@ -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', () => {

View File

@ -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()

View File

@ -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 () => {

View File

@ -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')
}) })

View File

@ -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) {

View File

@ -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(() => {

View File

@ -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 }
) )