fix(watch): flush:pre watchers should not fire if state change causes
owner component to unmount fix #2291
This commit is contained in:
parent
a95554d35c
commit
78c199d6db
@ -509,7 +509,8 @@ describe('api: watch', () => {
|
|||||||
expect(cb).not.toHaveBeenCalled()
|
expect(cb).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should fire on component unmount w/ flush: pre', async () => {
|
// #2291
|
||||||
|
it('should not fire on component unmount w/ flush: pre', async () => {
|
||||||
const toggle = ref(true)
|
const toggle = ref(true)
|
||||||
const cb = jest.fn()
|
const cb = jest.fn()
|
||||||
const Comp = {
|
const Comp = {
|
||||||
@ -527,7 +528,7 @@ describe('api: watch', () => {
|
|||||||
expect(cb).not.toHaveBeenCalled()
|
expect(cb).not.toHaveBeenCalled()
|
||||||
toggle.value = false
|
toggle.value = false
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(cb).toHaveBeenCalledTimes(1)
|
expect(cb).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
// #1763
|
// #1763
|
||||||
|
@ -3,9 +3,8 @@ import {
|
|||||||
nextTick,
|
nextTick,
|
||||||
queuePostFlushCb,
|
queuePostFlushCb,
|
||||||
invalidateJob,
|
invalidateJob,
|
||||||
queuePreFlushCb,
|
flushPostFlushCbs,
|
||||||
flushPreFlushCbs,
|
flushPreFlushCbs
|
||||||
flushPostFlushCbs
|
|
||||||
} from '../src/scheduler'
|
} from '../src/scheduler'
|
||||||
|
|
||||||
describe('scheduler', () => {
|
describe('scheduler', () => {
|
||||||
@ -114,65 +113,7 @@ describe('scheduler', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('queuePreFlushCb', () => {
|
describe('pre flush jobs', () => {
|
||||||
it('basic usage', async () => {
|
|
||||||
const calls: string[] = []
|
|
||||||
const cb1 = () => {
|
|
||||||
calls.push('cb1')
|
|
||||||
}
|
|
||||||
const cb2 = () => {
|
|
||||||
calls.push('cb2')
|
|
||||||
}
|
|
||||||
|
|
||||||
queuePreFlushCb(cb1)
|
|
||||||
queuePreFlushCb(cb2)
|
|
||||||
|
|
||||||
expect(calls).toEqual([])
|
|
||||||
await nextTick()
|
|
||||||
expect(calls).toEqual(['cb1', 'cb2'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should dedupe queued preFlushCb', async () => {
|
|
||||||
const calls: string[] = []
|
|
||||||
const cb1 = () => {
|
|
||||||
calls.push('cb1')
|
|
||||||
}
|
|
||||||
const cb2 = () => {
|
|
||||||
calls.push('cb2')
|
|
||||||
}
|
|
||||||
const cb3 = () => {
|
|
||||||
calls.push('cb3')
|
|
||||||
}
|
|
||||||
|
|
||||||
queuePreFlushCb(cb1)
|
|
||||||
queuePreFlushCb(cb2)
|
|
||||||
queuePreFlushCb(cb1)
|
|
||||||
queuePreFlushCb(cb2)
|
|
||||||
queuePreFlushCb(cb3)
|
|
||||||
|
|
||||||
expect(calls).toEqual([])
|
|
||||||
await nextTick()
|
|
||||||
expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('chained queuePreFlushCb', async () => {
|
|
||||||
const calls: string[] = []
|
|
||||||
const cb1 = () => {
|
|
||||||
calls.push('cb1')
|
|
||||||
// cb2 will be executed after cb1 at the same tick
|
|
||||||
queuePreFlushCb(cb2)
|
|
||||||
}
|
|
||||||
const cb2 = () => {
|
|
||||||
calls.push('cb2')
|
|
||||||
}
|
|
||||||
queuePreFlushCb(cb1)
|
|
||||||
|
|
||||||
await nextTick()
|
|
||||||
expect(calls).toEqual(['cb1', 'cb2'])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('queueJob w/ queuePreFlushCb', () => {
|
|
||||||
it('queueJob inside preFlushCb', async () => {
|
it('queueJob inside preFlushCb', async () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const job1 = () => {
|
const job1 = () => {
|
||||||
@ -183,8 +124,9 @@ describe('scheduler', () => {
|
|||||||
calls.push('cb1')
|
calls.push('cb1')
|
||||||
queueJob(job1)
|
queueJob(job1)
|
||||||
}
|
}
|
||||||
|
cb1.pre = true
|
||||||
|
|
||||||
queuePreFlushCb(cb1)
|
queueJob(cb1)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(calls).toEqual(['cb1', 'job1'])
|
expect(calls).toEqual(['cb1', 'job1'])
|
||||||
})
|
})
|
||||||
@ -194,17 +136,23 @@ describe('scheduler', () => {
|
|||||||
const job1 = () => {
|
const job1 = () => {
|
||||||
calls.push('job1')
|
calls.push('job1')
|
||||||
}
|
}
|
||||||
|
job1.id = 1
|
||||||
|
|
||||||
const cb1 = () => {
|
const cb1 = () => {
|
||||||
calls.push('cb1')
|
calls.push('cb1')
|
||||||
queueJob(job1)
|
queueJob(job1)
|
||||||
// cb2 should execute before the job
|
// cb2 should execute before the job
|
||||||
queuePreFlushCb(cb2)
|
queueJob(cb2)
|
||||||
}
|
}
|
||||||
|
cb1.pre = true
|
||||||
|
|
||||||
const cb2 = () => {
|
const cb2 = () => {
|
||||||
calls.push('cb2')
|
calls.push('cb2')
|
||||||
}
|
}
|
||||||
|
cb2.pre = true
|
||||||
|
cb2.id = 1
|
||||||
|
|
||||||
queuePreFlushCb(cb1)
|
queueJob(cb1)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(calls).toEqual(['cb1', 'cb2', 'job1'])
|
expect(calls).toEqual(['cb1', 'cb2', 'job1'])
|
||||||
})
|
})
|
||||||
@ -216,9 +164,9 @@ describe('scheduler', () => {
|
|||||||
// when updating the props of a child component. This is handled
|
// when updating the props of a child component. This is handled
|
||||||
// directly inside `updateComponentPreRender` to avoid non atomic
|
// directly inside `updateComponentPreRender` to avoid non atomic
|
||||||
// cb triggers (#1763)
|
// cb triggers (#1763)
|
||||||
queuePreFlushCb(cb1)
|
queueJob(cb1)
|
||||||
queuePreFlushCb(cb2)
|
queueJob(cb2)
|
||||||
flushPreFlushCbs(undefined, job1)
|
flushPreFlushCbs()
|
||||||
calls.push('job1')
|
calls.push('job1')
|
||||||
}
|
}
|
||||||
const cb1 = () => {
|
const cb1 = () => {
|
||||||
@ -226,9 +174,11 @@ describe('scheduler', () => {
|
|||||||
// a cb triggers its parent job, which should be skipped
|
// a cb triggers its parent job, which should be skipped
|
||||||
queueJob(job1)
|
queueJob(job1)
|
||||||
}
|
}
|
||||||
|
cb1.pre = true
|
||||||
const cb2 = () => {
|
const cb2 = () => {
|
||||||
calls.push('cb2')
|
calls.push('cb2')
|
||||||
}
|
}
|
||||||
|
cb2.pre = true
|
||||||
|
|
||||||
queueJob(job1)
|
queueJob(job1)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@ -237,12 +187,14 @@ describe('scheduler', () => {
|
|||||||
|
|
||||||
// #3806
|
// #3806
|
||||||
it('queue preFlushCb inside postFlushCb', async () => {
|
it('queue preFlushCb inside postFlushCb', async () => {
|
||||||
const cb = jest.fn()
|
const spy = jest.fn()
|
||||||
|
const cb = () => spy()
|
||||||
|
cb.pre = true
|
||||||
queuePostFlushCb(() => {
|
queuePostFlushCb(() => {
|
||||||
queuePreFlushCb(cb)
|
queueJob(cb)
|
||||||
})
|
})
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(cb).toHaveBeenCalled()
|
expect(spy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
EffectScheduler,
|
EffectScheduler,
|
||||||
DebuggerOptions
|
DebuggerOptions
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import { SchedulerJob, queuePreFlushCb } from './scheduler'
|
import { SchedulerJob, queueJob } from './scheduler'
|
||||||
import {
|
import {
|
||||||
EMPTY_OBJ,
|
EMPTY_OBJ,
|
||||||
isObject,
|
isObject,
|
||||||
@ -345,7 +345,9 @@ function doWatch(
|
|||||||
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
|
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
|
||||||
} else {
|
} else {
|
||||||
// default: 'pre'
|
// default: 'pre'
|
||||||
scheduler = () => queuePreFlushCb(job)
|
job.pre = true
|
||||||
|
if (instance) job.id = instance.uid
|
||||||
|
scheduler = () => queueJob(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
const effect = new ReactiveEffect(getter, scheduler)
|
const effect = new ReactiveEffect(getter, scheduler)
|
||||||
|
@ -1590,7 +1590,7 @@ function baseCreateRenderer(
|
|||||||
pauseTracking()
|
pauseTracking()
|
||||||
// props update may have triggered pre-flush watchers.
|
// props update may have triggered pre-flush watchers.
|
||||||
// flush them before the render update.
|
// flush them before the render update.
|
||||||
flushPreFlushCbs(undefined, instance.update)
|
flushPreFlushCbs()
|
||||||
resetTracking()
|
resetTracking()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2331,6 +2331,7 @@ function baseCreateRenderer(
|
|||||||
} else {
|
} else {
|
||||||
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
|
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
|
||||||
}
|
}
|
||||||
|
flushPreFlushCbs()
|
||||||
flushPostFlushCbs()
|
flushPostFlushCbs()
|
||||||
container._vnode = vnode
|
container._vnode = vnode
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { warn } from './warning'
|
|||||||
|
|
||||||
export interface SchedulerJob extends Function {
|
export interface SchedulerJob extends Function {
|
||||||
id?: number
|
id?: number
|
||||||
|
pre?: boolean
|
||||||
active?: boolean
|
active?: boolean
|
||||||
computed?: boolean
|
computed?: boolean
|
||||||
/**
|
/**
|
||||||
@ -39,10 +40,6 @@ let isFlushPending = false
|
|||||||
const queue: SchedulerJob[] = []
|
const queue: SchedulerJob[] = []
|
||||||
let flushIndex = 0
|
let flushIndex = 0
|
||||||
|
|
||||||
const pendingPreFlushCbs: SchedulerJob[] = []
|
|
||||||
let activePreFlushCbs: SchedulerJob[] | null = null
|
|
||||||
let preFlushIndex = 0
|
|
||||||
|
|
||||||
const pendingPostFlushCbs: SchedulerJob[] = []
|
const pendingPostFlushCbs: SchedulerJob[] = []
|
||||||
let activePostFlushCbs: SchedulerJob[] | null = null
|
let activePostFlushCbs: SchedulerJob[] | null = null
|
||||||
let postFlushIndex = 0
|
let postFlushIndex = 0
|
||||||
@ -50,8 +47,6 @@ let postFlushIndex = 0
|
|||||||
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
|
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
|
||||||
let currentFlushPromise: Promise<void> | null = null
|
let currentFlushPromise: Promise<void> | null = null
|
||||||
|
|
||||||
let currentPreFlushParentJob: SchedulerJob | null = null
|
|
||||||
|
|
||||||
const RECURSION_LIMIT = 100
|
const RECURSION_LIMIT = 100
|
||||||
type CountMap = Map<SchedulerJob, number>
|
type CountMap = Map<SchedulerJob, number>
|
||||||
|
|
||||||
@ -89,12 +84,11 @@ export function queueJob(job: SchedulerJob) {
|
|||||||
// allow it recursively trigger itself - it is the user's responsibility to
|
// allow it recursively trigger itself - it is the user's responsibility to
|
||||||
// ensure it doesn't end up in an infinite loop.
|
// ensure it doesn't end up in an infinite loop.
|
||||||
if (
|
if (
|
||||||
(!queue.length ||
|
!queue.length ||
|
||||||
!queue.includes(
|
!queue.includes(
|
||||||
job,
|
job,
|
||||||
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
|
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
|
||||||
)) &&
|
)
|
||||||
job !== currentPreFlushParentJob
|
|
||||||
) {
|
) {
|
||||||
if (job.id == null) {
|
if (job.id == null) {
|
||||||
queue.push(job)
|
queue.push(job)
|
||||||
@ -119,71 +113,44 @@ export function invalidateJob(job: SchedulerJob) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueCb(
|
export function queuePostFlushCb(cb: SchedulerJobs) {
|
||||||
cb: SchedulerJobs,
|
|
||||||
activeQueue: SchedulerJob[] | null,
|
|
||||||
pendingQueue: SchedulerJob[],
|
|
||||||
index: number
|
|
||||||
) {
|
|
||||||
if (!isArray(cb)) {
|
if (!isArray(cb)) {
|
||||||
if (
|
if (
|
||||||
!activeQueue ||
|
!activePostFlushCbs ||
|
||||||
!activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
|
!activePostFlushCbs.includes(
|
||||||
|
cb,
|
||||||
|
cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
pendingQueue.push(cb)
|
pendingPostFlushCbs.push(cb)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if cb is an array, it is a component lifecycle hook which can only be
|
// if cb is an array, it is a component lifecycle hook which can only be
|
||||||
// triggered by a job, which is already deduped in the main queue, so
|
// triggered by a job, which is already deduped in the main queue, so
|
||||||
// we can skip duplicate check here to improve perf
|
// we can skip duplicate check here to improve perf
|
||||||
pendingQueue.push(...cb)
|
pendingPostFlushCbs.push(...cb)
|
||||||
}
|
}
|
||||||
queueFlush()
|
queueFlush()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queuePreFlushCb(cb: SchedulerJob) {
|
export function flushPreFlushCbs(seen?: CountMap, i = flushIndex) {
|
||||||
queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
|
if (__DEV__) {
|
||||||
}
|
seen = seen || new Map()
|
||||||
|
}
|
||||||
export function queuePostFlushCb(cb: SchedulerJobs) {
|
for (; i < queue.length; i++) {
|
||||||
queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
|
const cb = queue[i]
|
||||||
}
|
if (cb && cb.pre) {
|
||||||
|
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
|
||||||
export function flushPreFlushCbs(
|
|
||||||
seen?: CountMap,
|
|
||||||
parentJob: SchedulerJob | null = null
|
|
||||||
) {
|
|
||||||
if (pendingPreFlushCbs.length) {
|
|
||||||
currentPreFlushParentJob = parentJob
|
|
||||||
activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
|
|
||||||
pendingPreFlushCbs.length = 0
|
|
||||||
if (__DEV__) {
|
|
||||||
seen = seen || new Map()
|
|
||||||
}
|
|
||||||
for (
|
|
||||||
preFlushIndex = 0;
|
|
||||||
preFlushIndex < activePreFlushCbs.length;
|
|
||||||
preFlushIndex++
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
__DEV__ &&
|
|
||||||
checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
|
|
||||||
) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
activePreFlushCbs[preFlushIndex]()
|
queue.splice(i, 1)
|
||||||
|
i--
|
||||||
|
cb()
|
||||||
}
|
}
|
||||||
activePreFlushCbs = null
|
|
||||||
preFlushIndex = 0
|
|
||||||
currentPreFlushParentJob = null
|
|
||||||
// recursively flush until it drains
|
|
||||||
flushPreFlushCbs(seen, parentJob)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flushPostFlushCbs(seen?: CountMap) {
|
export function flushPostFlushCbs(seen?: CountMap) {
|
||||||
// flush any pre cbs queued during the flush (e.g. pre watchers)
|
|
||||||
flushPreFlushCbs()
|
|
||||||
if (pendingPostFlushCbs.length) {
|
if (pendingPostFlushCbs.length) {
|
||||||
const deduped = [...new Set(pendingPostFlushCbs)]
|
const deduped = [...new Set(pendingPostFlushCbs)]
|
||||||
pendingPostFlushCbs.length = 0
|
pendingPostFlushCbs.length = 0
|
||||||
@ -222,6 +189,15 @@ export function flushPostFlushCbs(seen?: CountMap) {
|
|||||||
const getId = (job: SchedulerJob): number =>
|
const getId = (job: SchedulerJob): number =>
|
||||||
job.id == null ? Infinity : job.id
|
job.id == null ? Infinity : job.id
|
||||||
|
|
||||||
|
const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
|
||||||
|
const diff = getId(a) - getId(b)
|
||||||
|
if (diff === 0) {
|
||||||
|
if (a.pre && !b.pre) return -1
|
||||||
|
if (b.pre && !a.pre) return 1
|
||||||
|
}
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
function flushJobs(seen?: CountMap) {
|
function flushJobs(seen?: CountMap) {
|
||||||
isFlushPending = false
|
isFlushPending = false
|
||||||
isFlushing = true
|
isFlushing = true
|
||||||
@ -229,8 +205,6 @@ function flushJobs(seen?: CountMap) {
|
|||||||
seen = seen || new Map()
|
seen = seen || new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
flushPreFlushCbs(seen)
|
|
||||||
|
|
||||||
// Sort queue before flush.
|
// Sort queue before flush.
|
||||||
// This ensures that:
|
// This ensures that:
|
||||||
// 1. Components are updated from parent to child. (because parent is always
|
// 1. Components are updated from parent to child. (because parent is always
|
||||||
@ -238,7 +212,7 @@ function flushJobs(seen?: CountMap) {
|
|||||||
// priority number)
|
// priority number)
|
||||||
// 2. If a component is unmounted during a parent component's update,
|
// 2. If a component is unmounted during a parent component's update,
|
||||||
// its update can be skipped.
|
// its update can be skipped.
|
||||||
queue.sort((a, b) => getId(a) - getId(b))
|
queue.sort(comparator)
|
||||||
|
|
||||||
// conditional usage of checkRecursiveUpdate must be determined out of
|
// conditional usage of checkRecursiveUpdate must be determined out of
|
||||||
// try ... catch block since Rollup by default de-optimizes treeshaking
|
// try ... catch block since Rollup by default de-optimizes treeshaking
|
||||||
@ -270,11 +244,7 @@ function flushJobs(seen?: CountMap) {
|
|||||||
currentFlushPromise = null
|
currentFlushPromise = null
|
||||||
// some postFlushCb queued jobs!
|
// some postFlushCb queued jobs!
|
||||||
// keep flushing until it drains.
|
// keep flushing until it drains.
|
||||||
if (
|
if (queue.length || pendingPostFlushCbs.length) {
|
||||||
queue.length ||
|
|
||||||
pendingPreFlushCbs.length ||
|
|
||||||
pendingPostFlushCbs.length
|
|
||||||
) {
|
|
||||||
flushJobs(seen)
|
flushJobs(seen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user