refactor(runtime-core/scheduler): dedicated preFlush queue
properly fix #1763, #1777, #1781
This commit is contained in:
parent
74a1265fea
commit
3692f2738f
@ -2,7 +2,9 @@ import {
|
|||||||
queueJob,
|
queueJob,
|
||||||
nextTick,
|
nextTick,
|
||||||
queuePostFlushCb,
|
queuePostFlushCb,
|
||||||
invalidateJob
|
invalidateJob,
|
||||||
|
queuePreFlushCb,
|
||||||
|
flushPreFlushCbs
|
||||||
} from '../src/scheduler'
|
} from '../src/scheduler'
|
||||||
|
|
||||||
describe('scheduler', () => {
|
describe('scheduler', () => {
|
||||||
@ -75,6 +77,128 @@ describe('scheduler', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('queuePreFlushCb', () => {
|
||||||
|
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 () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const job1 = () => {
|
||||||
|
calls.push('job1')
|
||||||
|
}
|
||||||
|
const cb1 = () => {
|
||||||
|
// queueJob in postFlushCb
|
||||||
|
calls.push('cb1')
|
||||||
|
queueJob(job1)
|
||||||
|
}
|
||||||
|
|
||||||
|
queuePreFlushCb(cb1)
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual(['cb1', 'job1'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('queueJob & preFlushCb inside preFlushCb', async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const job1 = () => {
|
||||||
|
calls.push('job1')
|
||||||
|
}
|
||||||
|
const cb1 = () => {
|
||||||
|
calls.push('cb1')
|
||||||
|
queueJob(job1)
|
||||||
|
// cb2 should execute before the job
|
||||||
|
queuePreFlushCb(cb2)
|
||||||
|
}
|
||||||
|
const cb2 = () => {
|
||||||
|
calls.push('cb2')
|
||||||
|
}
|
||||||
|
|
||||||
|
queuePreFlushCb(cb1)
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual(['cb1', 'cb2', 'job1'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preFlushCb inside queueJob', async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const job1 = () => {
|
||||||
|
// the only case where a pre-flush cb can be queued inside a job is
|
||||||
|
// when updating the props of a child component. This is handled
|
||||||
|
// directly inside `updateComponentPreRender` to avoid non atomic
|
||||||
|
// cb triggers (#1763)
|
||||||
|
queuePreFlushCb(cb1)
|
||||||
|
queuePreFlushCb(cb2)
|
||||||
|
flushPreFlushCbs(undefined, job1)
|
||||||
|
calls.push('job1')
|
||||||
|
}
|
||||||
|
const cb1 = () => {
|
||||||
|
calls.push('cb1')
|
||||||
|
// a cb triggers its parent job, which should be skipped
|
||||||
|
queueJob(job1)
|
||||||
|
}
|
||||||
|
const cb2 = () => {
|
||||||
|
calls.push('cb2')
|
||||||
|
}
|
||||||
|
|
||||||
|
queueJob(job1)
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual(['cb1', 'cb2', 'job1'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('queuePostFlushCb', () => {
|
describe('queuePostFlushCb', () => {
|
||||||
it('basic usage', async () => {
|
it('basic usage', async () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
ReactiveEffectOptions,
|
ReactiveEffectOptions,
|
||||||
isReactive
|
isReactive
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import { queueJob, SchedulerJob } from './scheduler'
|
import { SchedulerJob, queuePreFlushCb } from './scheduler'
|
||||||
import {
|
import {
|
||||||
EMPTY_OBJ,
|
EMPTY_OBJ,
|
||||||
isObject,
|
isObject,
|
||||||
@ -271,7 +271,7 @@ function doWatch(
|
|||||||
job.id = -1
|
job.id = -1
|
||||||
scheduler = () => {
|
scheduler = () => {
|
||||||
if (!instance || instance.isMounted) {
|
if (!instance || instance.isMounted) {
|
||||||
queueJob(job)
|
queuePreFlushCb(job)
|
||||||
} else {
|
} else {
|
||||||
// with 'pre' option, the first call must happen before
|
// with 'pre' option, the first call must happen before
|
||||||
// the component is mounted so it is called synchronously.
|
// the component is mounted so it is called synchronously.
|
||||||
|
@ -41,7 +41,7 @@ import {
|
|||||||
queuePostFlushCb,
|
queuePostFlushCb,
|
||||||
flushPostFlushCbs,
|
flushPostFlushCbs,
|
||||||
invalidateJob,
|
invalidateJob,
|
||||||
runPreflushJobs
|
flushPreFlushCbs
|
||||||
} from './scheduler'
|
} from './scheduler'
|
||||||
import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
|
import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
|
||||||
import { updateProps } from './componentProps'
|
import { updateProps } from './componentProps'
|
||||||
@ -1430,7 +1430,10 @@ function baseCreateRenderer(
|
|||||||
instance.next = null
|
instance.next = null
|
||||||
updateProps(instance, nextVNode.props, prevProps, optimized)
|
updateProps(instance, nextVNode.props, prevProps, optimized)
|
||||||
updateSlots(instance, nextVNode.children)
|
updateSlots(instance, nextVNode.children)
|
||||||
runPreflushJobs(instance.update)
|
|
||||||
|
// props update may have triggered pre-flush watchers.
|
||||||
|
// flush them before the render update.
|
||||||
|
flushPreFlushCbs(undefined, instance.update)
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchChildren: PatchChildrenFn = (
|
const patchChildren: PatchChildrenFn = (
|
||||||
|
@ -16,17 +16,23 @@ export interface SchedulerJob {
|
|||||||
cb?: boolean
|
cb?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isFlushing = false
|
||||||
|
let isFlushPending = false
|
||||||
|
|
||||||
const queue: (SchedulerJob | null)[] = []
|
const queue: (SchedulerJob | null)[] = []
|
||||||
const postFlushCbs: Function[] = []
|
let flushIndex = 0
|
||||||
|
|
||||||
|
const pendingPreFlushCbs: Function[] = []
|
||||||
|
let activePreFlushCbs: Function[] | null = null
|
||||||
|
let preFlushIndex = 0
|
||||||
|
|
||||||
|
const pendingPostFlushCbs: Function[] = []
|
||||||
|
let activePostFlushCbs: Function[] | null = null
|
||||||
|
let postFlushIndex = 0
|
||||||
|
|
||||||
const resolvedPromise: Promise<any> = Promise.resolve()
|
const resolvedPromise: Promise<any> = Promise.resolve()
|
||||||
let currentFlushPromise: Promise<void> | null = null
|
let currentFlushPromise: Promise<void> | null = null
|
||||||
|
|
||||||
let isFlushing = false
|
|
||||||
let isFlushPending = false
|
|
||||||
let flushIndex = 0
|
|
||||||
let pendingPostFlushCbs: Function[] | null = null
|
|
||||||
let pendingPostFlushIndex = 0
|
|
||||||
let hasPendingPreFlushJobs = false
|
|
||||||
let currentPreFlushParentJob: SchedulerJob | null = null
|
let currentPreFlushParentJob: SchedulerJob | null = null
|
||||||
|
|
||||||
const RECURSION_LIMIT = 100
|
const RECURSION_LIMIT = 100
|
||||||
@ -53,11 +59,17 @@ export function queueJob(job: SchedulerJob) {
|
|||||||
job !== currentPreFlushParentJob
|
job !== currentPreFlushParentJob
|
||||||
) {
|
) {
|
||||||
queue.push(job)
|
queue.push(job)
|
||||||
if ((job.id as number) < 0) hasPendingPreFlushJobs = true
|
|
||||||
queueFlush()
|
queueFlush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function queueFlush() {
|
||||||
|
if (!isFlushing && !isFlushPending) {
|
||||||
|
isFlushPending = true
|
||||||
|
currentFlushPromise = resolvedPromise.then(flushJobs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function invalidateJob(job: SchedulerJob) {
|
export function invalidateJob(job: SchedulerJob) {
|
||||||
const i = queue.indexOf(job)
|
const i = queue.indexOf(job)
|
||||||
if (i > -1) {
|
if (i > -1) {
|
||||||
@ -65,78 +77,84 @@ export function invalidateJob(job: SchedulerJob) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function queueCb(
|
||||||
* Run flush: 'pre' watcher callbacks. This is only called in
|
cb: Function | Function[],
|
||||||
* `updateComponentPreRender` to cover the case where pre-flush watchers are
|
activeQueue: Function[] | null,
|
||||||
* triggered by the change of a component's props. This means the scheduler is
|
pendingQueue: Function[],
|
||||||
* already flushing and we are already inside the component's update effect,
|
index: number
|
||||||
* right when the render function is about to be called. So if the watcher
|
) {
|
||||||
* triggers the same component to update, we don't want it to be queued (this
|
|
||||||
* is checked via `currentPreFlushParentJob`).
|
|
||||||
*/
|
|
||||||
export function runPreflushJobs(parentJob: SchedulerJob) {
|
|
||||||
if (hasPendingPreFlushJobs) {
|
|
||||||
currentPreFlushParentJob = parentJob
|
|
||||||
hasPendingPreFlushJobs = false
|
|
||||||
for (let job, i = flushIndex + 1; i < queue.length; i++) {
|
|
||||||
job = queue[i]
|
|
||||||
if (job && (job.id as number) < 0) {
|
|
||||||
job()
|
|
||||||
queue[i] = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentPreFlushParentJob = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queuePostFlushCb(cb: Function | Function[]) {
|
|
||||||
if (!isArray(cb)) {
|
if (!isArray(cb)) {
|
||||||
if (
|
if (
|
||||||
!pendingPostFlushCbs ||
|
!activeQueue ||
|
||||||
!pendingPostFlushCbs.includes(
|
!activeQueue.includes(cb, (cb as SchedulerJob).cb ? index + 1 : index)
|
||||||
cb,
|
|
||||||
(cb as SchedulerJob).cb
|
|
||||||
? pendingPostFlushIndex + 1
|
|
||||||
: pendingPostFlushIndex
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
postFlushCbs.push(cb)
|
pendingQueue.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 dupicate check here to improve perf
|
// we can skip dupicate check here to improve perf
|
||||||
postFlushCbs.push(...cb)
|
pendingQueue.push(...cb)
|
||||||
}
|
}
|
||||||
queueFlush()
|
queueFlush()
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueFlush() {
|
export function queuePreFlushCb(cb: Function) {
|
||||||
if (!isFlushing && !isFlushPending) {
|
queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
|
||||||
isFlushPending = true
|
|
||||||
currentFlushPromise = resolvedPromise.then(flushJobs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flushPostFlushCbs(seen?: CountMap) {
|
export function queuePostFlushCb(cb: Function | Function[]) {
|
||||||
if (postFlushCbs.length) {
|
queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
|
||||||
pendingPostFlushCbs = [...new Set(postFlushCbs)]
|
}
|
||||||
postFlushCbs.length = 0
|
|
||||||
|
export function flushPreFlushCbs(
|
||||||
|
seen?: CountMap,
|
||||||
|
parentJob: SchedulerJob | null = null
|
||||||
|
) {
|
||||||
|
if (pendingPreFlushCbs.length) {
|
||||||
|
currentPreFlushParentJob = parentJob
|
||||||
|
activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
|
||||||
|
pendingPreFlushCbs.length = 0
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
seen = seen || new Map()
|
seen = seen || new Map()
|
||||||
}
|
}
|
||||||
for (
|
for (
|
||||||
pendingPostFlushIndex = 0;
|
preFlushIndex = 0;
|
||||||
pendingPostFlushIndex < pendingPostFlushCbs.length;
|
preFlushIndex < activePreFlushCbs.length;
|
||||||
pendingPostFlushIndex++
|
preFlushIndex++
|
||||||
) {
|
) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
checkRecursiveUpdates(seen!, pendingPostFlushCbs[pendingPostFlushIndex])
|
checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
|
||||||
}
|
}
|
||||||
pendingPostFlushCbs[pendingPostFlushIndex]()
|
activePreFlushCbs[preFlushIndex]()
|
||||||
}
|
}
|
||||||
pendingPostFlushCbs = null
|
activePreFlushCbs = null
|
||||||
pendingPostFlushIndex = 0
|
preFlushIndex = 0
|
||||||
|
currentPreFlushParentJob = null
|
||||||
|
// recursively flush until it drains
|
||||||
|
flushPreFlushCbs(seen, parentJob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flushPostFlushCbs(seen?: CountMap) {
|
||||||
|
if (pendingPostFlushCbs.length) {
|
||||||
|
activePostFlushCbs = [...new Set(pendingPostFlushCbs)]
|
||||||
|
pendingPostFlushCbs.length = 0
|
||||||
|
if (__DEV__) {
|
||||||
|
seen = seen || new Map()
|
||||||
|
}
|
||||||
|
for (
|
||||||
|
postFlushIndex = 0;
|
||||||
|
postFlushIndex < activePostFlushCbs.length;
|
||||||
|
postFlushIndex++
|
||||||
|
) {
|
||||||
|
if (__DEV__) {
|
||||||
|
checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
|
||||||
|
}
|
||||||
|
activePostFlushCbs[postFlushIndex]()
|
||||||
|
}
|
||||||
|
activePostFlushCbs = null
|
||||||
|
postFlushIndex = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +167,8 @@ 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
|
||||||
@ -175,11 +195,12 @@ function flushJobs(seen?: CountMap) {
|
|||||||
queue.length = 0
|
queue.length = 0
|
||||||
|
|
||||||
flushPostFlushCbs(seen)
|
flushPostFlushCbs(seen)
|
||||||
|
|
||||||
isFlushing = false
|
isFlushing = false
|
||||||
currentFlushPromise = null
|
currentFlushPromise = null
|
||||||
// some postFlushCb queued jobs!
|
// some postFlushCb queued jobs!
|
||||||
// keep flushing until it drains.
|
// keep flushing until it drains.
|
||||||
if (queue.length || postFlushCbs.length) {
|
if (queue.length || pendingPostFlushCbs.length) {
|
||||||
flushJobs(seen)
|
flushJobs(seen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user