fix(watch): post flush watchers should not fire when component is unmounted

fix #1603
This commit is contained in:
Evan You
2020-07-17 11:17:29 -04:00
parent 024a8f10f5
commit 341b30c961
4 changed files with 100 additions and 39 deletions

View File

@@ -260,12 +260,13 @@ describe('api: watch', () => {
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}`
expect(serializeInner(root)).toBe(expectedDOM)
result = serializeInner(root) === expectedDOM
})
const Comp = {
@@ -279,10 +280,12 @@ describe('api: watch', () => {
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 () => {
@@ -290,16 +293,18 @@ describe('api: watch', () => {
const count2 = ref(0)
let callCount = 0
let result1
let result2
const assertion = jest.fn((count, count2Value) => {
callCount++
// on mount, the watcher callback should be called before DOM render
// on update, should be called before the count is updated
const expectedDOM = callCount === 1 ? `` : `${count - 1}`
expect(serializeInner(root)).toBe(expectedDOM)
result1 = serializeInner(root) === expectedDOM
// in a pre-flush callback, all state should have been updated
const expectedState = callCount === 1 ? 0 : 1
expect(count2Value).toBe(expectedState)
const expectedState = callCount - 1
result2 = count === expectedState && count2Value === expectedState
})
const Comp = {
@@ -318,12 +323,16 @@ describe('api: watch', () => {
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(assertion).toHaveBeenCalledTimes(1)
expect(result1).toBe(true)
expect(result2).toBe(true)
count.value++
count2.value++
await nextTick()
// two mutations should result in 1 callback execution
expect(assertion).toHaveBeenCalledTimes(2)
expect(result1).toBe(true)
expect(result2).toBe(true)
})
it('flush timing: sync', async () => {
@@ -331,17 +340,19 @@ describe('api: watch', () => {
const count2 = ref(0)
let callCount = 0
let result1
let result2
const assertion = jest.fn(count => {
callCount++
// on mount, the watcher callback should be called before DOM render
// on update, should be called before the count is updated
const expectedDOM = callCount === 1 ? `` : `${count - 1}`
expect(serializeInner(root)).toBe(expectedDOM)
result1 = serializeInner(root) === expectedDOM
// in a sync callback, state mutation on the next line should not have
// executed yet on the 2nd call, but will be on the 3rd call.
const expectedState = callCount < 3 ? 0 : 1
expect(count2.value).toBe(expectedState)
result2 = count2.value === expectedState
})
const Comp = {
@@ -360,11 +371,57 @@ describe('api: watch', () => {
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(assertion).toHaveBeenCalledTimes(1)
expect(result1).toBe(true)
expect(result2).toBe(true)
count.value++
count2.value++
await nextTick()
expect(assertion).toHaveBeenCalledTimes(3)
expect(result1).toBe(true)
expect(result2).toBe(true)
})
it('should not fire on component unmount w/ flush: post', async () => {
const toggle = ref(true)
const cb = jest.fn()
const Comp = {
setup() {
watch(toggle, cb)
},
render() {}
}
const App = {
render() {
return toggle.value ? h(Comp) : null
}
}
render(h(App), nodeOps.createElement('div'))
expect(cb).not.toHaveBeenCalled()
toggle.value = false
await nextTick()
expect(cb).not.toHaveBeenCalled()
})
it('should fire on component unmount w/ flush: pre', async () => {
const toggle = ref(true)
const cb = jest.fn()
const Comp = {
setup() {
watch(toggle, cb, { flush: 'pre' })
},
render() {}
}
const App = {
render() {
return toggle.value ? h(Comp) : null
}
}
render(h(App), nodeOps.createElement('div'))
expect(cb).not.toHaveBeenCalled()
toggle.value = false
await nextTick()
expect(cb).toHaveBeenCalledTimes(1)
})
it('deep', async () => {

View File

@@ -170,7 +170,7 @@ describe('Suspense', () => {
})
const count = ref(0)
watch(count, v => {
watch(count, () => {
calls.push('watch callback')
})
count.value++ // trigger the watcher now
@@ -367,7 +367,7 @@ describe('Suspense', () => {
await nextTick()
expect(serializeInner(root)).toBe(`<!---->`)
// should discard effects (except for immediate ones)
expect(calls).toEqual(['immediate effect', 'watch callback', 'unmounted'])
expect(calls).toEqual(['immediate effect', 'unmounted'])
})
test('unmount suspense after resolve', async () => {