From 5c069eeae7b316014506768daf8c616264f1255a Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 28 May 2019 17:19:47 +0800 Subject: [PATCH] wip: scheduler, more component --- packages/runtime-core/src/component.ts | 81 +++- packages/runtime-core/src/createRenderer.ts | 90 ++-- packages/runtime-core/src/scheduler.ts | 74 ++++ packages/runtime-core/src/vnode.ts | 17 +- packages/scheduler/.npmignore | 3 - packages/scheduler/README.md | 5 - .../scheduler/__tests__/scheduler.spec.ts | 123 ------ packages/scheduler/index.js | 7 - packages/scheduler/package.json | 22 - packages/scheduler/src/index.ts | 385 ------------------ packages/shared/src/index.ts | 1 + tsconfig.json | 1 - 12 files changed, 216 insertions(+), 593 deletions(-) create mode 100644 packages/runtime-core/src/scheduler.ts delete mode 100644 packages/scheduler/.npmignore delete mode 100644 packages/scheduler/README.md delete mode 100644 packages/scheduler/__tests__/scheduler.spec.ts delete mode 100644 packages/scheduler/index.js delete mode 100644 packages/scheduler/package.json delete mode 100644 packages/scheduler/src/index.ts diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index e6d9ecb8..3ba82c31 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -1,9 +1,82 @@ -import { VNode, normalizeVNode } from './vnode' +import { VNode, normalizeVNode, VNodeChild } from './vnode' +import { ReactiveEffect } from '@vue/observer' +import { isFunction, EMPTY_OBJ } from '@vue/shared' -export class Component {} +interface Value { + value: T +} -export function renderComponentRoot(instance: any): VNode { - return normalizeVNode(instance.render(instance.vnode.props)) +type UnwrapBindings = { + [key in keyof T]: T[key] extends Value ? V : T[key] +} + +type Prop = { (): T } | { new (...args: any[]): T & object } + +type ExtractPropTypes = { + readonly [key in keyof PropOptions]: PropOptions[key] extends Prop + ? V + : PropOptions[key] extends null | undefined ? any : PropOptions[key] +} + +interface ComponentPublicProperties { + $props: P + $state: S +} + +export interface ComponentOptions< + RawProps = { [key: string]: Prop }, + RawBindings = { [key: string]: any } | void, + Props = ExtractPropTypes, + Bindings = UnwrapBindings +> { + props?: RawProps + setup?: (props: Props) => RawBindings + render?: ( + this: ComponentPublicProperties, + ctx: { + state: B + props: Props + } + ) => VNodeChild +} + +// no-op, for type inference only +export function createComponent< + RawProps, + RawBindings, + Props = ExtractPropTypes, + Bindings = UnwrapBindings +>( + options: ComponentOptions +): { + // for TSX + new (): { $props: Props } +} { + return options as any +} + +export interface ComponentHandle { + type: Function | ComponentOptions + vnode: VNode | null + next: VNode | null + subTree: VNode | null + update: ReactiveEffect +} + +export function renderComponentRoot(handle: ComponentHandle): VNode { + const { type, vnode } = handle + // TODO actually resolve props + const renderArg = { + props: (vnode as VNode).props || EMPTY_OBJ + } + if (isFunction(type)) { + return normalizeVNode(type(renderArg)) + } else { + if (__DEV__ && !type.render) { + // TODO warn missing render + } + return normalizeVNode((type.render as Function)(renderArg)) + } } export function shouldUpdateComponent( diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 7a7d8d58..9a907855 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -1,9 +1,9 @@ // TODO: // - component -// - lifecycle +// - lifecycle / refs +// - keep alive // - app context // - svg -// - refs // - hydration // - warning context // - parent chain @@ -13,17 +13,20 @@ import { Text, Fragment, Empty, + Portal, normalizeVNode, VNode, VNodeChildren -} from './vnode.js' +} from './vnode' +import { isString, isArray, EMPTY_OBJ, EMPTY_ARR } from '@vue/shared' import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' -import { effect } from '@vue/observer' -import { isString, isFunction, isArray } from '@vue/shared' -import { renderComponentRoot, shouldUpdateComponent } from './component.js' - -const emptyArr: any[] = [] -const emptyObj: { [key: string]: any } = {} +import { effect, stop } from '@vue/observer' +import { + ComponentHandle, + renderComponentRoot, + shouldUpdateComponent +} from './component' +import { queueJob } from './scheduler' function isSameType(n1: VNode, n2: VNode): boolean { return n1.type === n2.type && n1.key === n2.key @@ -81,16 +84,26 @@ export function createRenderer(options: RendererOptions) { } const { type } = n2 - if (type === Text) { - processText(n1, n2, container, anchor) - } else if (type === Empty) { - processEmptyNode(n1, n2, container, anchor) - } else if (type === Fragment) { - processFragment(n1, n2, container, anchor, optimized) - } else if (isFunction(type)) { - processComponent(n1, n2, container, anchor) - } else { - processElement(n1, n2, container, anchor, optimized) + switch (type) { + case Text: + processText(n1, n2, container, anchor) + break + case Empty: + processEmptyNode(n1, n2, container, anchor) + break + case Fragment: + processFragment(n1, n2, container, anchor, optimized) + break + case Portal: + // TODO + break + default: + if (isString(type)) { + processElement(n1, n2, container, anchor, optimized) + } else { + processComponent(n1, n2, container, anchor) + } + break } } @@ -172,8 +185,8 @@ export function createRenderer(options: RendererOptions) { function patchElement(n1: VNode, n2: VNode, optimized?: boolean) { const el = (n2.el = n1.el) const { patchFlag, dynamicChildren } = n2 - const oldProps = (n1 && n1.props) || emptyObj - const newProps = n2.props || emptyObj + const oldProps = (n1 && n1.props) || EMPTY_OBJ + const newProps = n2.props || EMPTY_OBJ if (patchFlag != null) { // the presence of a patchFlag means this element's render code was @@ -272,7 +285,7 @@ export function createRenderer(options: RendererOptions) { ) } } - if (oldProps !== emptyObj) { + if (oldProps !== EMPTY_OBJ) { for (const key in oldProps) { if (!(key in newProps)) { hostPatchProp( @@ -320,10 +333,10 @@ export function createRenderer(options: RendererOptions) { if (n1 == null) { mountComponent(n2, container, anchor) } else { - const instance = (n2.component = n1.component) + const instance = (n2.component = n1.component) as ComponentHandle if (shouldUpdateComponent(n1, n2)) { instance.next = n2 - instance.forceUpdate() + instance.update() } else { n2.el = n1.el } @@ -335,15 +348,17 @@ export function createRenderer(options: RendererOptions) { container: HostNode, anchor?: HostNode ) { - const instance = (vnode.component = { + const instance: ComponentHandle = (vnode.component = { + type: vnode.type as Function, vnode: null, next: null, subTree: null, - forceUpdate: null, - render: vnode.type - } as any) + update: null as any + }) - instance.forceUpdate = effect( + // TODO call setup, handle bindings and render context + + instance.update = effect( () => { if (!instance.vnode) { // initial mount @@ -357,9 +372,9 @@ export function createRenderer(options: RendererOptions) { } }, { - scheduler: e => e() // TODO use proper scheduler + scheduler: queueJob } - ) as any + ) } function updateComponent( @@ -454,8 +469,8 @@ export function createRenderer(options: RendererOptions) { anchor?: HostNode, optimized?: boolean ) { - c1 = c1 || emptyArr - c2 = c2 || emptyArr + c1 = c1 || EMPTY_ARR + c2 = c2 || EMPTY_ARR const oldLength = c1.length const newLength = c2.length const commonLength = Math.min(oldLength, newLength) @@ -618,7 +633,7 @@ export function createRenderer(options: RendererOptions) { // generate longest stable subsequence only when nodes have moved const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) - : emptyArr + : EMPTY_ARR j = increasingNewIndexSequence.length - 1 // looping backwards so that we can use last patched node as anchor for (i = toBePatched - 1; i >= 0; i--) { @@ -645,7 +660,7 @@ export function createRenderer(options: RendererOptions) { function move(vnode: VNode, container: HostNode, anchor: HostNode) { if (vnode.component != null) { - move(vnode.component.subTree, container, anchor) + move(vnode.component.subTree as VNode, container, anchor) return } if (vnode.type === Fragment) { @@ -663,7 +678,8 @@ export function createRenderer(options: RendererOptions) { function unmount(vnode: VNode, doRemove?: boolean) { if (vnode.component != null) { // TODO teardown component - unmount(vnode.component.subTree, doRemove) + stop(vnode.component.update) + unmount(vnode.component.subTree as VNode, doRemove) return } const shouldRemoveChildren = vnode.type === Fragment && doRemove @@ -691,7 +707,7 @@ export function createRenderer(options: RendererOptions) { function getNextHostNode(vnode: VNode): HostNode { return vnode.component === null ? hostNextSibling(vnode.anchor || vnode.el) - : getNextHostNode(vnode.component.subTree) + : getNextHostNode(vnode.component.subTree as VNode) } return function render(vnode: VNode, dom: HostNode): VNode { diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts new file mode 100644 index 00000000..1652fd55 --- /dev/null +++ b/packages/runtime-core/src/scheduler.ts @@ -0,0 +1,74 @@ +const queue: Array<() => void> = [] +const postFlushCbs: Array<() => void> = [] +const p = Promise.resolve() + +let isFlushing = false + +export function nextTick(fn?: () => void): Promise { + return fn ? p.then(fn) : p +} + +export function queueJob(job: () => void, onError?: (err: Error) => void) { + if (queue.indexOf(job) === -1) { + queue.push(job) + if (!isFlushing) { + const p = nextTick(flushJobs) + if (onError) p.catch(onError) + } + } +} + +export function queuePostFlushCb(cb: () => void) { + if (postFlushCbs.indexOf(cb) === -1) { + postFlushCbs.push(cb) + } +} + +export function flushPostFlushCbs() { + const cbs = postFlushCbs.slice() + let i = cbs.length + postFlushCbs.length = 0 + // post flush cbs are flushed in reverse since they are queued top-down + // but should fire bottom-up + while (i--) { + cbs[i]() + } +} + +const RECURSION_LIMIT = 100 +type JobCountMap = Map + +function flushJobs(seenJobs?: JobCountMap) { + isFlushing = true + let job + if (__DEV__) { + seenJobs = seenJobs || new Map() + } + while ((job = queue.shift())) { + if (__DEV__) { + const seen = seenJobs as JobCountMap + if (!seen.has(job)) { + seen.set(job, 1) + } else { + const count = seen.get(job) as number + if (count > RECURSION_LIMIT) { + throw new Error( + 'Maximum recursive updates exceeded. ' + + "You may have code that is mutating state in your component's " + + 'render function or updated hook.' + ) + } else { + seen.set(job, count + 1) + } + } + } + job() + } + flushPostFlushCbs() + isFlushing = false + // some postFlushCb queued jobs! + // keep flushing until it drains. + if (queue.length) { + flushJobs(seenJobs) + } +} diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index e32fc00e..3fd87435 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -1,29 +1,34 @@ import { isArray, isFunction } from '@vue/shared' +import { ComponentHandle } from './component' +import { HostNode } from './createRenderer' export const Fragment = Symbol('Fragment') export const Text = Symbol('Text') export const Empty = Symbol('Empty') +export const Portal = Symbol('Portal') type VNodeTypes = | string | Function + | Object | typeof Fragment | typeof Text | typeof Empty -export type VNodeChild = VNode | string | number | null -export interface VNodeChildren extends Array {} +type VNodeChildAtom = VNode | string | number | null | void +export interface VNodeChildren extends Array {} +export type VNodeChild = VNodeChildAtom | VNodeChildren export interface VNode { type: VNodeTypes props: { [key: string]: any } | null key: string | number | null children: string | VNodeChildren | null - component: any + component: ComponentHandle | null // DOM - el: any - anchor: any // fragment anchor + el: HostNode | null + anchor: HostNode | null // fragment anchor // optimization only patchFlag: number | null @@ -98,7 +103,7 @@ export function cloneVNode(vnode: VNode): VNode { return vnode } -export function normalizeVNode(child: any): VNode { +export function normalizeVNode(child: VNodeChild): VNode { if (child == null) { // empty placeholder return createVNode(Empty) diff --git a/packages/scheduler/.npmignore b/packages/scheduler/.npmignore deleted file mode 100644 index bb5c8a54..00000000 --- a/packages/scheduler/.npmignore +++ /dev/null @@ -1,3 +0,0 @@ -__tests__/ -__mocks__/ -dist/packages \ No newline at end of file diff --git a/packages/scheduler/README.md b/packages/scheduler/README.md deleted file mode 100644 index 2af0546c..00000000 --- a/packages/scheduler/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @vue/scheduler - -> This package is published only for typing and building custom renderers. It is NOT meant to be used in applications. - -The default scheduler that uses Promise / Microtask to defer and batch updates. diff --git a/packages/scheduler/__tests__/scheduler.spec.ts b/packages/scheduler/__tests__/scheduler.spec.ts deleted file mode 100644 index d196ce9a..00000000 --- a/packages/scheduler/__tests__/scheduler.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { queueJob, queuePostEffect, nextTick } from '../src/index' - -describe('scheduler', () => { - it('queueJob', async () => { - const calls: any = [] - const job1 = () => { - calls.push('job1') - } - const job2 = () => { - calls.push('job2') - } - queueJob(job1) - queueJob(job2) - expect(calls).toEqual([]) - await nextTick() - expect(calls).toEqual(['job1', 'job2']) - }) - - it('queueJob while already flushing', async () => { - const calls: any = [] - const job1 = () => { - calls.push('job1') - // job1 queues job2 - queueJob(job2) - } - const job2 = () => { - calls.push('job2') - } - queueJob(job1) - expect(calls).toEqual([]) - await nextTick() - expect(calls).toEqual(['job1', 'job2']) - }) - - it('queueJob w/ postCommitCb', async () => { - const calls: any = [] - const job1 = () => { - calls.push('job1') - queuePostEffect(cb1) - } - const job2 = () => { - calls.push('job2') - queuePostEffect(cb2) - } - const cb1 = () => { - calls.push('cb1') - } - const cb2 = () => { - calls.push('cb2') - } - queueJob(job1) - queueJob(job2) - await nextTick() - // post commit cbs are called in reverse! - expect(calls).toEqual(['job1', 'job2', 'cb2', 'cb1']) - }) - - it('queueJob w/ postFlushCb while flushing', async () => { - const calls: any = [] - const job1 = () => { - calls.push('job1') - queuePostEffect(cb1) - // job1 queues job2 - queueJob(job2) - } - const job2 = () => { - calls.push('job2') - queuePostEffect(cb2) - } - const cb1 = () => { - calls.push('cb1') - } - const cb2 = () => { - calls.push('cb2') - } - queueJob(job1) - expect(calls).toEqual([]) - await nextTick() - expect(calls).toEqual(['job1', 'job2', 'cb2', 'cb1']) - }) - - it('should dedupe queued tasks', async () => { - const calls: any = [] - const job1 = () => { - calls.push('job1') - } - const job2 = () => { - calls.push('job2') - } - queueJob(job1) - queueJob(job2) - queueJob(job1) - queueJob(job2) - expect(calls).toEqual([]) - await nextTick() - expect(calls).toEqual(['job1', 'job2']) - }) - - it('queueJob inside postEffect', async () => { - const calls: any = [] - const job1 = () => { - calls.push('job1') - queuePostEffect(cb1) - } - const cb1 = () => { - // queue another job in postFlushCb - calls.push('cb1') - queueJob(job2) - } - const job2 = () => { - calls.push('job2') - queuePostEffect(cb2) - } - const cb2 = () => { - calls.push('cb2') - } - - queueJob(job1) - queueJob(job2) - await nextTick() - expect(calls).toEqual(['job1', 'job2', 'cb2', 'cb1', 'job2', 'cb2']) - }) -}) diff --git a/packages/scheduler/index.js b/packages/scheduler/index.js deleted file mode 100644 index 9a0883bf..00000000 --- a/packages/scheduler/index.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -if (process.env.NODE_ENV === 'production') { - module.exports = require('./dist/scheduler.cjs.prod.js') -} else { - module.exports = require('./dist/scheduler.cjs.js') -} diff --git a/packages/scheduler/package.json b/packages/scheduler/package.json deleted file mode 100644 index 397fdc2a..00000000 --- a/packages/scheduler/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@vue/scheduler", - "version": "3.0.0-alpha.1", - "description": "@vue/scheduler", - "main": "index.js", - "module": "dist/scheduler.esm-bundler.js", - "types": "dist/index.d.ts", - "sideEffects": false, - "repository": { - "type": "git", - "url": "git+https://github.com/vuejs/vue.git" - }, - "keywords": [ - "vue" - ], - "author": "Evan You", - "license": "MIT", - "bugs": { - "url": "https://github.com/vuejs/vue/issues" - }, - "homepage": "https://github.com/vuejs/vue/tree/dev/packages/scheduler#readme" -} diff --git a/packages/scheduler/src/index.ts b/packages/scheduler/src/index.ts deleted file mode 100644 index 89323efd..00000000 --- a/packages/scheduler/src/index.ts +++ /dev/null @@ -1,385 +0,0 @@ -// 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 8 arguments. -type Op = [Function, ...any[]] - -// A "job" stands for a unit of work that needs to be performed. -// Typically, one job corresponds to the mounting or updating of one component -// instance (including functional ones). -interface Job 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 `queuePostEffect` inside the job function. - postEffects: 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 ReactiveEffect). 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 { - IDLE = 0, - PENDING_STAGE, - PENDING_COMMIT -} - -// Priorities for different types of jobs. This number is added to the -// current time when a new job is queued to calculate the expiration time -// for that job. -// -// Currently we have only one type which expires 500ms after it is initially -// queued. There could be higher/lower priorities in the future. -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 - -let globalHandler: ErrorHandler - -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() - -function flushAfterMicroTask() { - flushStartTimestamp = getNow() - return p.then(flush).catch(handleError) -} - -// 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` - -window.addEventListener( - 'message', - event => { - if (event.source !== window || event.data !== key) { - return - } - flushStartTimestamp = getNow() - try { - flush() - } catch (e) { - handleError(e) - } - }, - false -) - -function flushAfterMacroTask() { - 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._update which is in turn -// an ReactiveEffect 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 queuePostEffect(fn: Function) { - if (currentJob) { - currentJob.postEffects.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(fn?: () => T): Promise { - return new Promise((resolve, reject) => { - p.then(() => { - if (hasPendingFlush) { - nextTickQueue.push(() => { - resolve(fn ? fn() : undefined) - }) - pendingRejectors.push(err => { - if (fn) fn() - reject(err) - }) - } else { - resolve(fn ? fn() : undefined) - } - }).catch(reject) - }) -} - -// Internals ------------------------------------------------------------------- - -function flush(): void { - let job - while (true) { - job = stageQueue.shift() - if (job) { - stageJob(job) - } else { - break - } - if (!__COMPAT__) { - const now = getNow() - if (now - flushStartTimestamp > frameBudget && job.expiration > now) { - break - } - } - } - - if (stageQueue.length === 0) { - // all done, time to commit! - for (let i = 0; i < commitQueue.length; i++) { - commitJob(commitQueue[i]) - } - commitQueue.length = 0 - flushEffects() - // some post commit hook triggered more updates... - if (stageQueue.length > 0) { - if (!__COMPAT__ && getNow() - flushStartTimestamp > frameBudget) { - return flushAfterMacroTask() - } else { - // not out of budget yet, flush sync - return flush() - } - } - // now we are really done - hasPendingFlush = false - pendingRejectors.length = 0 - for (let i = 0; i < nextTickQueue.length; i++) { - nextTickQueue[i]() - } - nextTickQueue.length = 0 - } else { - // got more job to do - // shouldn't reach here in compat mode, because the stageQueue is - // guarunteed to have been depleted - flushAfterMacroTask() - } -} - -function resetJob(job: Job) { - job.ops.length = 0 - job.postEffects.length = 0 - job.children.length = 0 -} - -function queueJobForStaging(job: Job) { - job.ops = job.ops || [] - job.postEffects = job.postEffects || [] - job.children = job.children || [] - resetJob(job) - // inherit parent job's expiration deadline - job.expiration = currentJob - ? currentJob.expiration - : getNow() + JobPriorities.NORMAL - stageQueue.push(job) - job.status = JobStatus.PENDING_STAGE -} - -function invalidateJob(job: Job) { - // recursively invalidate all child jobs - const { children } = job - for (let i = 0; i < children.length; i++) { - const child = children[i] - if (child.status === JobStatus.PENDING_COMMIT) { - invalidateJob(child) - } else if (child.status === JobStatus.PENDING_STAGE) { - stageQueue.splice(stageQueue.indexOf(child), 1) - child.status = JobStatus.IDLE - } - } - if (job.cleanup) { - job.cleanup() - job.cleanup = null - } - resetJob(job) - // remove from commit queue - commitQueue.splice(commitQueue.indexOf(job), 1) - job.status = JobStatus.IDLE -} - -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 - if (job.ops.length === 0) { - currentJob = job - job.cleanup = job() - currentJob = null - commitQueue.push(job) - job.status = JobStatus.PENDING_COMMIT - } -} - -function commitJob(job: Job) { - const { ops, postEffects } = job - for (let i = 0; i < ops.length; i++) { - applyOp(ops[i]) - } - // queue post commit cbs - if (postEffects) { - postEffectsQueue.push(...postEffects) - } - resetJob(job) - job.status = JobStatus.IDLE -} - -function applyOp(op: Op) { - const fn = op[0] - // optimize for more common cases - // only patchData needs 8 arguments - if (op.length <= 3) { - fn(op[1], op[2], op[3]) - } else { - fn(op[1], op[2], op[3], op[4], op[5], op[6], op[7], op[8]) - } -} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 1f23bf40..002aab83 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,4 +1,5 @@ export const EMPTY_OBJ: { readonly [key: string]: any } = Object.freeze({}) +export const EMPTY_ARR: [] = [] export const NOOP = () => {} diff --git a/tsconfig.json b/tsconfig.json index 69423b40..8c69e693 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,6 @@ "@vue/runtime-dom": ["packages/runtime-dom/src"], "@vue/runtime-test": ["packages/runtime-test/src"], "@vue/observer": ["packages/observer/src"], - "@vue/scheduler": ["packages/scheduler/src"], "@vue/compiler-core": ["packages/compiler-core/src"], "@vue/server-renderer": ["packages/server-renderer/src"] }