diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 41997f36..fcc16869 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -42,7 +42,6 @@ export interface ComponentInstance

extends InternalComponent { data?(): Partial render(props: Readonly

, slots: Slots, attrs: Data): any - renderError?(e: Error): any renderTracked?(e: DebuggerEvent): void renderTriggered?(e: DebuggerEvent): void beforeCreate?(): void @@ -56,7 +55,8 @@ export interface ComponentInstance

extends InternalComponent { errorCaptured?(): ( err: Error, type: ErrorTypes, - target: ComponentInstance + instance: ComponentInstance | null, + vnode: VNode ) => boolean | void activated?(): void deactivated?(): void diff --git a/packages/core/src/componentUtils.ts b/packages/core/src/componentUtils.ts index a902ad95..d50a0dee 100644 --- a/packages/core/src/componentUtils.ts +++ b/packages/core/src/componentUtils.ts @@ -2,10 +2,15 @@ import { VNodeFlags } from './flags' import { EMPTY_OBJ } from './utils' import { h } from './h' import { VNode, MountedVNode, createFragment } from './vdom' -import { Component, ComponentInstance, ComponentClass } from './component' +import { + Component, + ComponentInstance, + ComponentClass, + FunctionalComponent +} from './component' import { createTextVNode, cloneVNode } from './vdom' import { initializeState } from './componentState' -import { initializeProps } from './componentProps' +import { initializeProps, resolveProps } from './componentProps' import { initializeComputed, resolveComputedOptions, @@ -104,19 +109,24 @@ export function renderInstanceRoot(instance: ComponentInstance): VNode { instance.$slots, instance.$attrs ) - } catch (e1) { - handleError(e1, instance, ErrorTypes.RENDER) - if (__DEV__ && instance.renderError) { - try { - vnode = instance.renderError.call(instance.$proxy, e1) - } catch (e2) { - handleError(e2, instance, ErrorTypes.RENDER_ERROR) - } - } + } catch (err) { + handleError(err, instance, ErrorTypes.RENDER) } return normalizeComponentRoot(vnode, instance.$parentVNode) } +export function renderFunctionalRoot(vnode: VNode): VNode { + const render = vnode.tag as FunctionalComponent + const { props, attrs } = resolveProps(vnode.data, render.props) + let subTree + try { + subTree = render(props, vnode.slots || EMPTY_OBJ, attrs || EMPTY_OBJ) + } catch (err) { + handleError(err, vnode, ErrorTypes.RENDER) + } + return normalizeComponentRoot(subTree, vnode) +} + export function teardownComponentInstance(instance: ComponentInstance) { if (instance._unmounted) { return @@ -132,7 +142,7 @@ export function teardownComponentInstance(instance: ComponentInstance) { teardownWatch(instance) } -export function normalizeComponentRoot( +function normalizeComponentRoot( vnode: any, componentVNode: VNode | null ): VNode { diff --git a/packages/core/src/createRenderer.ts b/packages/core/src/createRenderer.ts index 49cd3d1d..13624023 100644 --- a/packages/core/src/createRenderer.ts +++ b/packages/core/src/createRenderer.ts @@ -16,15 +16,16 @@ import { FunctionalComponent, ComponentClass } from './component' -import { updateProps, resolveProps } from './componentProps' +import { updateProps } from './componentProps' import { renderInstanceRoot, + renderFunctionalRoot, createComponentInstance, teardownComponentInstance, - normalizeComponentRoot, shouldUpdateFunctionalComponent } from './componentUtils' import { KeepAliveSymbol } from './optional/keepAlive' +import { pushContext, popContext } from './warning' interface NodeOps { createElement: (tag: string, isSVG?: boolean) => any @@ -118,10 +119,8 @@ export function createRenderer(options: RendererOptions) { const { flags } = vnode if (flags & VNodeFlags.ELEMENT) { mountElement(vnode, container, contextVNode, isSVG, endNode) - } else if (flags & VNodeFlags.COMPONENT_STATEFUL) { - mountStatefulComponent(vnode, container, contextVNode, isSVG, endNode) - } else if (flags & VNodeFlags.COMPONENT_FUNCTIONAL) { - mountFunctionalComponent(vnode, container, contextVNode, isSVG, endNode) + } else if (flags & VNodeFlags.COMPONENT) { + mountComponent(vnode, container, contextVNode, isSVG, endNode) } else if (flags & VNodeFlags.TEXT) { mountText(vnode, container, endNode) } else if (flags & VNodeFlags.FRAGMENT) { @@ -198,7 +197,7 @@ export function createRenderer(options: RendererOptions) { }) } - function mountStatefulComponent( + function mountComponent( vnode: VNode, container: RenderNode | null, contextVNode: MountedVNode | null, @@ -206,6 +205,27 @@ export function createRenderer(options: RendererOptions) { endNode: RenderNode | null ) { vnode.contextVNode = contextVNode + if (__DEV__) { + pushContext(vnode) + } + const { flags } = vnode + if (flags & VNodeFlags.COMPONENT_STATEFUL) { + mountStatefulComponent(vnode, container, contextVNode, isSVG, endNode) + } else { + mountFunctionalComponent(vnode, container, contextVNode, isSVG, endNode) + } + if (__DEV__) { + popContext() + } + } + + function mountStatefulComponent( + vnode: VNode, + container: RenderNode | null, + contextVNode: MountedVNode | null, + isSVG: boolean, + endNode: RenderNode | null + ) { if (vnode.flags & VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE) { // kept-alive activateComponentInstance(vnode, container, endNode) @@ -228,14 +248,7 @@ export function createRenderer(options: RendererOptions) { isSVG: boolean, endNode: RenderNode | null ) { - vnode.contextVNode = contextVNode - const { tag, data, slots } = vnode - const render = tag as FunctionalComponent - const { props, attrs } = resolveProps(data, render.props) - const subTree = (vnode.children = normalizeComponentRoot( - render(props, slots || EMPTY_OBJ, attrs || EMPTY_OBJ), - vnode - )) + const subTree = (vnode.children = renderFunctionalRoot(vnode)) mount(subTree, container, vnode as MountedVNode, isSVG, endNode) vnode.el = subTree.el as RenderNode } @@ -443,6 +456,9 @@ export function createRenderer(options: RendererOptions) { contextVNode: MountedVNode | null, isSVG: boolean ) { + if (__DEV__) { + pushContext(nextVNode) + } nextVNode.contextVNode = contextVNode const { tag, flags } = nextVNode if (tag !== prevVNode.tag) { @@ -458,6 +474,9 @@ export function createRenderer(options: RendererOptions) { isSVG ) } + if (__DEV__) { + popContext() + } } function patchStatefulComponent(prevVNode: MountedVNode, nextVNode: VNode) { @@ -512,11 +531,7 @@ export function createRenderer(options: RendererOptions) { } if (shouldUpdate) { - const { props, attrs } = resolveProps(nextData, render.props) - const nextTree = (nextVNode.children = normalizeComponentRoot( - render(props, nextSlots || EMPTY_OBJ, attrs || EMPTY_OBJ), - nextVNode - )) + const nextTree = (nextVNode.children = renderFunctionalRoot(nextVNode)) patch(prevTree, nextTree, container, nextVNode as MountedVNode, isSVG) nextVNode.el = nextTree.el } else if (prevTree.flags & VNodeFlags.COMPONENT) { @@ -630,7 +645,7 @@ export function createRenderer(options: RendererOptions) { isSVG: boolean ) { const refNode = platformNextSibling(getVNodeLastEl(prevVNode)) - reinsertVNode(prevVNode, container) + removeVNode(prevVNode, container) mount(nextVNode, container, contextVNode, isSVG, refNode) } @@ -657,10 +672,10 @@ export function createRenderer(options: RendererOptions) { ) break case ChildrenFlags.NO_CHILDREN: - reinsertVNode(prevChildren as MountedVNode, container) + removeVNode(prevChildren as MountedVNode, container) break default: - reinsertVNode(prevChildren as MountedVNode, container) + removeVNode(prevChildren as MountedVNode, container) mountArrayChildren( nextChildren as VNode[], container, @@ -781,7 +796,7 @@ export function createRenderer(options: RendererOptions) { } } else if (prevLength > nextLength) { for (i = commonLength; i < prevLength; i++) { - reinsertVNode(prevChildren[i], container) + removeVNode(prevChildren[i], container) } } } @@ -856,7 +871,7 @@ export function createRenderer(options: RendererOptions) { } } else if (j > nextEnd) { while (j <= prevEnd) { - reinsertVNode(prevChildren[j++], container) + removeVNode(prevChildren[j++], container) } } else { let prevStart = j @@ -885,7 +900,7 @@ export function createRenderer(options: RendererOptions) { if (canRemoveWholeContent) { canRemoveWholeContent = false while (i > prevStart) { - reinsertVNode(prevChildren[prevStart++], container) + removeVNode(prevChildren[prevStart++], container) } } if (pos > j) { @@ -902,10 +917,10 @@ export function createRenderer(options: RendererOptions) { } } if (!canRemoveWholeContent && j > nextEnd) { - reinsertVNode(prevVNode, container) + removeVNode(prevVNode, container) } } else if (!canRemoveWholeContent) { - reinsertVNode(prevVNode, container) + removeVNode(prevVNode, container) } } } else { @@ -927,7 +942,7 @@ export function createRenderer(options: RendererOptions) { if (canRemoveWholeContent) { canRemoveWholeContent = false while (i > prevStart) { - reinsertVNode(prevChildren[prevStart++], container) + removeVNode(prevChildren[prevStart++], container) } } nextVNode = nextChildren[j] @@ -943,10 +958,10 @@ export function createRenderer(options: RendererOptions) { patch(prevVNode, nextVNode, container, contextVNode, isSVG) patched++ } else if (!canRemoveWholeContent) { - reinsertVNode(prevVNode, container) + removeVNode(prevVNode, container) } } else if (!canRemoveWholeContent) { - reinsertVNode(prevVNode, container) + removeVNode(prevVNode, container) } } } @@ -1074,7 +1089,7 @@ export function createRenderer(options: RendererOptions) { null ) } else if (childFlags === ChildrenFlags.SINGLE_VNODE) { - reinsertVNode(children as MountedVNode, vnode.tag as RenderNode) + removeVNode(children as MountedVNode, vnode.tag as RenderNode) } } if (ref) { @@ -1096,21 +1111,21 @@ export function createRenderer(options: RendererOptions) { } } - function reinsertVNode(vnode: MountedVNode, container: RenderNode) { + function removeVNode(vnode: MountedVNode, container: RenderNode) { unmount(vnode) const { el, flags, children, childFlags } = vnode if (container && el) { if (flags & VNodeFlags.FRAGMENT) { switch (childFlags) { case ChildrenFlags.SINGLE_VNODE: - reinsertVNode(children as MountedVNode, container) + removeVNode(children as MountedVNode, container) break case ChildrenFlags.NO_CHILDREN: platformRemoveChild(container, el) break default: for (let i = 0; i < (children as MountedVNode[]).length; i++) { - reinsertVNode((children as MountedVNode[])[i], container) + removeVNode((children as MountedVNode[])[i], container) } } } else { @@ -1130,7 +1145,7 @@ export function createRenderer(options: RendererOptions) { platformClearContent(container) } else { for (let i = 0; i < children.length; i++) { - reinsertVNode(children[i], container) + removeVNode(children[i], container) } } } @@ -1220,6 +1235,9 @@ export function createRenderer(options: RendererOptions) { instance: ComponentInstance, isSVG: boolean ) { + if (__DEV__ && instance.$parentVNode) { + pushContext(instance.$parentVNode as VNode) + } const prevVNode = instance.$vnode if (instance.beforeUpdate) { @@ -1275,6 +1293,10 @@ export function createRenderer(options: RendererOptions) { } }) } + + if (__DEV__ && instance.$parentVNode) { + popContext() + } } function unmountComponentInstance(instance: ComponentInstance) { @@ -1380,7 +1402,7 @@ export function createRenderer(options: RendererOptions) { patch(prevVNode, vnode, container, null, false) container.vnode = vnode } else { - reinsertVNode(prevVNode, container) + removeVNode(prevVNode, container) container.vnode = null } } diff --git a/packages/core/src/errorHandling.ts b/packages/core/src/errorHandling.ts index c740457f..863485e4 100644 --- a/packages/core/src/errorHandling.ts +++ b/packages/core/src/errorHandling.ts @@ -1,4 +1,7 @@ import { ComponentInstance } from './component' +import { warn } from './warning' +import { VNode } from './vdom' +import { VNodeFlags } from './flags' export const enum ErrorTypes { BEFORE_CREATE = 1, @@ -11,7 +14,6 @@ export const enum ErrorTypes { DESTROYED, ERROR_CAPTURED, RENDER, - RENDER_ERROR, WATCH_CALLBACK, NATIVE_EVENT_HANDLER, COMPONENT_EVENT_HANDLER @@ -28,7 +30,6 @@ const ErrorTypeStrings: Record = { [ErrorTypes.DESTROYED]: 'destroyed lifecycle hook', [ErrorTypes.ERROR_CAPTURED]: 'errorCaptured lifecycle hook', [ErrorTypes.RENDER]: 'render function', - [ErrorTypes.RENDER_ERROR]: 'renderError function', [ErrorTypes.WATCH_CALLBACK]: 'watcher callback', [ErrorTypes.NATIVE_EVENT_HANDLER]: 'native event handler', [ErrorTypes.COMPONENT_EVENT_HANDLER]: 'component event handler' @@ -36,21 +37,39 @@ const ErrorTypeStrings: Record = { export function handleError( err: Error, - instance: ComponentInstance, + instance: ComponentInstance | VNode, type: ErrorTypes ) { - let cur = instance - while (cur.$parent) { - cur = cur.$parent + const isFunctional = (instance as VNode)._isVNode + let cur: ComponentInstance | null = null + if (isFunctional) { + let vnode = instance as VNode | null + while (vnode && !(vnode.flags & VNodeFlags.COMPONENT_STATEFUL)) { + vnode = vnode.contextVNode + } + if (vnode) { + cur = vnode.children as ComponentInstance + } + } else { + cur = (instance as ComponentInstance).$parent + } + while (cur) { const handler = cur.errorCaptured if (handler) { try { - const captured = handler.call(cur, err, type, instance) + const captured = handler.call( + cur, + err, + type, + isFunctional ? null : instance, + isFunctional ? instance : (instance as ComponentInstance).$parentVNode + ) if (captured) return } catch (err2) { logError(err2, ErrorTypes.ERROR_CAPTURED) } } + cur = cur.$parent } logError(err, type) } @@ -58,7 +77,7 @@ export function handleError( function logError(err: Error, type: ErrorTypes) { if (__DEV__) { const info = ErrorTypeStrings[type] - console.warn(`Unhandled error${info ? ` in ${info}` : ``}:`) + warn(`Unhandled error${info ? ` in ${info}` : ``}`) console.error(err) } else { throw err diff --git a/packages/core/src/warning.ts b/packages/core/src/warning.ts index 4d42ddca..32ed0616 100644 --- a/packages/core/src/warning.ts +++ b/packages/core/src/warning.ts @@ -1,22 +1,14 @@ import { ComponentType, ComponentClass, FunctionalComponent } from './component' import { EMPTY_OBJ } from './utils' +import { VNode } from './vdom' -// TODO push vnodes instead -// component vnodes get a new property (contextVNode) which points to the -// parent component (stateful or functional) -// this way we can use any component vnode to construct a trace that inludes -// functional and stateful components. +let stack: VNode[] = [] -// in createRenderer, parentComponent should be replced by ctx -// $parent logic should also accomodate - -let stack: ComponentType[] = [] - -export function pushComponent(c: ComponentType) { - stack.push(c) +export function pushContext(vnode: VNode) { + stack.push(vnode) } -export function popComponent() { +export function popContext() { stack.pop() } @@ -26,33 +18,42 @@ export function warn(msg: string) { } function getComponentTrace(): string { - const current = stack[stack.length - 1] + let current: VNode | null | undefined = stack[stack.length - 1] if (!current) { - return '' + return '\nat ' } - // we can't just use the stack itself, because it will be incomplete - // during updates - // check recursive + + // we can't just use the stack because it will be incomplete during updates + // that did not start from the root. Re-construct the parent chain using + // contextVNode information. const normlaizedStack: Array<{ - type: ComponentType + type: VNode recurseCount: number }> = [] - stack.forEach(c => { - const last = normlaizedStack[normlaizedStack.length - 1] - if (last && last.type === c) { + + while (current) { + const last = normlaizedStack[0] + if (last && last.type === current) { last.recurseCount++ } else { - normlaizedStack.push({ type: c, recurseCount: 0 }) + normlaizedStack.unshift({ + type: current, + recurseCount: 0 + }) } - }) + current = current.contextVNode + } + return ( - `\n\nfound in\n\n` + + `\nat ` + normlaizedStack .map(({ type, recurseCount }, i) => { - const padding = i === 0 ? '---> ' : ' '.repeat(5 + i * 2) + const padding = i === 0 ? '' : ' '.repeat(i + 1) const postfix = recurseCount > 0 ? `... (${recurseCount} recursive calls)` : `` - return padding + formatComponentName(type) + postfix + return ( + padding + formatComponentName(type.tag as ComponentType) + postfix + ) }) .join('\n') ) @@ -66,12 +67,14 @@ function formatComponentName(c: ComponentType, includeFile?: boolean): string { let name: string let file: string | null = null - if (c.prototype.render) { + if (c.prototype && c.prototype.render) { + // stateful const cc = c as ComponentClass const options = cc.options || EMPTY_OBJ name = options.displayName || cc.name file = options.__file } else { + // functional const fc = c as FunctionalComponent name = fc.displayName || fc.name }