diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index d749f497..e8317c39 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -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 } diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index 971938f1..e2eb4259 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -40,13 +40,15 @@ const ErrorTypeStrings: Record = { 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 { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 47b9c81d..fc5d1ca3 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -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' diff --git a/packages/runtime-dom/src/nodeOps.ts b/packages/runtime-dom/src/nodeOps.ts index 0d6a358f..b53bb1ed 100644 --- a/packages/runtime-dom/src/nodeOps.ts +++ b/packages/runtime-dom/src/nodeOps.ts @@ -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 index 00000000..e6aaf634 --- /dev/null +++ b/packages/scheduler/src/experimental.ts @@ -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 { + 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() + } +} diff --git a/packages/scheduler/src/index.ts b/packages/scheduler/src/index.ts index 7918d17c..d07e7c3e 100644 --- a/packages/scheduler/src/index.ts +++ b/packages/scheduler/src/index.ts @@ -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 { - 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 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 index 00000000..0ab693c4 --- /dev/null +++ b/packages/scheduler/src/sync.ts @@ -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 { + 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 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 + } +}