]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: error handling and nextTick for time slicing
authorEvan You <yyx990803@gmail.com>
Thu, 1 Nov 2018 21:08:33 +0000 (06:08 +0900)
committerEvan You <yyx990803@gmail.com>
Fri, 2 Nov 2018 21:31:31 +0000 (06:31 +0900)
13 files changed:
packages/runtime-core/__tests__/attrsFallthrough.spec.ts
packages/runtime-core/__tests__/hooks.spec.ts
packages/runtime-core/__tests__/inheritance.spec.ts
packages/runtime-core/__tests__/memoize.spec.ts
packages/runtime-core/__tests__/parentChain.spec.ts
packages/runtime-core/src/componentUtils.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/errorHandling.ts
packages/runtime-dom/src/index.ts
packages/runtime-test/__tests__/testRuntime.spec.ts [moved from packages/runtime-test/__tests__/testRenderer.spec.ts with 98% similarity]
packages/runtime-test/src/index.ts
packages/scheduler/src/experimental.ts
packages/scheduler/src/patchNodeOps.ts [new file with mode: 0644]

index 20b4ad765b5077ad03f412db4fe1449be9a2c8ec..9cc625ac45bfda2163f029d1a6ead83c46857125 100644 (file)
@@ -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
 
index 29d1073b5f8370111c876e8417e59820ded3e989..55f9a4885c35440f095d8deffd68e85f1438a460 100644 (file)
@@ -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(`<div>0</div>`)
 
     triggerEvent(counter.$el, 'click')
@@ -40,7 +40,7 @@ describe('hooks', () => {
       }
     }
 
-    const counter = renderIntsance(Counter)
+    const counter = await renderInstance(Counter)
     expect(serialize(counter.$el)).toBe(`<div>0</div>`)
 
     triggerEvent(counter.$el, 'click')
@@ -71,7 +71,7 @@ describe('hooks', () => {
       }
     }
 
-    const counter = renderIntsance(Counter)
+    const counter = await renderInstance(Counter)
     expect(serialize(counter.$el)).toBe(`<div>0</div>`)
 
     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()
index 55b3a58cfe43c4e6c5326baf0fb7854a2dae8f13..61ca8058f18a3845557b6250cb978e21a2527a2c 100644 (file)
@@ -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
index f2e05398211e34928eca47d3ac158e07ec2d9c5d..bc6d5cbd9ae837a8017c6f600ddaeeb4fbe5517a 100644 (file)
@@ -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(`<div>1<div>A1</div><div>B1</div></div>`)
 
     app.count++
@@ -38,7 +38,7 @@ describe('memoize', () => {
       }
     }
 
-    const app = renderIntsance(App)
+    const app = await renderInstance(App)
     expect(serialize(app.$el)).toBe(`<div>2</div>`)
 
     app.foo++
index 322115c1cc7d6f9e2e2a4cf89121e7cf32225cbd..119b6536519fe640f7faa9adb4772d8c1ba46aeb 100644 (file)
@@ -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)
index 59c3ddb52cd3a416406d6dc0dba6851ebb206bea..c7dda26ba12bfc91d808c2512c111de8f64b9649 100644 (file)
@@ -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<T extends Component>(
   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)
 }
index 991092db88b2e9c4070c25ed74496c24a9525b16..3fe1b4d512f5504021d75561df865f7d9ffc1c9c 100644 (file)
@@ -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 }
index e2eb425957fd5ac747f30d3e81bdc4b8cd2930fe..1205d5fafcb65de23e815a77ae867d72810cb36a 100644 (file)
@@ -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<number, string> = {
   [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<number, string> = {
     '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<any>).catch(err => {
+        handleError(err, instanceProxy._self, type)
+      })
+    }
+  } catch (err) {
+    handleError(err, instanceProxy._self, type)
+  }
+}
+
 export function handleError(
   err: Error,
   instance: ComponentInstance | VNode | null,
index 615e1818c37b3b28ad7bee9c39244d275a78306c..38e3532650d46389658a2c27b3cdb9ff7dbe3d24 100644 (file)
@@ -12,7 +12,7 @@ const { render: _render } = createRenderer({
 type publicRender = (
   node: {} | null,
   container: HTMLElement
-) => Component | null
+) => Promise<Component | null>
 export const render = _render as publicRender
 
 // re-export everything from core
similarity index 98%
rename from packages/runtime-test/__tests__/testRenderer.spec.ts
rename to packages/runtime-test/__tests__/testRuntime.spec.ts
index 9e04db18640e42779b0eff315266ad1bbf432247..0948046578f1cd0857aee9c43b58178cb8443d04 100644 (file)
@@ -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()
index 0c90181649a856f3014128c8f1bd5b324f8902e0..5e1d35a98661188b5974d58b8c0f4142ccc1937c 100644 (file)
@@ -15,7 +15,7 @@ const { render: _render } = createRenderer({
 type publicRender = (
   node: {} | null,
   container: TestElement
-) => Component | null
+) => Promise<Component | null>
 export const render = _render as publicRender
 
 export function createInstance<T extends Component>(
@@ -25,10 +25,10 @@ export function createInstance<T extends Component>(
   return createComponentInstance(h(Class, props)).$proxy as any
 }
 
-export function renderIntsance<T extends Component>(
+export function renderInstance<T extends Component>(
   Class: new () => T,
   props?: any
-): T {
+): Promise<T> {
   return render(h(Class, props), nodeOps.createElement('div')) as any
 }
 
index ef10d3aeb008af30339f2d55673057145822744d..7c4a0ed5b75b57fe310e494c160319df480e141a 100644 (file)
@@ -1,60 +1,34 @@
-import { NodeOps } from '@vue/runtime-core'
-import { nodeOps } from '../../runtime-dom/src/nodeOps'
+import { Op, setCurrentOps } from './patchNodeOps'
+
+interface Job extends Function {
+  ops: Op[]
+  post: Function | null
+  expiration: number
+}
 
 const enum Priorities {
   NORMAL = 500
 }
 
-const frameBudget = 1000 / 60
+type ErrorHandler = (err: Error) => any
 
 let start: number = 0
-let currentOps: Op[]
-
 const getNow = () => window.performance.now()
+const frameBudget = __JSDOM__ ? Infinity : 1000 / 60
 
-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[]]
+const patchQueue: Job[] = []
+const commitQueue: Job[] = []
+const postCommitQueue: Function[] = []
+const nextTickQueue: Function[] = []
 
-interface Job extends Function {
-  ops: Op[]
-  post: Function | null
-  expiration: number
-}
+let globalHandler: ErrorHandler
+const pendingRejectors: ErrorHandler[] = []
 
 // Microtask for batching state mutations
 const p = Promise.resolve()
 
-export function nextTick(fn?: () => void): Promise<void> {
-  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<T>(fn?: () => T): Promise<T> {
+  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 (file)
index 0000000..0ac25cc
--- /dev/null
@@ -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)
+      }
+    }
+  }
+})