From d70b7d6dd5e25fa2a13c004d656b83296af51005 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 2 Nov 2018 06:08:33 +0900 Subject: [PATCH] wip: error handling and nextTick for time slicing --- .../__tests__/attrsFallthrough.spec.ts | 6 +- packages/runtime-core/__tests__/hooks.spec.ts | 10 +- .../__tests__/inheritance.spec.ts | 6 +- .../runtime-core/__tests__/memoize.spec.ts | 6 +- .../__tests__/parentChain.spec.ts | 4 +- packages/runtime-core/src/componentUtils.ts | 10 +- packages/runtime-core/src/createRenderer.ts | 178 ++++++++++-------- packages/runtime-core/src/errorHandling.ts | 30 ++- packages/runtime-dom/src/index.ts | 2 +- ...stRenderer.spec.ts => testRuntime.spec.ts} | 4 +- packages/runtime-test/src/index.ts | 6 +- packages/scheduler/src/experimental.ts | 175 ++++++++--------- packages/scheduler/src/patchNodeOps.ts | 40 ++++ 13 files changed, 286 insertions(+), 191 deletions(-) rename packages/runtime-test/__tests__/{testRenderer.spec.ts => testRuntime.spec.ts} (98%) create mode 100644 packages/scheduler/src/patchNodeOps.ts diff --git a/packages/runtime-core/__tests__/attrsFallthrough.spec.ts b/packages/runtime-core/__tests__/attrsFallthrough.spec.ts index 20b4ad76..9cc625ac 100644 --- a/packages/runtime-core/__tests__/attrsFallthrough.spec.ts +++ b/packages/runtime-core/__tests__/attrsFallthrough.spec.ts @@ -44,7 +44,7 @@ describe('attribute fallthrough', () => { const root = document.createElement('div') document.body.appendChild(root) - render(h(Hello), root) + await render(h(Hello), root) const node = root.children[0] as HTMLElement @@ -110,7 +110,7 @@ describe('attribute fallthrough', () => { const root = document.createElement('div') document.body.appendChild(root) - render(h(Hello), root) + await render(h(Hello), root) const node = root.children[0] as HTMLElement @@ -190,7 +190,7 @@ describe('attribute fallthrough', () => { const root = document.createElement('div') document.body.appendChild(root) - render(h(Hello), root) + await render(h(Hello), root) const node = root.children[0] as HTMLElement diff --git a/packages/runtime-core/__tests__/hooks.spec.ts b/packages/runtime-core/__tests__/hooks.spec.ts index 29d1073b..55f9a488 100644 --- a/packages/runtime-core/__tests__/hooks.spec.ts +++ b/packages/runtime-core/__tests__/hooks.spec.ts @@ -1,5 +1,5 @@ import { withHooks, useState, h, nextTick, useEffect, Component } from '../src' -import { renderIntsance, serialize, triggerEvent } from '@vue/runtime-test' +import { renderInstance, serialize, triggerEvent } from '@vue/runtime-test' describe('hooks', () => { it('useState', async () => { @@ -16,7 +16,7 @@ describe('hooks', () => { ) }) - const counter = renderIntsance(Counter) + const counter = await renderInstance(Counter) expect(serialize(counter.$el)).toBe(`
0
`) triggerEvent(counter.$el, 'click') @@ -40,7 +40,7 @@ describe('hooks', () => { } } - const counter = renderIntsance(Counter) + const counter = await renderInstance(Counter) expect(serialize(counter.$el)).toBe(`
0
`) triggerEvent(counter.$el, 'click') @@ -71,7 +71,7 @@ describe('hooks', () => { } } - const counter = renderIntsance(Counter) + const counter = await renderInstance(Counter) expect(serialize(counter.$el)).toBe(`
0
`) triggerEvent(counter.$el, 'click') @@ -98,7 +98,7 @@ describe('hooks', () => { ) }) - const counter = renderIntsance(Counter) + const counter = await renderInstance(Counter) expect(effect).toBe(0) triggerEvent(counter.$el, 'click') await nextTick() diff --git a/packages/runtime-core/__tests__/inheritance.spec.ts b/packages/runtime-core/__tests__/inheritance.spec.ts index 55b3a58c..61ca8058 100644 --- a/packages/runtime-core/__tests__/inheritance.spec.ts +++ b/packages/runtime-core/__tests__/inheritance.spec.ts @@ -7,7 +7,7 @@ import { ComponentPropsOptions, ComponentWatchOptions } from '@vue/runtime-core' -import { createInstance, renderIntsance } from '@vue/runtime-test' +import { createInstance, renderInstance } from '@vue/runtime-test' describe('class inheritance', () => { it('should merge data', () => { @@ -136,7 +136,7 @@ describe('class inheritance', () => { } } - const container = renderIntsance(Container) + const container = await renderInstance(Container) expect(calls).toEqual([ 'base beforeCreate', 'child beforeCreate', @@ -200,7 +200,7 @@ describe('class inheritance', () => { } } - const container = renderIntsance(Container) + const container = await renderInstance(Container) expect(container.$el.text).toBe('foo') container.ok = false diff --git a/packages/runtime-core/__tests__/memoize.spec.ts b/packages/runtime-core/__tests__/memoize.spec.ts index f2e05398..bc6d5cbd 100644 --- a/packages/runtime-core/__tests__/memoize.spec.ts +++ b/packages/runtime-core/__tests__/memoize.spec.ts @@ -1,5 +1,5 @@ import { h, Component, memoize, nextTick } from '../src' -import { renderIntsance, serialize } from '@vue/runtime-test' +import { renderInstance, serialize } from '@vue/runtime-test' describe('memoize', () => { it('should work', async () => { @@ -16,7 +16,7 @@ describe('memoize', () => { } } - const app = renderIntsance(App) + const app = await renderInstance(App) expect(serialize(app.$el)).toBe(`
1
A1
B1
`) app.count++ @@ -38,7 +38,7 @@ describe('memoize', () => { } } - const app = renderIntsance(App) + const app = await renderInstance(App) expect(serialize(app.$el)).toBe(`
2
`) app.foo++ diff --git a/packages/runtime-core/__tests__/parentChain.spec.ts b/packages/runtime-core/__tests__/parentChain.spec.ts index 322115c1..119b6536 100644 --- a/packages/runtime-core/__tests__/parentChain.spec.ts +++ b/packages/runtime-core/__tests__/parentChain.spec.ts @@ -40,7 +40,7 @@ describe('Parent chain management', () => { } const root = nodeOps.createElement('div') - const parent = render(h(Parent), root) as Component + const parent = (await render(h(Parent), root)) as Component expect(child.$parent).toBe(parent) expect(child.$root).toBe(parent) @@ -99,7 +99,7 @@ describe('Parent chain management', () => { } const root = nodeOps.createElement('div') - const parent = render(h(Parent), root) as Component + const parent = (await render(h(Parent), root)) as Component expect(child.$parent).toBe(parent) expect(child.$root).toBe(parent) diff --git a/packages/runtime-core/src/componentUtils.ts b/packages/runtime-core/src/componentUtils.ts index 59c3ddb5..c7dda26b 100644 --- a/packages/runtime-core/src/componentUtils.ts +++ b/packages/runtime-core/src/componentUtils.ts @@ -18,7 +18,11 @@ import { resolveComponentOptionsFromClass } from './componentOptions' import { createRenderProxy } from './componentProxy' -import { handleError, ErrorTypes } from './errorHandling' +import { + handleError, + ErrorTypes, + callLifecycleHookWithHandle +} from './errorHandling' import { warn } from './warning' import { setCurrentInstance, unsetCurrentInstance } from './experimental/hooks' @@ -52,7 +56,7 @@ export function createComponentInstance( instance.$slots = currentVNode.slots || EMPTY_OBJ if (created) { - created.call($proxy) + callLifecycleHookWithHandle(created, $proxy, ErrorTypes.CREATED) } currentVNode = currentContextVNode = null @@ -96,7 +100,7 @@ export function initializeComponentInstance(instance: ComponentInstance) { // beforeCreate hook is called right in the constructor const { beforeCreate, props } = instance.$options if (beforeCreate) { - beforeCreate.call(proxy) + callLifecycleHookWithHandle(beforeCreate, proxy, ErrorTypes.BEFORE_CREATE) } initializeProps(instance, props, (currentVNode as VNode).data) } diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 991092db..3fe1b4d5 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -1,5 +1,5 @@ import { autorun, stop, Autorun, immutable } from '@vue/observer' -import { queueJob } from '@vue/scheduler' +import { queueJob, handleSchedulerError, nextTick } from '@vue/scheduler' import { VNodeFlags, ChildrenFlags } from './flags' import { EMPTY_OBJ, reservedPropRE, isString } from '@vue/shared' import { @@ -20,8 +20,12 @@ import { } from './componentUtils' import { KeepAliveSymbol } from './optional/keepAlive' import { pushWarningContext, popWarningContext, warn } from './warning' -import { handleError, ErrorTypes } from './errorHandling' import { resolveProps } from './componentProps' +import { + handleError, + ErrorTypes, + callLifecycleHookWithHandle +} from './errorHandling' export interface NodeOps { createElement: (tag: string, isSVG?: boolean) => any @@ -64,6 +68,8 @@ export interface FunctionalHandle { forceUpdate: () => void } +handleSchedulerError(err => handleError(err, null, ErrorTypes.SCHEDULER)) + // The whole mounting / patching / unmouting logic is placed inside this // single function so that we can create multiple renderes with different // platform definitions. This allows for use cases like creating a test @@ -184,7 +190,7 @@ export function createRenderer(options: RendererOptions) { mountRef(ref, el) } if (data != null && data.vnodeMounted) { - lifecycleHooks.push(() => { + lifecycleHooks.unshift(() => { data.vnodeMounted(vnode) }) } @@ -204,18 +210,12 @@ export function createRenderer(options: RendererOptions) { endNode: RenderNode | null ) { vnode.contextVNode = contextVNode - if (__DEV__) { - pushWarningContext(vnode) - } const { flags } = vnode if (flags & VNodeFlags.COMPONENT_STATEFUL) { mountStatefulComponent(vnode, container, isSVG, endNode) } else { mountFunctionalComponent(vnode, container, isSVG, endNode) } - if (__DEV__) { - popWarningContext() - } } function mountStatefulComponent( @@ -228,15 +228,13 @@ export function createRenderer(options: RendererOptions) { // kept-alive activateComponentInstance(vnode, container, endNode) } else { - queueJob( - () => { + if (__JSDOM__) { + mountComponentInstance(vnode, container, isSVG, endNode) + } else { + queueJob(() => { mountComponentInstance(vnode, container, isSVG, endNode) - }, - flushHooks, - err => { - handleError(err, vnode.contextVNode as VNode, ErrorTypes.SCHEDULER) - } - ) + }, flushHooks) + } } } @@ -260,50 +258,54 @@ export function createRenderer(options: RendererOptions) { forceUpdate: null as any }) - const handleSchedulerError = (err: Error) => { - handleError(err, handle.current as VNode, ErrorTypes.SCHEDULER) - } - const queueUpdate = (handle.forceUpdate = () => { - queueJob(handle.runner, null, handleSchedulerError) + queueJob(handle.runner) }) // we are using vnode.ref to store the functional component's update job - queueJob( - () => { - handle.runner = autorun( - () => { - if (handle.prevTree) { - // mounted - const { prevTree, current } = handle - const nextTree = (handle.prevTree = current.children = renderFunctionalRoot( - current - )) - patch( - prevTree as MountedVNode, - nextTree, - platformParentNode(current.el), - current as MountedVNode, - isSVG - ) - current.el = nextTree.el - } else { - // initial mount - const subTree = (handle.prevTree = vnode.children = renderFunctionalRoot( - vnode - )) - mount(subTree, container, vnode as MountedVNode, isSVG, endNode) - vnode.el = subTree.el as RenderNode + queueJob(() => { + handle.runner = autorun( + () => { + if (handle.prevTree) { + // mounted + const { prevTree, current } = handle + if (__DEV__) { + pushWarningContext(current) + } + const nextTree = (handle.prevTree = current.children = renderFunctionalRoot( + current + )) + patch( + prevTree as MountedVNode, + nextTree, + platformParentNode(current.el), + current as MountedVNode, + isSVG + ) + current.el = nextTree.el + if (__DEV__) { + popWarningContext() + } + } else { + // initial mount + if (__DEV__) { + pushWarningContext(vnode) + } + const subTree = (handle.prevTree = vnode.children = renderFunctionalRoot( + vnode + )) + mount(subTree, container, vnode as MountedVNode, isSVG, endNode) + vnode.el = subTree.el as RenderNode + if (__DEV__) { + popWarningContext() } - }, - { - scheduler: queueUpdate } - ) - }, - null, - handleSchedulerError - ) + }, + { + scheduler: queueUpdate + } + ) + }) } function mountText( @@ -514,9 +516,6 @@ export function createRenderer(options: RendererOptions) { contextVNode: MountedVNode | null, isSVG: boolean ) { - if (__DEV__) { - pushWarningContext(nextVNode) - } nextVNode.contextVNode = contextVNode const { tag, flags } = nextVNode if (tag !== prevVNode.tag) { @@ -526,9 +525,6 @@ export function createRenderer(options: RendererOptions) { } else { patchFunctionalComponent(prevVNode, nextVNode) } - if (__DEV__) { - popWarningContext() - } } function patchStatefulComponent(prevVNode: MountedVNode, nextVNode: VNode) { @@ -1161,6 +1157,10 @@ export function createRenderer(options: RendererOptions) { isSVG: boolean, endNode: RenderNode | null ): RenderNode { + if (__DEV__) { + pushWarningContext(vnode) + } + // a vnode may already have an instance if this is a compat call with // new Vue() const instance = ((__COMPAT__ && vnode.children) || @@ -1177,15 +1177,11 @@ export function createRenderer(options: RendererOptions) { } = instance if (beforeMount) { - beforeMount.call($proxy) - } - - const handleSchedulerError = (err: Error) => { - handleError(err, instance, ErrorTypes.SCHEDULER) + callLifecycleHookWithHandle(beforeMount, $proxy, ErrorTypes.BEFORE_MOUNT) } const queueUpdate = (instance.$forceUpdate = () => { - queueJob(instance._updateHandle, flushHooks, handleSchedulerError) + queueJob(instance._updateHandle, flushHooks) }) instance._updateHandle = autorun( @@ -1222,7 +1218,7 @@ export function createRenderer(options: RendererOptions) { const { mounted } = instance.$options if (mounted) { lifecycleHooks.unshift(() => { - mounted.call($proxy) + callLifecycleHookWithHandle(mounted, $proxy, ErrorTypes.MOUNTED) }) } } @@ -1234,6 +1230,10 @@ export function createRenderer(options: RendererOptions) { } ) + if (__DEV__) { + popWarningContext() + } + return vnode.el as RenderNode } @@ -1252,7 +1252,12 @@ export function createRenderer(options: RendererOptions) { $options: { beforeUpdate } } = instance if (beforeUpdate) { - beforeUpdate.call($proxy, prevVNode) + callLifecycleHookWithHandle( + beforeUpdate, + $proxy, + ErrorTypes.BEFORE_UPDATE, + prevVNode + ) } const nextVNode = (instance.$vnode = renderInstanceRoot( @@ -1286,7 +1291,12 @@ export function createRenderer(options: RendererOptions) { // invoked BEFORE the parent's. Therefore we add them to the head of the // queue instead. lifecycleHooks.unshift(() => { - updated.call($proxy, nextVNode) + callLifecycleHookWithHandle( + updated, + $proxy, + ErrorTypes.UPDATED, + nextVNode + ) }) } @@ -1316,7 +1326,11 @@ export function createRenderer(options: RendererOptions) { $options: { beforeUnmount, unmounted } } = instance if (beforeUnmount) { - beforeUnmount.call($proxy) + callLifecycleHookWithHandle( + beforeUnmount, + $proxy, + ErrorTypes.BEFORE_UNMOUNT + ) } if ($vnode) { unmount($vnode) @@ -1325,7 +1339,7 @@ export function createRenderer(options: RendererOptions) { teardownComponentInstance(instance) instance._unmounted = true if (unmounted) { - unmounted.call($proxy) + callLifecycleHookWithHandle(unmounted, $proxy, ErrorTypes.UNMOUNTED) } } @@ -1336,11 +1350,17 @@ export function createRenderer(options: RendererOptions) { container: RenderNode | null, endNode: RenderNode | null ) { + if (__DEV__) { + pushWarningContext(vnode) + } const instance = vnode.children as ComponentInstance vnode.el = instance.$el as RenderNode if (container != null) { insertVNode(instance.$vnode, container, endNode) } + if (__DEV__) { + popWarningContext() + } lifecycleHooks.push(() => { callActivatedHook(instance, true) }) @@ -1363,7 +1383,7 @@ export function createRenderer(options: RendererOptions) { callActivatedHook($children[i], false) } if (activated) { - activated.call($proxy) + callLifecycleHookWithHandle(activated, $proxy, ErrorTypes.ACTIVATED) } } } @@ -1388,7 +1408,7 @@ export function createRenderer(options: RendererOptions) { callDeactivateHook($children[i], false) } if (deactivated) { - deactivated.call($proxy) + callLifecycleHookWithHandle(deactivated, $proxy, ErrorTypes.DEACTIVATED) } } } @@ -1420,10 +1440,12 @@ export function createRenderer(options: RendererOptions) { container.vnode = null } } - // flushHooks() - // return vnode && vnode.flags & VNodeFlags.COMPONENT_STATEFUL - // ? (vnode.children as ComponentInstance).$proxy - // : null + return nextTick(() => { + debugger + return vnode && vnode.flags & VNodeFlags.COMPONENT_STATEFUL + ? (vnode.children as ComponentInstance).$proxy + : null + }) } return { render } diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index e2eb4259..1205d5fa 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -10,8 +10,10 @@ export const enum ErrorTypes { MOUNTED, BEFORE_UPDATE, UPDATED, - BEFORE_DESTROY, - DESTROYED, + BEFORE_UNMOUNT, + UNMOUNTED, + ACTIVATED, + DEACTIVATED, ERROR_CAPTURED, RENDER, WATCH_CALLBACK, @@ -27,8 +29,10 @@ const ErrorTypeStrings: Record = { [ErrorTypes.MOUNTED]: 'in mounted lifecycle hook', [ErrorTypes.BEFORE_UPDATE]: 'in beforeUpdate lifecycle hook', [ErrorTypes.UPDATED]: 'in updated lifecycle hook', - [ErrorTypes.BEFORE_DESTROY]: 'in beforeDestroy lifecycle hook', - [ErrorTypes.DESTROYED]: 'in destroyed lifecycle hook', + [ErrorTypes.BEFORE_UNMOUNT]: 'in beforeUnmount lifecycle hook', + [ErrorTypes.UNMOUNTED]: 'in unmounted lifecycle hook', + [ErrorTypes.ACTIVATED]: 'in activated lifecycle hook', + [ErrorTypes.DEACTIVATED]: 'in deactivated lifecycle hook', [ErrorTypes.ERROR_CAPTURED]: 'in errorCaptured lifecycle hook', [ErrorTypes.RENDER]: 'in render function', [ErrorTypes.WATCH_CALLBACK]: 'in watcher callback', @@ -38,6 +42,24 @@ const ErrorTypeStrings: Record = { 'when flushing updates. This may be a Vue internals bug.' } +export function callLifecycleHookWithHandle( + hook: Function, + instanceProxy: ComponentInstance, + type: ErrorTypes, + arg?: any +) { + try { + const res = hook.call(instanceProxy, arg) + if (res && typeof res.then === 'function') { + ;(res as Promise).catch(err => { + handleError(err, instanceProxy._self, type) + }) + } + } catch (err) { + handleError(err, instanceProxy._self, type) + } +} + export function handleError( err: Error, instance: ComponentInstance | VNode | null, diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 615e1818..38e35326 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -12,7 +12,7 @@ const { render: _render } = createRenderer({ type publicRender = ( node: {} | null, container: HTMLElement -) => Component | null +) => Promise export const render = _render as publicRender // re-export everything from core diff --git a/packages/runtime-test/__tests__/testRenderer.spec.ts b/packages/runtime-test/__tests__/testRuntime.spec.ts similarity index 98% rename from packages/runtime-test/__tests__/testRenderer.spec.ts rename to packages/runtime-test/__tests__/testRuntime.spec.ts index 9e04db18..09480465 100644 --- a/packages/runtime-test/__tests__/testRenderer.spec.ts +++ b/packages/runtime-test/__tests__/testRuntime.spec.ts @@ -12,7 +12,7 @@ import { observable, resetOps, serialize, - renderIntsance, + renderInstance, triggerEvent } from '../src' @@ -171,7 +171,7 @@ describe('test renderer', () => { ) } } - const app = renderIntsance(App) + const app = await renderInstance(App) triggerEvent(app.$el, 'click') expect(app.count).toBe(1) await nextTick() diff --git a/packages/runtime-test/src/index.ts b/packages/runtime-test/src/index.ts index 0c901816..5e1d35a9 100644 --- a/packages/runtime-test/src/index.ts +++ b/packages/runtime-test/src/index.ts @@ -15,7 +15,7 @@ const { render: _render } = createRenderer({ type publicRender = ( node: {} | null, container: TestElement -) => Component | null +) => Promise export const render = _render as publicRender export function createInstance( @@ -25,10 +25,10 @@ export function createInstance( return createComponentInstance(h(Class, props)).$proxy as any } -export function renderIntsance( +export function renderInstance( Class: new () => T, props?: any -): T { +): Promise { return render(h(Class, props), nodeOps.createElement('div')) as any } diff --git a/packages/scheduler/src/experimental.ts b/packages/scheduler/src/experimental.ts index ef10d3ae..7c4a0ed5 100644 --- a/packages/scheduler/src/experimental.ts +++ b/packages/scheduler/src/experimental.ts @@ -1,48 +1,4 @@ -import { NodeOps } from '@vue/runtime-core' -import { nodeOps } from '../../runtime-dom/src/nodeOps' - -const enum Priorities { - NORMAL = 500 -} - -const frameBudget = 1000 / 60 - -let start: number = 0 -let currentOps: Op[] - -const getNow = () => window.performance.now() - -const evaluate = (v: any) => { - return typeof v === 'function' ? v() : v -} - -// patch nodeOps to record operations without touching the DOM -Object.keys(nodeOps).forEach((key: keyof NodeOps) => { - const original = nodeOps[key] as Function - if (key === 'querySelector') { - return - } - if (/create/.test(key)) { - nodeOps[key] = (...args: any[]) => { - let res: any - if (currentOps) { - return () => res || (res = original(...args)) - } else { - return original(...args) - } - } - } else { - nodeOps[key] = (...args: any[]) => { - if (currentOps) { - currentOps.push([original, ...args.map(evaluate)]) - } else { - original(...args) - } - } - } -}) - -type Op = [Function, ...any[]] +import { Op, setCurrentOps } from './patchNodeOps' interface Job extends Function { ops: Op[] @@ -50,11 +6,29 @@ interface Job extends Function { expiration: number } +const enum Priorities { + NORMAL = 500 +} + +type ErrorHandler = (err: Error) => any + +let start: number = 0 +const getNow = () => window.performance.now() +const frameBudget = __JSDOM__ ? Infinity : 1000 / 60 + +const patchQueue: Job[] = [] +const commitQueue: Job[] = [] +const postCommitQueue: Function[] = [] +const nextTickQueue: Function[] = [] + +let globalHandler: ErrorHandler +const pendingRejectors: ErrorHandler[] = [] + // Microtask for batching state mutations const p = Promise.resolve() -export function nextTick(fn?: () => void): Promise { - return p.then(fn) +function flushAfterMicroTask() { + return p.then(flush).catch(handleError) } // Macrotask for time slicing @@ -67,46 +41,48 @@ window.addEventListener( return } start = getNow() - flush() + try { + flush() + } catch (e) { + handleError(e) + } }, false ) -function flushAfterYield() { +function flushAfterMacroTask() { window.postMessage(key, `*`) } -const patchQueue: Job[] = [] -const commitQueue: Job[] = [] - -function patch(job: Job) { - // job with existing ops means it's already been patched in a low priority queue - if (job.ops.length === 0) { - currentOps = job.ops - job() - commitQueue.push(job) - } +export function nextTick(fn?: () => T): Promise { + return new Promise((resolve, reject) => { + p.then(() => { + if (hasPendingFlush) { + nextTickQueue.push(() => { + resolve(fn ? fn() : undefined) + }) + pendingRejectors.push(reject) + } else { + resolve(fn ? fn() : undefined) + } + }).catch(reject) + }) } -function commit({ ops }: Job) { - for (let i = 0; i < ops.length; i++) { - const [fn, ...args] = ops[i] - fn(...args) - } - ops.length = 0 +function handleError(err: Error) { + if (globalHandler) globalHandler(err) + pendingRejectors.forEach(handler => { + handler(err) + }) } -function invalidate(job: Job) { - job.ops.length = 0 +export function handleSchedulerError(handler: ErrorHandler) { + globalHandler = handler } let hasPendingFlush = false -export function queueJob( - rawJob: Function, - postJob?: Function | null, - onError?: (reason: any) => void -) { +export function queueJob(rawJob: Function, postJob?: Function | null) { const job = rawJob as Job job.post = postJob || null job.ops = job.ops || [] @@ -117,7 +93,7 @@ export function queueJob( // invalidated. remove from commit queue // and move it back to the patch queue commitQueue.splice(commitIndex, 1) - invalidate(job) + invalidateJob(job) // With varying priorities we should insert job at correct position // based on expiration time. for (let i = 0; i < patchQueue.length; i++) { @@ -135,17 +111,16 @@ export function queueJob( if (!hasPendingFlush) { hasPendingFlush = true start = getNow() - const p = nextTick(flush) - if (onError) p.catch(onError) + flushAfterMicroTask() } } -function flush() { +function flush(): void { let job while (true) { job = patchQueue.shift() if (job) { - patch(job) + patchJob(job) } else { break } @@ -156,23 +131,55 @@ function flush() { } if (patchQueue.length === 0) { - const postQueue: Function[] = [] // all done, time to commit! while ((job = commitQueue.shift())) { - commit(job) - if (job.post && postQueue.indexOf(job.post) < 0) { - postQueue.push(job.post) + commitJob(job) + if (job.post && postCommitQueue.indexOf(job.post) < 0) { + postCommitQueue.push(job.post) } } - while ((job = postQueue.shift())) { + // post commit hooks (updated, mounted) + while ((job = postCommitQueue.shift())) { job() } + // some post commit hook triggered more updates... if (patchQueue.length > 0) { - return flushAfterYield() + if (getNow() - start > frameBudget) { + return flushAfterMacroTask() + } else { + // not out of budget yet, flush sync + return flush() + } } + // now we are really done hasPendingFlush = false + pendingRejectors.length = 0 + while ((job = nextTickQueue.shift())) { + job() + } } else { // got more job to do - flushAfterYield() + flushAfterMacroTask() } } + +function patchJob(job: Job) { + // job with existing ops means it's already been patched in a low priority queue + if (job.ops.length === 0) { + setCurrentOps(job.ops) + job() + commitQueue.push(job) + } +} + +function commitJob({ ops }: Job) { + for (let i = 0; i < ops.length; i++) { + const [fn, ...args] = ops[i] + fn(...args) + } + ops.length = 0 +} + +function invalidateJob(job: Job) { + job.ops.length = 0 +} diff --git a/packages/scheduler/src/patchNodeOps.ts b/packages/scheduler/src/patchNodeOps.ts new file mode 100644 index 00000000..0ac25cc4 --- /dev/null +++ b/packages/scheduler/src/patchNodeOps.ts @@ -0,0 +1,40 @@ +import { NodeOps } from '@vue/runtime-core' +import { nodeOps } from '../../runtime-dom/src/nodeOps' + +export type Op = [Function, ...any[]] + +let currentOps: Op[] + +export function setCurrentOps(ops: Op[]) { + currentOps = ops +} + +const evaluate = (v: any) => { + return typeof v === 'function' ? v() : v +} + +// patch nodeOps to record operations without touching the DOM +Object.keys(nodeOps).forEach((key: keyof NodeOps) => { + const original = nodeOps[key] as Function + if (key === 'querySelector') { + return + } + if (/create/.test(key)) { + nodeOps[key] = (...args: any[]) => { + let res: any + if (currentOps) { + return () => res || (res = original(...args)) + } else { + return original(...args) + } + } + } else { + nodeOps[key] = (...args: any[]) => { + if (currentOps) { + currentOps.push([original, ...args.map(evaluate)]) + } else { + original(...args) + } + } + } +})