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:
Evan You
2020-09-17 23:17:21 -04:00
parent 58c31e3699
commit 49bb44756f
4 changed files with 78 additions and 67 deletions

View File

@@ -154,7 +154,7 @@ describe('Suspense', () => {
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 calls: string[] = []
const toggle = ref(true)
@@ -165,14 +165,21 @@ describe('Suspense', () => {
// extra tick needed for Node 12+
deps.push(p.then(() => Promise.resolve()))
watchEffect(() => {
calls.push('immediate effect')
})
watchEffect(
() => {
calls.push('watch effect')
},
{ flush: 'post' }
)
const count = ref(0)
watch(count, () => {
calls.push('watch callback')
})
watch(
count,
() => {
calls.push('watch callback')
},
{ flush: 'post' }
)
count.value++ // trigger the watcher now
onMounted(() => {
@@ -201,12 +208,12 @@ describe('Suspense', () => {
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
expect(calls).toEqual([`immediate effect`])
expect(calls).toEqual([])
await Promise.all(deps)
await nextTick()
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
toggle.value = false
@@ -214,7 +221,7 @@ describe('Suspense', () => {
await nextTick()
expect(serializeInner(root)).toBe(`<!---->`)
expect(calls).toEqual([
`immediate effect`,
`watch effect`,
`watch callback`,
`mounted`,
'unmounted'
@@ -319,14 +326,21 @@ describe('Suspense', () => {
const p = new Promise(r => setTimeout(r, 1))
deps.push(p)
watchEffect(() => {
calls.push('immediate effect')
})
watchEffect(
() => {
calls.push('watch effect')
},
{ flush: 'post' }
)
const count = ref(0)
watch(count, () => {
calls.push('watch callback')
})
watch(
count,
() => {
calls.push('watch callback')
},
{ flush: 'post' }
)
count.value++ // trigger the watcher now
onMounted(() => {
@@ -355,7 +369,7 @@ describe('Suspense', () => {
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
expect(calls).toEqual(['immediate effect'])
expect(calls).toEqual([])
// remove the async dep before it's resolved
toggle.value = false
@@ -366,8 +380,8 @@ describe('Suspense', () => {
await Promise.all(deps)
await nextTick()
expect(serializeInner(root)).toBe(`<!---->`)
// should discard effects (except for immediate ones)
expect(calls).toEqual(['immediate effect', 'unmounted'])
// should discard effects (except for unmount)
expect(calls).toEqual(['unmounted'])
})
test('unmount suspense after resolve', async () => {