fix(watch): post flush watchers should not fire when component is unmounted
fix #1603
This commit is contained in:
parent
024a8f10f5
commit
341b30c961
@ -260,12 +260,13 @@ describe('api: watch', () => {
|
|||||||
it('flush timing: post (default)', async () => {
|
it('flush timing: post (default)', async () => {
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
let callCount = 0
|
let callCount = 0
|
||||||
|
let result
|
||||||
const assertion = jest.fn(count => {
|
const assertion = jest.fn(count => {
|
||||||
callCount++
|
callCount++
|
||||||
// on mount, the watcher callback should be called before DOM render
|
// on mount, the watcher callback should be called before DOM render
|
||||||
// on update, should be called after the count is updated
|
// on update, should be called after the count is updated
|
||||||
const expectedDOM = callCount === 1 ? `` : `${count}`
|
const expectedDOM = callCount === 1 ? `` : `${count}`
|
||||||
expect(serializeInner(root)).toBe(expectedDOM)
|
result = serializeInner(root) === expectedDOM
|
||||||
})
|
})
|
||||||
|
|
||||||
const Comp = {
|
const Comp = {
|
||||||
@ -279,10 +280,12 @@ describe('api: watch', () => {
|
|||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
expect(assertion).toHaveBeenCalledTimes(1)
|
expect(assertion).toHaveBeenCalledTimes(1)
|
||||||
|
expect(result).toBe(true)
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(assertion).toHaveBeenCalledTimes(2)
|
expect(assertion).toHaveBeenCalledTimes(2)
|
||||||
|
expect(result).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('flush timing: pre', async () => {
|
it('flush timing: pre', async () => {
|
||||||
@ -290,16 +293,18 @@ describe('api: watch', () => {
|
|||||||
const count2 = ref(0)
|
const count2 = ref(0)
|
||||||
|
|
||||||
let callCount = 0
|
let callCount = 0
|
||||||
|
let result1
|
||||||
|
let result2
|
||||||
const assertion = jest.fn((count, count2Value) => {
|
const assertion = jest.fn((count, count2Value) => {
|
||||||
callCount++
|
callCount++
|
||||||
// on mount, the watcher callback should be called before DOM render
|
// on mount, the watcher callback should be called before DOM render
|
||||||
// on update, should be called before the count is updated
|
// on update, should be called before the count is updated
|
||||||
const expectedDOM = callCount === 1 ? `` : `${count - 1}`
|
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
|
// in a pre-flush callback, all state should have been updated
|
||||||
const expectedState = callCount === 1 ? 0 : 1
|
const expectedState = callCount - 1
|
||||||
expect(count2Value).toBe(expectedState)
|
result2 = count === expectedState && count2Value === expectedState
|
||||||
})
|
})
|
||||||
|
|
||||||
const Comp = {
|
const Comp = {
|
||||||
@ -318,12 +323,16 @@ describe('api: watch', () => {
|
|||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
expect(assertion).toHaveBeenCalledTimes(1)
|
expect(assertion).toHaveBeenCalledTimes(1)
|
||||||
|
expect(result1).toBe(true)
|
||||||
|
expect(result2).toBe(true)
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
count2.value++
|
count2.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
// two mutations should result in 1 callback execution
|
// two mutations should result in 1 callback execution
|
||||||
expect(assertion).toHaveBeenCalledTimes(2)
|
expect(assertion).toHaveBeenCalledTimes(2)
|
||||||
|
expect(result1).toBe(true)
|
||||||
|
expect(result2).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('flush timing: sync', async () => {
|
it('flush timing: sync', async () => {
|
||||||
@ -331,17 +340,19 @@ describe('api: watch', () => {
|
|||||||
const count2 = ref(0)
|
const count2 = ref(0)
|
||||||
|
|
||||||
let callCount = 0
|
let callCount = 0
|
||||||
|
let result1
|
||||||
|
let result2
|
||||||
const assertion = jest.fn(count => {
|
const assertion = jest.fn(count => {
|
||||||
callCount++
|
callCount++
|
||||||
// on mount, the watcher callback should be called before DOM render
|
// on mount, the watcher callback should be called before DOM render
|
||||||
// on update, should be called before the count is updated
|
// on update, should be called before the count is updated
|
||||||
const expectedDOM = callCount === 1 ? `` : `${count - 1}`
|
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
|
// 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.
|
// executed yet on the 2nd call, but will be on the 3rd call.
|
||||||
const expectedState = callCount < 3 ? 0 : 1
|
const expectedState = callCount < 3 ? 0 : 1
|
||||||
expect(count2.value).toBe(expectedState)
|
result2 = count2.value === expectedState
|
||||||
})
|
})
|
||||||
|
|
||||||
const Comp = {
|
const Comp = {
|
||||||
@ -360,11 +371,57 @@ describe('api: watch', () => {
|
|||||||
const root = nodeOps.createElement('div')
|
const root = nodeOps.createElement('div')
|
||||||
render(h(Comp), root)
|
render(h(Comp), root)
|
||||||
expect(assertion).toHaveBeenCalledTimes(1)
|
expect(assertion).toHaveBeenCalledTimes(1)
|
||||||
|
expect(result1).toBe(true)
|
||||||
|
expect(result2).toBe(true)
|
||||||
|
|
||||||
count.value++
|
count.value++
|
||||||
count2.value++
|
count2.value++
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(assertion).toHaveBeenCalledTimes(3)
|
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 () => {
|
it('deep', async () => {
|
||||||
|
@ -170,7 +170,7 @@ describe('Suspense', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
watch(count, v => {
|
watch(count, () => {
|
||||||
calls.push('watch callback')
|
calls.push('watch callback')
|
||||||
})
|
})
|
||||||
count.value++ // trigger the watcher now
|
count.value++ // trigger the watcher now
|
||||||
@ -367,7 +367,7 @@ describe('Suspense', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
expect(serializeInner(root)).toBe(`<!---->`)
|
expect(serializeInner(root)).toBe(`<!---->`)
|
||||||
// should discard effects (except for immediate ones)
|
// 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 () => {
|
test('unmount suspense after resolve', async () => {
|
||||||
|
@ -234,11 +234,12 @@ function doWatch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
|
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
|
||||||
const applyCb = cb
|
const job = () => {
|
||||||
? () => {
|
if (!runner.active) {
|
||||||
if (instance && instance.isUnmounted) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (cb) {
|
||||||
|
// watch(source, cb)
|
||||||
const newValue = runner()
|
const newValue = runner()
|
||||||
if (deep || hasChanged(newValue, oldValue)) {
|
if (deep || hasChanged(newValue, oldValue)) {
|
||||||
// cleanup before running cb again
|
// cleanup before running cb again
|
||||||
@ -253,14 +254,19 @@ function doWatch(
|
|||||||
])
|
])
|
||||||
oldValue = newValue
|
oldValue = newValue
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// watchEffect
|
||||||
|
runner()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
: void 0
|
|
||||||
|
|
||||||
let scheduler: (job: () => any) => void
|
let scheduler: (job: () => any) => void
|
||||||
if (flush === 'sync') {
|
if (flush === 'sync') {
|
||||||
scheduler = invoke
|
scheduler = invoke
|
||||||
} else if (flush === 'pre') {
|
} else if (flush === 'pre') {
|
||||||
scheduler = job => {
|
// ensure it's queued before component updates (which have positive ids)
|
||||||
|
job.id = -1
|
||||||
|
scheduler = () => {
|
||||||
if (!instance || instance.isMounted) {
|
if (!instance || instance.isMounted) {
|
||||||
queueJob(job)
|
queueJob(job)
|
||||||
} else {
|
} else {
|
||||||
@ -270,22 +276,22 @@ function doWatch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
|
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
|
||||||
}
|
}
|
||||||
|
|
||||||
const runner = effect(getter, {
|
const runner = effect(getter, {
|
||||||
lazy: true,
|
lazy: true,
|
||||||
onTrack,
|
onTrack,
|
||||||
onTrigger,
|
onTrigger,
|
||||||
scheduler: applyCb ? () => scheduler(applyCb) : scheduler
|
scheduler
|
||||||
})
|
})
|
||||||
|
|
||||||
recordInstanceBoundEffect(runner)
|
recordInstanceBoundEffect(runner)
|
||||||
|
|
||||||
// initial run
|
// initial run
|
||||||
if (applyCb) {
|
if (cb) {
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
applyCb()
|
job()
|
||||||
} else {
|
} else {
|
||||||
oldValue = runner()
|
oldValue = runner()
|
||||||
}
|
}
|
||||||
|
@ -2025,19 +2025,17 @@ function baseCreateRenderer(
|
|||||||
if (bum) {
|
if (bum) {
|
||||||
invokeArrayFns(bum)
|
invokeArrayFns(bum)
|
||||||
}
|
}
|
||||||
|
if (effects) {
|
||||||
|
for (let i = 0; i < effects.length; i++) {
|
||||||
|
stop(effects[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
// update may be null if a component is unmounted before its async
|
// update may be null if a component is unmounted before its async
|
||||||
// setup has resolved.
|
// setup has resolved.
|
||||||
if (update) {
|
if (update) {
|
||||||
stop(update)
|
stop(update)
|
||||||
unmount(subTree, instance, parentSuspense, doRemove)
|
unmount(subTree, instance, parentSuspense, doRemove)
|
||||||
}
|
}
|
||||||
if (effects) {
|
|
||||||
queuePostRenderEffect(() => {
|
|
||||||
for (let i = 0; i < effects.length; i++) {
|
|
||||||
stop(effects[i])
|
|
||||||
}
|
|
||||||
}, parentSuspense)
|
|
||||||
}
|
|
||||||
// unmounted hook
|
// unmounted hook
|
||||||
if (um) {
|
if (um) {
|
||||||
queuePostRenderEffect(um, parentSuspense)
|
queuePostRenderEffect(um, parentSuspense)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user