]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: experimental time-slicing
authorEvan You <yyx990803@gmail.com>
Wed, 31 Oct 2018 21:58:06 +0000 (06:58 +0900)
committerEvan You <yyx990803@gmail.com>
Fri, 2 Nov 2018 21:31:30 +0000 (06:31 +0900)
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/errorHandling.ts
packages/runtime-core/src/index.ts
packages/runtime-dom/src/nodeOps.ts
packages/scheduler/src/experimental.ts [new file with mode: 0644]
packages/scheduler/src/index.ts
packages/scheduler/src/sync.ts [new file with mode: 0644]

index d749f497a9694050f66b0ad9f139288fbac0ea74..e8317c398fef0f6f20c9ef7b77ae6809c5eb647b 100644 (file)
@@ -23,7 +23,7 @@ import { KeepAliveSymbol } from './optional/keepAlive'
 import { pushWarningContext, popWarningContext } from './warning'
 import { handleError, ErrorTypes } from './errorHandling'
 
-interface NodeOps {
+export interface NodeOps {
   createElement: (tag: string, isSVG?: boolean) => any
   createText: (text: string) => any
   setText: (node: any, text: string) => void
@@ -36,7 +36,7 @@ interface NodeOps {
   querySelector: (selector: string) => any
 }
 
-interface PatchDataFunction {
+export interface PatchDataFunction {
   (
     el: any,
     key: string,
@@ -51,7 +51,7 @@ interface PatchDataFunction {
   ): void
 }
 
-interface RendererOptions {
+export interface RendererOptions {
   nodeOps: NodeOps
   patchData: PatchDataFunction
   teardownVNode?: (vnode: VNode) => void
@@ -221,7 +221,15 @@ export function createRenderer(options: RendererOptions) {
       // kept-alive
       activateComponentInstance(vnode, container, endNode)
     } else {
-      mountComponentInstance(vnode, container, isSVG, endNode)
+      queueJob(
+        () => {
+          mountComponentInstance(vnode, container, isSVG, endNode)
+        },
+        flushHooks,
+        err => {
+          handleError(err, vnode.contextVNode as VNode, ErrorTypes.SCHEDULER)
+        }
+      )
     }
   }
 
@@ -311,7 +319,7 @@ export function createRenderer(options: RendererOptions) {
   // patching ------------------------------------------------------------------
 
   function patchData(
-    el: RenderNode,
+    el: RenderNode | (() => RenderNode),
     key: string,
     prevValue: any,
     nextValue: any,
@@ -323,7 +331,7 @@ export function createRenderer(options: RendererOptions) {
       return
     }
     platformPatchData(
-      el,
+      typeof el === 'function' ? el() : el,
       key,
       prevValue,
       nextValue,
@@ -1360,10 +1368,7 @@ export function createRenderer(options: RendererOptions) {
 
   // API -----------------------------------------------------------------------
 
-  function render(
-    vnode: VNode | null,
-    container: any
-  ): ComponentInstance | null {
+  function render(vnode: VNode | null, container: any) {
     const prevVNode = container.vnode
     if (prevVNode == null) {
       if (vnode) {
@@ -1379,10 +1384,10 @@ export function createRenderer(options: RendererOptions) {
         container.vnode = null
       }
     }
-    flushHooks()
-    return vnode && vnode.flags & VNodeFlags.COMPONENT_STATEFUL
-      ? (vnode.children as ComponentInstance).$proxy
-      : null
+    // flushHooks()
+    // return vnode && vnode.flags & VNodeFlags.COMPONENT_STATEFUL
+    // ? (vnode.children as ComponentInstance).$proxy
+    // : null
   }
 
   return { render }
index 971938f13f1951fdab773101b98d33077eb0e1da..e2eb425957fd5ac747f30d3e81bdc4b8cd2930fe 100644 (file)
@@ -40,13 +40,15 @@ const ErrorTypeStrings: Record<number, string> = {
 
 export function handleError(
   err: Error,
-  instance: ComponentInstance | VNode,
+  instance: ComponentInstance | VNode | null,
   type: ErrorTypes
 ) {
-  const isFunctional = (instance as VNode)._isVNode
-  const contextVNode = (isFunctional
-    ? instance
-    : (instance as ComponentInstance).$parentVNode) as VNode | null
+  const isFunctional = instance && (instance as VNode)._isVNode
+  const contextVNode =
+    instance &&
+    ((isFunctional
+      ? instance
+      : (instance as ComponentInstance).$parentVNode) as VNode | null)
   let cur: ComponentInstance | null = null
   if (isFunctional) {
     let vnode = instance as VNode | null
@@ -56,10 +58,11 @@ export function handleError(
     if (vnode) {
       cur = vnode.children as ComponentInstance
     }
-  } else {
+  } else if (instance) {
     cur = (instance as ComponentInstance).$parent
   }
   while (cur) {
+    cur = cur._self
     const handler = cur.errorCaptured
     if (handler) {
       try {
index 47b9c81daccdc779b35e587ecd7cbe35ab2c26ef..fc5d1ca3ab6b35ed2ad2b3f9f3cb56e6439cdca5 100644 (file)
@@ -2,7 +2,12 @@
 export { h, Fragment, Portal } from './h'
 export { Component } from './component'
 export { cloneVNode, createPortal, createFragment } from './vdom'
-export { createRenderer } from './createRenderer'
+export {
+  createRenderer,
+  NodeOps,
+  PatchDataFunction,
+  RendererOptions
+} from './createRenderer'
 
 // Observer API
 export * from '@vue/observer'
index 0d6a358fc2eff1959dec472147c2deb952924457..b53bb1ed33c056ac4f720d60c97d4e0463860be0 100644 (file)
@@ -1,6 +1,8 @@
+import { NodeOps } from '@vue/runtime-core'
+
 const svgNS = 'http://www.w3.org/2000/svg'
 
-export const nodeOps = {
+export const nodeOps: NodeOps = {
   createElement: (tag: string, isSVG?: boolean): Element =>
     isSVG ? document.createElementNS(svgNS, tag) : document.createElement(tag),
 
diff --git a/packages/scheduler/src/experimental.ts b/packages/scheduler/src/experimental.ts
new file mode 100644 (file)
index 0000000..e6aaf63
--- /dev/null
@@ -0,0 +1,176 @@
+import { NodeOps } from '@vue/runtime-core'
+import { nodeOps } from '../../runtime-dom/src/nodeOps'
+
+const enum Priorities {
+  NORMAL = 500
+}
+
+const frameBudget = 1000 / 60
+
+let currentOps: Op[]
+
+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[]) => {
+      if (currentOps) {
+        let res: any
+        return () => {
+          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[]]
+
+interface Job extends Function {
+  ops: Op[]
+  post: Function
+  expiration: number
+}
+
+// Microtask for batching state mutations
+const p = Promise.resolve()
+
+export function nextTick(fn?: () => void): Promise<void> {
+  return p.then(fn)
+}
+
+// Macrotask for time slicing
+const key = `__vueSchedulerTick`
+
+window.addEventListener(
+  'message',
+  event => {
+    if (event.source !== window || event.data !== key) {
+      return
+    }
+    flush()
+  },
+  false
+)
+
+function flushAfterYield() {
+  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)
+  }
+}
+
+function commit({ ops }: Job) {
+  for (let i = 0; i < ops.length; i++) {
+    const [fn, ...args] = ops[i]
+    fn(...args)
+  }
+  ops.length = 0
+}
+
+function invalidate(job: Job) {
+  job.ops.length = 0
+}
+
+let hasPendingFlush = false
+
+export function queueJob(
+  rawJob: Function,
+  postJob: Function,
+  onError?: (reason: any) => void
+) {
+  const job = rawJob as Job
+  job.post = postJob
+  job.ops = job.ops || []
+  // 1. let's see if this invalidates any work that
+  // has already been done.
+  const commitIndex = commitQueue.indexOf(job)
+  if (commitIndex > -1) {
+    // invalidated. remove from commit queue
+    // and move it back to the patch queue
+    commitQueue.splice(commitIndex, 1)
+    invalidate(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)
+        break
+      }
+    }
+  } else if (patchQueue.indexOf(job) === -1) {
+    // a new job
+    job.expiration = performance.now() + Priorities.NORMAL
+    patchQueue.push(job)
+  }
+
+  if (!hasPendingFlush) {
+    hasPendingFlush = true
+    const p = nextTick(flush)
+    if (onError) p.catch(onError)
+  }
+}
+
+function flush() {
+  let job
+  let start = window.performance.now()
+  while (true) {
+    job = patchQueue.shift()
+    if (job) {
+      patch(job)
+    } else {
+      break
+    }
+    const now = performance.now()
+    if (now - start > frameBudget && job.expiration > now) {
+      break
+    }
+  }
+
+  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)
+      }
+    }
+    while ((job = postQueue.shift())) {
+      job()
+    }
+    if (patchQueue.length > 0) {
+      return flushAfterYield()
+    }
+    hasPendingFlush = false
+  } else {
+    // got more job to do
+    flushAfterYield()
+  }
+}
index 7918d17c8fe38e3011891253695c4bf6bcd80c42..d07e7c3e8729b931c38a47bb8bca05ea0c11bce2 100644 (file)
@@ -1,68 +1,2 @@
-const queue: Array<() => void> = []
-const postFlushCbs: Array<() => void> = []
-const p = Promise.resolve()
-
-let isFlushing = false
-
-export function nextTick(fn?: () => void): Promise<void> {
-  return p.then(fn)
-}
-
-export function queueJob(
-  job: () => void,
-  postFlushCb?: () => void,
-  onError?: (err: Error) => void
-) {
-  if (queue.indexOf(job) === -1) {
-    queue.push(job)
-    if (!isFlushing) {
-      const p = nextTick(flushJobs)
-      if (onError) p.catch(onError)
-    }
-  }
-  if (postFlushCb && postFlushCbs.indexOf(postFlushCb) === -1) {
-    postFlushCbs.push(postFlushCb)
-  }
-}
-
-const RECURSION_LIMIT = 100
-type JobCountMap = Map<Function, number>
-
-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()
-  }
-  const cbs = postFlushCbs.slice()
-  postFlushCbs.length = 0
-  for (let i = 0; i < cbs.length; i++) {
-    cbs[i]()
-  }
-  isFlushing = false
-  // some postFlushCb queued jobs!
-  // keep flushing until it drains.
-  if (queue.length) {
-    flushJobs(seenJobs)
-  }
-}
+export * from './experimental'
+// export * from './sync'
diff --git a/packages/scheduler/src/sync.ts b/packages/scheduler/src/sync.ts
new file mode 100644 (file)
index 0000000..0ab693c
--- /dev/null
@@ -0,0 +1,69 @@
+const queue: Array<() => void> = []
+const postFlushCbs: Array<() => void> = []
+const p = Promise.resolve()
+
+let isFlushing = false
+
+export function nextTick(fn?: () => void): Promise<void> {
+  return p.then(fn)
+}
+
+export function queueJob(
+  job: () => void,
+  postFlushCb?: () => void,
+  onError?: (err: Error) => void
+) {
+  if (queue.indexOf(job) === -1) {
+    queue.push(job)
+    if (!isFlushing) {
+      isFlushing = true
+      const p = nextTick(flushJobs)
+      if (onError) p.catch(onError)
+    }
+  }
+  if (postFlushCb && postFlushCbs.indexOf(postFlushCb) === -1) {
+    postFlushCbs.push(postFlushCb)
+  }
+}
+
+const RECURSION_LIMIT = 100
+type JobCountMap = Map<Function, number>
+
+function flushJobs(seenJobs?: JobCountMap) {
+  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()
+  }
+  const cbs = postFlushCbs.slice()
+  postFlushCbs.length = 0
+  for (let i = 0; i < cbs.length; i++) {
+    cbs[i]()
+  }
+  // some postFlushCb queued jobs!
+  // keep flushing until it drains.
+  if (queue.length) {
+    flushJobs(seenJobs)
+  } else {
+    isFlushing = false
+  }
+}