refactor: document new scheduler
This commit is contained in:
parent
cb7ad12ed4
commit
2f3ddf20b5
@ -9,8 +9,8 @@ import {
|
|||||||
queueJob,
|
queueJob,
|
||||||
handleSchedulerError,
|
handleSchedulerError,
|
||||||
nextTick,
|
nextTick,
|
||||||
queuePostCommitCb,
|
queueEffect,
|
||||||
flushPostCommitCbs,
|
flushEffects,
|
||||||
queueNodeOp
|
queueNodeOp
|
||||||
} from '@vue/scheduler'
|
} from '@vue/scheduler'
|
||||||
import { VNodeFlags, ChildrenFlags } from './flags'
|
import { VNodeFlags, ChildrenFlags } from './flags'
|
||||||
@ -188,12 +188,12 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
insertOrAppend(container, el, endNode)
|
insertOrAppend(container, el, endNode)
|
||||||
}
|
}
|
||||||
if (ref) {
|
if (ref) {
|
||||||
queuePostCommitCb(() => {
|
queueEffect(() => {
|
||||||
ref(el)
|
ref(el)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (data != null && data.vnodeMounted) {
|
if (data != null && data.vnodeMounted) {
|
||||||
queuePostCommitCb(() => {
|
queueEffect(() => {
|
||||||
data.vnodeMounted(vnode)
|
data.vnodeMounted(vnode)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -268,7 +268,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
const subTree = (handle.prevTree = vnode.children = renderFunctionalRoot(
|
const subTree = (handle.prevTree = vnode.children = renderFunctionalRoot(
|
||||||
vnode
|
vnode
|
||||||
))
|
))
|
||||||
queuePostCommitCb(() => {
|
queueEffect(() => {
|
||||||
vnode.el = subTree.el as RenderNode
|
vnode.el = subTree.el as RenderNode
|
||||||
})
|
})
|
||||||
mount(subTree, container, vnode as MountedVNode, isSVG, endNode)
|
mount(subTree, container, vnode as MountedVNode, isSVG, endNode)
|
||||||
@ -308,7 +308,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
const nextTree = (handle.prevTree = current.children = renderFunctionalRoot(
|
const nextTree = (handle.prevTree = current.children = renderFunctionalRoot(
|
||||||
current
|
current
|
||||||
))
|
))
|
||||||
queuePostCommitCb(() => {
|
queueEffect(() => {
|
||||||
current.el = nextTree.el
|
current.el = nextTree.el
|
||||||
})
|
})
|
||||||
patch(
|
patch(
|
||||||
@ -344,7 +344,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
const { children, childFlags } = vnode
|
const { children, childFlags } = vnode
|
||||||
switch (childFlags) {
|
switch (childFlags) {
|
||||||
case ChildrenFlags.SINGLE_VNODE:
|
case ChildrenFlags.SINGLE_VNODE:
|
||||||
queuePostCommitCb(() => {
|
queueEffect(() => {
|
||||||
vnode.el = (children as MountedVNode).el
|
vnode.el = (children as MountedVNode).el
|
||||||
})
|
})
|
||||||
mount(children as VNode, container, contextVNode, isSVG, endNode)
|
mount(children as VNode, container, contextVNode, isSVG, endNode)
|
||||||
@ -355,7 +355,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
vnode.el = placeholder.el
|
vnode.el = placeholder.el
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
queuePostCommitCb(() => {
|
queueEffect(() => {
|
||||||
vnode.el = (children as MountedVNode[])[0].el
|
vnode.el = (children as MountedVNode[])[0].el
|
||||||
})
|
})
|
||||||
mountArrayChildren(
|
mountArrayChildren(
|
||||||
@ -392,7 +392,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (ref) {
|
if (ref) {
|
||||||
queuePostCommitCb(() => {
|
queueEffect(() => {
|
||||||
ref(target)
|
ref(target)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -607,7 +607,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
// then retrieve its next sibling to use as the end node for patchChildren.
|
// then retrieve its next sibling to use as the end node for patchChildren.
|
||||||
const endNode = platformNextSibling(getVNodeLastEl(prevVNode))
|
const endNode = platformNextSibling(getVNodeLastEl(prevVNode))
|
||||||
const { childFlags, children } = nextVNode
|
const { childFlags, children } = nextVNode
|
||||||
queuePostCommitCb(() => {
|
queueEffect(() => {
|
||||||
switch (childFlags) {
|
switch (childFlags) {
|
||||||
case ChildrenFlags.SINGLE_VNODE:
|
case ChildrenFlags.SINGLE_VNODE:
|
||||||
nextVNode.el = (children as MountedVNode).el
|
nextVNode.el = (children as MountedVNode).el
|
||||||
@ -1280,7 +1280,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
|
|
||||||
instance.$vnode = renderInstanceRoot(instance) as MountedVNode
|
instance.$vnode = renderInstanceRoot(instance) as MountedVNode
|
||||||
|
|
||||||
queuePostCommitCb(() => {
|
queueEffect(() => {
|
||||||
vnode.el = instance.$vnode.el
|
vnode.el = instance.$vnode.el
|
||||||
if (__COMPAT__) {
|
if (__COMPAT__) {
|
||||||
// expose __vue__ for devtools
|
// expose __vue__ for devtools
|
||||||
@ -1337,7 +1337,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
|
|
||||||
const nextVNode = renderInstanceRoot(instance) as MountedVNode
|
const nextVNode = renderInstanceRoot(instance) as MountedVNode
|
||||||
|
|
||||||
queuePostCommitCb(() => {
|
queueEffect(() => {
|
||||||
instance.$vnode = nextVNode
|
instance.$vnode = nextVNode
|
||||||
const el = nextVNode.el as RenderNode
|
const el = nextVNode.el as RenderNode
|
||||||
if (__COMPAT__) {
|
if (__COMPAT__) {
|
||||||
@ -1426,7 +1426,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
popWarningContext()
|
popWarningContext()
|
||||||
}
|
}
|
||||||
queuePostCommitCb(() => {
|
queueEffect(() => {
|
||||||
callActivatedHook(instance, true)
|
callActivatedHook(instance, true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1510,7 +1510,7 @@ export function createRenderer(options: RendererOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (__COMPAT__) {
|
if (__COMPAT__) {
|
||||||
flushPostCommitCbs()
|
flushEffects()
|
||||||
return vnode && vnode.flags & VNodeFlags.COMPONENT_STATEFUL
|
return vnode && vnode.flags & VNodeFlags.COMPONENT_STATEFUL
|
||||||
? (vnode.children as ComponentInstance).$proxy
|
? (vnode.children as ComponentInstance).$proxy
|
||||||
: null
|
: null
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { queueJob, queuePostCommitCb, nextTick } from '../src/index'
|
import { queueJob, queueEffect, nextTick } from '../src/index'
|
||||||
|
|
||||||
describe('scheduler', () => {
|
describe('scheduler', () => {
|
||||||
it('queueJob', async () => {
|
it('queueJob', async () => {
|
||||||
@ -36,11 +36,11 @@ describe('scheduler', () => {
|
|||||||
const calls: any = []
|
const calls: any = []
|
||||||
const job1 = () => {
|
const job1 = () => {
|
||||||
calls.push('job1')
|
calls.push('job1')
|
||||||
queuePostCommitCb(cb1)
|
queueEffect(cb1)
|
||||||
}
|
}
|
||||||
const job2 = () => {
|
const job2 = () => {
|
||||||
calls.push('job2')
|
calls.push('job2')
|
||||||
queuePostCommitCb(cb2)
|
queueEffect(cb2)
|
||||||
}
|
}
|
||||||
const cb1 = () => {
|
const cb1 = () => {
|
||||||
calls.push('cb1')
|
calls.push('cb1')
|
||||||
@ -59,13 +59,13 @@ describe('scheduler', () => {
|
|||||||
const calls: any = []
|
const calls: any = []
|
||||||
const job1 = () => {
|
const job1 = () => {
|
||||||
calls.push('job1')
|
calls.push('job1')
|
||||||
queuePostCommitCb(cb1)
|
queueEffect(cb1)
|
||||||
// job1 queues job2
|
// job1 queues job2
|
||||||
queueJob(job2)
|
queueJob(job2)
|
||||||
}
|
}
|
||||||
const job2 = () => {
|
const job2 = () => {
|
||||||
calls.push('job2')
|
calls.push('job2')
|
||||||
queuePostCommitCb(cb2)
|
queueEffect(cb2)
|
||||||
}
|
}
|
||||||
const cb1 = () => {
|
const cb1 = () => {
|
||||||
calls.push('cb1')
|
calls.push('cb1')
|
||||||
@ -100,7 +100,7 @@ describe('scheduler', () => {
|
|||||||
const calls: any = []
|
const calls: any = []
|
||||||
const job1 = () => {
|
const job1 = () => {
|
||||||
calls.push('job1')
|
calls.push('job1')
|
||||||
queuePostCommitCb(cb1)
|
queueEffect(cb1)
|
||||||
}
|
}
|
||||||
const cb1 = () => {
|
const cb1 = () => {
|
||||||
// queue another job in postFlushCb
|
// queue another job in postFlushCb
|
||||||
@ -109,7 +109,7 @@ describe('scheduler', () => {
|
|||||||
}
|
}
|
||||||
const job2 = () => {
|
const job2 = () => {
|
||||||
calls.push('job2')
|
calls.push('job2')
|
||||||
queuePostCommitCb(cb2)
|
queueEffect(cb2)
|
||||||
}
|
}
|
||||||
const cb2 = () => {
|
const cb2 = () => {
|
||||||
calls.push('cb2')
|
calls.push('cb2')
|
||||||
|
@ -1,51 +1,150 @@
|
|||||||
// TODO infinite updates detection
|
// TODO infinite updates detection
|
||||||
|
|
||||||
|
// A data structure that stores a deferred DOM operation.
|
||||||
|
// the first element is the function to call, and the rest of the array
|
||||||
|
// stores up to 3 arguments.
|
||||||
type Op = [Function, ...any[]]
|
type Op = [Function, ...any[]]
|
||||||
|
|
||||||
const enum Priorities {
|
// A "job" stands for a unit of work that needs to be performed.
|
||||||
NORMAL = 500
|
// Typically, one job corresponds to the mounting or updating of one component
|
||||||
|
// instance (including functional ones).
|
||||||
|
interface Job<T extends Function = () => void> {
|
||||||
|
// A job is itself a function that performs work. It can contain work such as
|
||||||
|
// calling render functions, running the diff algorithm (patch), mounting new
|
||||||
|
// vnodes, and tearing down old vnodes. However, these work needs to be
|
||||||
|
// performed in several different phases, most importantly to separate
|
||||||
|
// workloads that do not produce side-effects ("stage") vs. those that do
|
||||||
|
// ("commit").
|
||||||
|
// During the stage call it should not perform any direct sife-effects.
|
||||||
|
// Instead, it buffers them. All side effects from multiple jobs queued in the
|
||||||
|
// same tick are flushed together during the "commit" phase. This allows us to
|
||||||
|
// perform side-effect-free work over multiple frames (yielding to the browser
|
||||||
|
// in-between to keep the app responsive), and only flush all the side effects
|
||||||
|
// together when all work is done (AKA time-slicing).
|
||||||
|
(): T
|
||||||
|
// A job's status changes over the different update phaes. See comments for
|
||||||
|
// phases below.
|
||||||
|
status: JobStatus
|
||||||
|
// Any operations performed by the job that directly mutates the DOM are
|
||||||
|
// buffered inside the job's ops queue, and only flushed in the commit phase.
|
||||||
|
// These ops are queued by calling `queueNodeOp` inside the job function.
|
||||||
|
ops: Op[]
|
||||||
|
// Any post DOM mutation side-effects (updated / mounted hooks, refs) are
|
||||||
|
// buffered inside the job's effects queue.
|
||||||
|
// Effects are queued by calling `queueEffect` inside the job function.
|
||||||
|
effects: Function[]
|
||||||
|
// A job may queue other jobs (e.g. a parent component update triggers the
|
||||||
|
// update of a child component). Jobs queued by another job is kept in the
|
||||||
|
// parent's children array, so that in case the parent job is invalidated,
|
||||||
|
// all its children can be invalidated as well (recursively).
|
||||||
|
children: Job[]
|
||||||
|
// Sometimes it's inevitable for a stage fn to produce some side effects
|
||||||
|
// (e.g. a component instance sets up an Autorun). In those cases the stage fn
|
||||||
|
// can return a cleanup function which will be called when the job is
|
||||||
|
// invalidated.
|
||||||
|
cleanup: T | null
|
||||||
|
// The expiration time is a timestamp past which the job needs to
|
||||||
|
// be force-committed regardless of frame budget.
|
||||||
|
// Why do we need an expiration time? Because a job may get invalidated before
|
||||||
|
// it is fully commited. If it keeps getting invalidated, we may "starve" the
|
||||||
|
// system and never apply any commits as jobs keep getting invalidated. The
|
||||||
|
// expiration time sets a limit on how long before a job can keep getting
|
||||||
|
// invalidated before it must be comitted.
|
||||||
|
expiration: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const enum JobStatus {
|
const enum JobStatus {
|
||||||
IDLE = 0,
|
IDLE = 0,
|
||||||
PENDING_PATCH,
|
PENDING_STAGE,
|
||||||
PENDING_COMMIT
|
PENDING_COMMIT
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Job extends Function {
|
// Priorities for different types of jobs. This number is added to the
|
||||||
status: JobStatus
|
// current time when a new job is queued to calculate the expiration time
|
||||||
ops: Op[]
|
// for that job.
|
||||||
post: Function[]
|
//
|
||||||
children: Job[]
|
// Currently we have only one type which expires 500ms after it is initially
|
||||||
cleanup: Function | null
|
// queued. There could be higher/lower priorities in the future.
|
||||||
expiration: number
|
const enum JobPriorities {
|
||||||
|
NORMAL = 500
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// There can be only one job being patched at one time. This allows us to
|
||||||
|
// automatically "capture" and buffer the node ops and post effects queued
|
||||||
|
// during a job.
|
||||||
|
let currentJob: Job | null = null
|
||||||
|
|
||||||
|
// Indicates we have a flush pending.
|
||||||
|
let hasPendingFlush = false
|
||||||
|
|
||||||
|
// A timestamp that indicates when a flush was started.
|
||||||
|
let flushStartTimestamp: number = 0
|
||||||
|
|
||||||
|
// The frame budget is the maximum amount of time passed while performing
|
||||||
|
// "stage" work before we need to yield back to the browser.
|
||||||
|
// Aiming for 60fps. Maybe we need to dynamically adjust this?
|
||||||
|
const frameBudget = __JSDOM__ ? Infinity : 1000 / 60
|
||||||
|
|
||||||
|
const getNow = () => performance.now()
|
||||||
|
|
||||||
|
// An entire update consists of 4 phases:
|
||||||
|
|
||||||
|
// 1. Stage phase. Render functions are called, diffs are performed, new
|
||||||
|
// component instances are created. However, no side-effects should be
|
||||||
|
// performed (i.e. no lifecycle hooks, no direct DOM operations).
|
||||||
|
const stageQueue: Job[] = []
|
||||||
|
|
||||||
|
// 2. Commit phase. This is only reached when the stageQueue has been depleted.
|
||||||
|
// Node ops are applied - in the browser, this means DOM is actually mutated
|
||||||
|
// during this phase. If a job is committed, it's post effects are then
|
||||||
|
// queued for the next phase.
|
||||||
|
const commitQueue: Job[] = []
|
||||||
|
|
||||||
|
// 3. Post-commit effects phase. Effect callbacks are only queued after a
|
||||||
|
// successful commit. These include callbacks that need to be invoked
|
||||||
|
// after DOM mutation - i.e. refs, mounted & updated hooks. This queue is
|
||||||
|
// flushed in reverse because child component effects are queued after but
|
||||||
|
// should be invoked before the parent's.
|
||||||
|
const postEffectsQueue: Function[] = []
|
||||||
|
|
||||||
|
// 4. NextTick phase. This is the user's catch-all mechanism for deferring
|
||||||
|
// work after a complete update cycle.
|
||||||
|
const nextTickQueue: Function[] = []
|
||||||
|
const pendingRejectors: ErrorHandler[] = []
|
||||||
|
|
||||||
|
// Error handling --------------------------------------------------------------
|
||||||
|
|
||||||
type ErrorHandler = (err: Error) => any
|
type ErrorHandler = (err: Error) => any
|
||||||
|
|
||||||
let currentJob: Job | null = null
|
|
||||||
|
|
||||||
let start: number = 0
|
|
||||||
const getNow = () => performance.now()
|
|
||||||
const frameBudget = __JSDOM__ ? Infinity : 1000 / 60
|
|
||||||
|
|
||||||
const patchQueue: Job[] = []
|
|
||||||
const commitQueue: Job[] = []
|
|
||||||
const postCommitQueue: Function[] = []
|
|
||||||
const nextTickQueue: Function[] = []
|
|
||||||
|
|
||||||
let globalHandler: ErrorHandler
|
let globalHandler: ErrorHandler
|
||||||
const pendingRejectors: ErrorHandler[] = []
|
|
||||||
|
|
||||||
// Microtask for batching state mutations
|
export function handleSchedulerError(handler: ErrorHandler) {
|
||||||
|
globalHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(err: Error) {
|
||||||
|
if (globalHandler) globalHandler(err)
|
||||||
|
pendingRejectors.forEach(handler => {
|
||||||
|
handler(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Microtask defer -------------------------------------------------------------
|
||||||
|
// For batching state mutations before we start an update. This does
|
||||||
|
// NOT yield to the browser.
|
||||||
|
|
||||||
const p = Promise.resolve()
|
const p = Promise.resolve()
|
||||||
|
|
||||||
function flushAfterMicroTask() {
|
function flushAfterMicroTask() {
|
||||||
start = getNow()
|
flushStartTimestamp = getNow()
|
||||||
return p.then(flush).catch(handleError)
|
return p.then(flush).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Macrotask for time slicing
|
// Macrotask defer -------------------------------------------------------------
|
||||||
|
// For time slicing. This uses the window postMessage event to "yield"
|
||||||
|
// to the browser so that other user events can trigger in between. This keeps
|
||||||
|
// the app responsive even when performing large amount of JavaScript work.
|
||||||
|
|
||||||
const key = `$vueTick`
|
const key = `$vueTick`
|
||||||
|
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
@ -54,7 +153,7 @@ window.addEventListener(
|
|||||||
if (event.source !== window || event.data !== key) {
|
if (event.source !== window || event.data !== key) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
start = getNow()
|
flushStartTimestamp = getNow()
|
||||||
try {
|
try {
|
||||||
flush()
|
flush()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -68,6 +167,65 @@ function flushAfterMacroTask() {
|
|||||||
window.postMessage(key, `*`)
|
window.postMessage(key, `*`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// This is the main API of the scheduler. The raw job can actually be any
|
||||||
|
// function, but since they are invalidated by identity, it is important that
|
||||||
|
// a component's update job is a consistent function across its lifecycle -
|
||||||
|
// in the renderer, it's actually instance._updateHandle which is in turn
|
||||||
|
// an Autorun function.
|
||||||
|
export function queueJob(rawJob: Function) {
|
||||||
|
const job = rawJob as Job
|
||||||
|
if (currentJob) {
|
||||||
|
currentJob.children.push(job)
|
||||||
|
}
|
||||||
|
// Let's see if this invalidates any work that
|
||||||
|
// has already been staged.
|
||||||
|
if (job.status === JobStatus.PENDING_COMMIT) {
|
||||||
|
// staged job invalidated
|
||||||
|
invalidateJob(job)
|
||||||
|
// re-insert it into the stage queue
|
||||||
|
requeueInvalidatedJob(job)
|
||||||
|
} else if (job.status !== JobStatus.PENDING_STAGE) {
|
||||||
|
// a new job
|
||||||
|
queueJobForStaging(job)
|
||||||
|
}
|
||||||
|
if (!hasPendingFlush) {
|
||||||
|
hasPendingFlush = true
|
||||||
|
flushAfterMicroTask()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queueEffect(fn: Function) {
|
||||||
|
if (currentJob) {
|
||||||
|
currentJob.effects.push(fn)
|
||||||
|
} else {
|
||||||
|
postEffectsQueue.push(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flushEffects() {
|
||||||
|
// post commit hooks (updated, mounted)
|
||||||
|
// this queue is flushed in reverse becuase these hooks should be invoked
|
||||||
|
// child first
|
||||||
|
let i = postEffectsQueue.length
|
||||||
|
while (i--) {
|
||||||
|
postEffectsQueue[i]()
|
||||||
|
}
|
||||||
|
postEffectsQueue.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queueNodeOp(op: Op) {
|
||||||
|
if (currentJob) {
|
||||||
|
currentJob.ops.push(op)
|
||||||
|
} else {
|
||||||
|
applyOp(op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The original nextTick now needs to be reworked so that the callback only
|
||||||
|
// triggers after the next commit, when all node ops and post effects have been
|
||||||
|
// completed.
|
||||||
export function nextTick<T>(fn?: () => T): Promise<T> {
|
export function nextTick<T>(fn?: () => T): Promise<T> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
p.then(() => {
|
p.then(() => {
|
||||||
@ -86,108 +244,35 @@ export function nextTick<T>(fn?: () => T): Promise<T> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleError(err: Error) {
|
// Internals -------------------------------------------------------------------
|
||||||
if (globalHandler) globalHandler(err)
|
|
||||||
pendingRejectors.forEach(handler => {
|
|
||||||
handler(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleSchedulerError(handler: ErrorHandler) {
|
|
||||||
globalHandler = handler
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasPendingFlush = false
|
|
||||||
|
|
||||||
export function queueJob(rawJob: Function) {
|
|
||||||
const job = rawJob as Job
|
|
||||||
if (currentJob) {
|
|
||||||
currentJob.children.push(job)
|
|
||||||
}
|
|
||||||
// 1. let's see if this invalidates any work that
|
|
||||||
// has already been done.
|
|
||||||
if (job.status === JobStatus.PENDING_COMMIT) {
|
|
||||||
// pending commit job invalidated
|
|
||||||
invalidateJob(job)
|
|
||||||
requeueInvalidatedJob(job)
|
|
||||||
} else if (job.status !== JobStatus.PENDING_PATCH) {
|
|
||||||
// a new job
|
|
||||||
insertNewJob(job)
|
|
||||||
}
|
|
||||||
if (!hasPendingFlush) {
|
|
||||||
hasPendingFlush = true
|
|
||||||
flushAfterMicroTask()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function requeueInvalidatedJob(job: Job) {
|
|
||||||
// With varying priorities we should insert job at correct position
|
|
||||||
// based on expiration time.
|
|
||||||
for (let i = 0; i < patchQueue.length; i++) {
|
|
||||||
if (job.expiration < patchQueue[i].expiration) {
|
|
||||||
patchQueue.splice(i, 0, job)
|
|
||||||
job.status = JobStatus.PENDING_PATCH
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
patchQueue.push(job)
|
|
||||||
job.status = JobStatus.PENDING_PATCH
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queuePostCommitCb(fn: Function) {
|
|
||||||
if (currentJob) {
|
|
||||||
currentJob.post.push(fn)
|
|
||||||
} else {
|
|
||||||
postCommitQueue.push(fn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function flushPostCommitCbs() {
|
|
||||||
// post commit hooks (updated, mounted)
|
|
||||||
// this queue is flushed in reverse becuase these hooks should be invoked
|
|
||||||
// child first
|
|
||||||
let i = postCommitQueue.length
|
|
||||||
while (i--) {
|
|
||||||
postCommitQueue[i]()
|
|
||||||
}
|
|
||||||
postCommitQueue.length = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queueNodeOp(op: Op) {
|
|
||||||
if (currentJob) {
|
|
||||||
currentJob.ops.push(op)
|
|
||||||
} else {
|
|
||||||
applyOp(op)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function flush(): void {
|
function flush(): void {
|
||||||
let job
|
let job
|
||||||
while (true) {
|
while (true) {
|
||||||
job = patchQueue.shift()
|
job = stageQueue.shift()
|
||||||
if (job) {
|
if (job) {
|
||||||
patchJob(job)
|
stageJob(job)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (!__COMPAT__) {
|
if (!__COMPAT__) {
|
||||||
const now = getNow()
|
const now = getNow()
|
||||||
if (now - start > frameBudget && job.expiration > now) {
|
if (now - flushStartTimestamp > frameBudget && job.expiration > now) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patchQueue.length === 0) {
|
if (stageQueue.length === 0) {
|
||||||
// all done, time to commit!
|
// all done, time to commit!
|
||||||
for (let i = 0; i < commitQueue.length; i++) {
|
for (let i = 0; i < commitQueue.length; i++) {
|
||||||
commitJob(commitQueue[i])
|
commitJob(commitQueue[i])
|
||||||
}
|
}
|
||||||
commitQueue.length = 0
|
commitQueue.length = 0
|
||||||
flushPostCommitCbs()
|
flushEffects()
|
||||||
// some post commit hook triggered more updates...
|
// some post commit hook triggered more updates...
|
||||||
if (patchQueue.length > 0) {
|
if (stageQueue.length > 0) {
|
||||||
if (!__COMPAT__ && getNow() - start > frameBudget) {
|
if (!__COMPAT__ && getNow() - flushStartTimestamp > frameBudget) {
|
||||||
return flushAfterMacroTask()
|
return flushAfterMacroTask()
|
||||||
} else {
|
} else {
|
||||||
// not out of budget yet, flush sync
|
// not out of budget yet, flush sync
|
||||||
@ -203,29 +288,29 @@ function flush(): void {
|
|||||||
nextTickQueue.length = 0
|
nextTickQueue.length = 0
|
||||||
} else {
|
} else {
|
||||||
// got more job to do
|
// got more job to do
|
||||||
// shouldn't reach here in compat mode, because the patchQueue is
|
// shouldn't reach here in compat mode, because the stageQueue is
|
||||||
// guarunteed to be drained
|
// guarunteed to have been depleted
|
||||||
flushAfterMacroTask()
|
flushAfterMacroTask()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetJob(job: Job) {
|
function resetJob(job: Job) {
|
||||||
job.ops.length = 0
|
job.ops.length = 0
|
||||||
job.post.length = 0
|
job.effects.length = 0
|
||||||
job.children.length = 0
|
job.children.length = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertNewJob(job: Job) {
|
function queueJobForStaging(job: Job) {
|
||||||
job.ops = job.ops || []
|
job.ops = job.ops || []
|
||||||
job.post = job.post || []
|
job.effects = job.effects || []
|
||||||
job.children = job.children || []
|
job.children = job.children || []
|
||||||
resetJob(job)
|
resetJob(job)
|
||||||
// inherit parent job's expiration deadline
|
// inherit parent job's expiration deadline
|
||||||
job.expiration = currentJob
|
job.expiration = currentJob
|
||||||
? currentJob.expiration
|
? currentJob.expiration
|
||||||
: getNow() + Priorities.NORMAL
|
: getNow() + JobPriorities.NORMAL
|
||||||
patchQueue.push(job)
|
stageQueue.push(job)
|
||||||
job.status = JobStatus.PENDING_PATCH
|
job.status = JobStatus.PENDING_STAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
function invalidateJob(job: Job) {
|
function invalidateJob(job: Job) {
|
||||||
@ -235,8 +320,8 @@ function invalidateJob(job: Job) {
|
|||||||
const child = children[i]
|
const child = children[i]
|
||||||
if (child.status === JobStatus.PENDING_COMMIT) {
|
if (child.status === JobStatus.PENDING_COMMIT) {
|
||||||
invalidateJob(child)
|
invalidateJob(child)
|
||||||
} else if (child.status === JobStatus.PENDING_PATCH) {
|
} else if (child.status === JobStatus.PENDING_STAGE) {
|
||||||
patchQueue.splice(patchQueue.indexOf(child), 1)
|
stageQueue.splice(stageQueue.indexOf(child), 1)
|
||||||
child.status = JobStatus.IDLE
|
child.status = JobStatus.IDLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,7 +335,21 @@ function invalidateJob(job: Job) {
|
|||||||
job.status = JobStatus.IDLE
|
job.status = JobStatus.IDLE
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchJob(job: Job) {
|
function requeueInvalidatedJob(job: Job) {
|
||||||
|
// With varying priorities we should insert job at correct position
|
||||||
|
// based on expiration time.
|
||||||
|
for (let i = 0; i < stageQueue.length; i++) {
|
||||||
|
if (job.expiration < stageQueue[i].expiration) {
|
||||||
|
stageQueue.splice(i, 0, job)
|
||||||
|
job.status = JobStatus.PENDING_STAGE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stageQueue.push(job)
|
||||||
|
job.status = JobStatus.PENDING_STAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
function stageJob(job: Job) {
|
||||||
// job with existing ops means it's already been patched in a low priority queue
|
// job with existing ops means it's already been patched in a low priority queue
|
||||||
if (job.ops.length === 0) {
|
if (job.ops.length === 0) {
|
||||||
currentJob = job
|
currentJob = job
|
||||||
@ -262,13 +361,13 @@ function patchJob(job: Job) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function commitJob(job: Job) {
|
function commitJob(job: Job) {
|
||||||
const { ops, post } = job
|
const { ops, effects } = job
|
||||||
for (let i = 0; i < ops.length; i++) {
|
for (let i = 0; i < ops.length; i++) {
|
||||||
applyOp(ops[i])
|
applyOp(ops[i])
|
||||||
}
|
}
|
||||||
// queue post commit cbs
|
// queue post commit cbs
|
||||||
if (post) {
|
if (effects) {
|
||||||
postCommitQueue.push(...post)
|
postEffectsQueue.push(...effects)
|
||||||
}
|
}
|
||||||
resetJob(job)
|
resetJob(job)
|
||||||
job.status = JobStatus.IDLE
|
job.status = JobStatus.IDLE
|
||||||
|
Loading…
Reference in New Issue
Block a user