refactor: watch APIs default to trigger pre-flush
BREAKING CHANGE: watch APIs now default to use `flush: 'pre'` instead of `flush: 'post'`. - This change affects `watch`, `watchEffect`, the `watch` component option, and `this.$watch`. - As pointed out by @skirtles-code in [this comment](https://github.com/vuejs/vue-next/issues/1706#issuecomment-666258948), Vue 2's watch behavior is pre-flush, and the ecosystem has many uses of watch that assumes the pre-flush behavior. Defaulting to post-flush can result in unnecessary re-renders without the users being aware of it. - With this change, watchers need to specify `{ flush: 'post' }` via options to trigger callback after Vue render updates. Note that specifying `{ flush: 'post' }` will also defer `watchEffect`'s initial run to wait for the component's initial render.
This commit is contained in:
parent
58c31e3699
commit
49bb44756f
@ -280,38 +280,7 @@ describe('api: watch', () => {
|
|||||||
expect(cleanup).toHaveBeenCalledTimes(2)
|
expect(cleanup).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('flush timing: post (default)', async () => {
|
it('flush timing: pre (default)', async () => {
|
||||||
const count = ref(0)
|
|
||||||
let callCount = 0
|
|
||||||
let result
|
|
||||||
const assertion = jest.fn(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}`
|
|
||||||
result = serializeInner(root) === expectedDOM
|
|
||||||
})
|
|
||||||
|
|
||||||
const Comp = {
|
|
||||||
setup() {
|
|
||||||
watchEffect(() => {
|
|
||||||
assertion(count.value)
|
|
||||||
})
|
|
||||||
return () => count.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const root = nodeOps.createElement('div')
|
|
||||||
render(h(Comp), root)
|
|
||||||
expect(assertion).toHaveBeenCalledTimes(1)
|
|
||||||
expect(result).toBe(true)
|
|
||||||
|
|
||||||
count.value++
|
|
||||||
await nextTick()
|
|
||||||
expect(assertion).toHaveBeenCalledTimes(2)
|
|
||||||
expect(result).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('flush timing: pre', async () => {
|
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
const count2 = ref(0)
|
const count2 = ref(0)
|
||||||
|
|
||||||
@ -332,14 +301,9 @@ describe('api: watch', () => {
|
|||||||
|
|
||||||
const Comp = {
|
const Comp = {
|
||||||
setup() {
|
setup() {
|
||||||
watchEffect(
|
watchEffect(() => {
|
||||||
() => {
|
|
||||||
assertion(count.value, count2.value)
|
assertion(count.value, count2.value)
|
||||||
},
|
})
|
||||||
{
|
|
||||||
flush: 'pre'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return () => count.value
|
return () => count.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -358,6 +322,35 @@ describe('api: watch', () => {
|
|||||||
expect(result2).toBe(true)
|
expect(result2).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('flush timing: post', async () => {
|
||||||
|
const count = ref(0)
|
||||||
|
let result
|
||||||
|
const assertion = jest.fn(count => {
|
||||||
|
result = serializeInner(root) === `${count}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const Comp = {
|
||||||
|
setup() {
|
||||||
|
watchEffect(
|
||||||
|
() => {
|
||||||
|
assertion(count.value)
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
)
|
||||||
|
return () => count.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Comp), root)
|
||||||
|
expect(assertion).toHaveBeenCalledTimes(1)
|
||||||
|
expect(result).toBe(true)
|
||||||
|
|
||||||
|
count.value++
|
||||||
|
await nextTick()
|
||||||
|
expect(assertion).toHaveBeenCalledTimes(2)
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('flush timing: sync', async () => {
|
it('flush timing: sync', async () => {
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
const count2 = ref(0)
|
const count2 = ref(0)
|
||||||
@ -410,7 +403,7 @@ describe('api: watch', () => {
|
|||||||
const cb = jest.fn()
|
const cb = jest.fn()
|
||||||
const Comp = {
|
const Comp = {
|
||||||
setup() {
|
setup() {
|
||||||
watch(toggle, cb)
|
watch(toggle, cb, { flush: 'post' })
|
||||||
},
|
},
|
||||||
render() {}
|
render() {}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ describe('Suspense', () => {
|
|||||||
expect(onResolve).toHaveBeenCalled()
|
expect(onResolve).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('buffer mounted/updated hooks & watch callbacks', async () => {
|
test('buffer mounted/updated hooks & post flush watch callbacks', async () => {
|
||||||
const deps: Promise<any>[] = []
|
const deps: Promise<any>[] = []
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const toggle = ref(true)
|
const toggle = ref(true)
|
||||||
@ -165,14 +165,21 @@ describe('Suspense', () => {
|
|||||||
// extra tick needed for Node 12+
|
// extra tick needed for Node 12+
|
||||||
deps.push(p.then(() => Promise.resolve()))
|
deps.push(p.then(() => Promise.resolve()))
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(
|
||||||
calls.push('immediate effect')
|
() => {
|
||||||
})
|
calls.push('watch effect')
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
)
|
||||||
|
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
watch(count, () => {
|
watch(
|
||||||
|
count,
|
||||||
|
() => {
|
||||||
calls.push('watch callback')
|
calls.push('watch callback')
|
||||||
})
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
)
|
||||||
count.value++ // trigger the watcher now
|
count.value++ // trigger the watcher now
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -201,12 +208,12 @@ 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([`immediate effect`])
|
expect(calls).toEqual([])
|
||||||
|
|
||||||
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([`immediate effect`, `watch callback`, `mounted`])
|
expect(calls).toEqual([`watch 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
|
||||||
@ -214,7 +221,7 @@ describe('Suspense', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
expect(serializeInner(root)).toBe(`<!---->`)
|
expect(serializeInner(root)).toBe(`<!---->`)
|
||||||
expect(calls).toEqual([
|
expect(calls).toEqual([
|
||||||
`immediate effect`,
|
`watch effect`,
|
||||||
`watch callback`,
|
`watch callback`,
|
||||||
`mounted`,
|
`mounted`,
|
||||||
'unmounted'
|
'unmounted'
|
||||||
@ -319,14 +326,21 @@ describe('Suspense', () => {
|
|||||||
const p = new Promise(r => setTimeout(r, 1))
|
const p = new Promise(r => setTimeout(r, 1))
|
||||||
deps.push(p)
|
deps.push(p)
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(
|
||||||
calls.push('immediate effect')
|
() => {
|
||||||
})
|
calls.push('watch effect')
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
)
|
||||||
|
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
watch(count, () => {
|
watch(
|
||||||
|
count,
|
||||||
|
() => {
|
||||||
calls.push('watch callback')
|
calls.push('watch callback')
|
||||||
})
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
)
|
||||||
count.value++ // trigger the watcher now
|
count.value++ // trigger the watcher now
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -355,7 +369,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(['immediate effect'])
|
expect(calls).toEqual([])
|
||||||
|
|
||||||
// remove the async dep before it's resolved
|
// remove the async dep before it's resolved
|
||||||
toggle.value = false
|
toggle.value = false
|
||||||
@ -366,8 +380,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 (except for immediate ones)
|
// should discard effects (except for unmount)
|
||||||
expect(calls).toEqual(['immediate effect', 'unmounted'])
|
expect(calls).toEqual(['unmounted'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('unmount suspense after resolve', async () => {
|
test('unmount suspense after resolve', async () => {
|
||||||
|
@ -268,9 +268,10 @@ function doWatch(
|
|||||||
let scheduler: (job: () => any) => void
|
let scheduler: (job: () => any) => void
|
||||||
if (flush === 'sync') {
|
if (flush === 'sync') {
|
||||||
scheduler = job
|
scheduler = job
|
||||||
} else if (flush === 'pre') {
|
} else if (flush === 'post') {
|
||||||
// ensure it's queued before component updates (which have positive ids)
|
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
|
||||||
job.id = -1
|
} else {
|
||||||
|
// default: 'pre'
|
||||||
scheduler = () => {
|
scheduler = () => {
|
||||||
if (!instance || instance.isMounted) {
|
if (!instance || instance.isMounted) {
|
||||||
queuePreFlushCb(job)
|
queuePreFlushCb(job)
|
||||||
@ -280,8 +281,6 @@ function doWatch(
|
|||||||
job()
|
job()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const runner = effect(getter, {
|
const runner = effect(getter, {
|
||||||
@ -300,6 +299,8 @@ function doWatch(
|
|||||||
} else {
|
} else {
|
||||||
oldValue = runner()
|
oldValue = runner()
|
||||||
}
|
}
|
||||||
|
} else if (flush === 'post') {
|
||||||
|
queuePostRenderEffect(runner, instance && instance.suspense)
|
||||||
} else {
|
} else {
|
||||||
runner()
|
runner()
|
||||||
}
|
}
|
||||||
|
@ -171,15 +171,18 @@ const KeepAliveImpl = {
|
|||||||
keys.delete(key)
|
keys.delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prune cache on include/exclude prop change
|
||||||
watch(
|
watch(
|
||||||
() => [props.include, props.exclude],
|
() => [props.include, props.exclude],
|
||||||
([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))
|
||||||
}
|
},
|
||||||
|
// prune post-render after `current` has been updated
|
||||||
|
{ flush: 'post' }
|
||||||
)
|
)
|
||||||
|
|
||||||
// cache sub tree in beforeMount/Update (i.e. right after the render)
|
// cache sub tree after render
|
||||||
let pendingCacheKey: CacheKey | null = null
|
let pendingCacheKey: CacheKey | null = null
|
||||||
const cacheSubtree = () => {
|
const cacheSubtree = () => {
|
||||||
// fix #1621, the pendingCacheKey could be 0
|
// fix #1621, the pendingCacheKey could be 0
|
||||||
|
Loading…
Reference in New Issue
Block a user