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

@@ -280,38 +280,7 @@ describe('api: watch', () => {
expect(cleanup).toHaveBeenCalledTimes(2)
})
it('flush timing: post (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 () => {
it('flush timing: pre (default)', async () => {
const count = ref(0)
const count2 = ref(0)
@@ -332,14 +301,9 @@ describe('api: watch', () => {
const Comp = {
setup() {
watchEffect(
() => {
assertion(count.value, count2.value)
},
{
flush: 'pre'
}
)
watchEffect(() => {
assertion(count.value, count2.value)
})
return () => count.value
}
}
@@ -358,6 +322,35 @@ describe('api: watch', () => {
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 () => {
const count = ref(0)
const count2 = ref(0)
@@ -410,7 +403,7 @@ describe('api: watch', () => {
const cb = jest.fn()
const Comp = {
setup() {
watch(toggle, cb)
watch(toggle, cb, { flush: 'post' })
},
render() {}
}

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