feat: warning traces & error handling for functional render

This commit is contained in:
Evan You 2018-10-11 17:14:39 -04:00
parent 3a7bbecb22
commit 5327abb249
5 changed files with 141 additions and 87 deletions

View File

@ -42,7 +42,6 @@ export interface ComponentInstance<P = {}, D = {}> extends InternalComponent {
data?(): Partial<D> data?(): Partial<D>
render(props: Readonly<P>, slots: Slots, attrs: Data): any render(props: Readonly<P>, slots: Slots, attrs: Data): any
renderError?(e: Error): any
renderTracked?(e: DebuggerEvent): void renderTracked?(e: DebuggerEvent): void
renderTriggered?(e: DebuggerEvent): void renderTriggered?(e: DebuggerEvent): void
beforeCreate?(): void beforeCreate?(): void
@ -56,7 +55,8 @@ export interface ComponentInstance<P = {}, D = {}> extends InternalComponent {
errorCaptured?(): ( errorCaptured?(): (
err: Error, err: Error,
type: ErrorTypes, type: ErrorTypes,
target: ComponentInstance instance: ComponentInstance | null,
vnode: VNode
) => boolean | void ) => boolean | void
activated?(): void activated?(): void
deactivated?(): void deactivated?(): void

View File

@ -2,10 +2,15 @@ import { VNodeFlags } from './flags'
import { EMPTY_OBJ } from './utils' import { EMPTY_OBJ } from './utils'
import { h } from './h' import { h } from './h'
import { VNode, MountedVNode, createFragment } from './vdom' 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 { createTextVNode, cloneVNode } from './vdom'
import { initializeState } from './componentState' import { initializeState } from './componentState'
import { initializeProps } from './componentProps' import { initializeProps, resolveProps } from './componentProps'
import { import {
initializeComputed, initializeComputed,
resolveComputedOptions, resolveComputedOptions,
@ -104,19 +109,24 @@ export function renderInstanceRoot(instance: ComponentInstance): VNode {
instance.$slots, instance.$slots,
instance.$attrs instance.$attrs
) )
} catch (e1) { } catch (err) {
handleError(e1, instance, ErrorTypes.RENDER) handleError(err, instance, ErrorTypes.RENDER)
if (__DEV__ && instance.renderError) {
try {
vnode = instance.renderError.call(instance.$proxy, e1)
} catch (e2) {
handleError(e2, instance, ErrorTypes.RENDER_ERROR)
}
}
} }
return normalizeComponentRoot(vnode, instance.$parentVNode) 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) { export function teardownComponentInstance(instance: ComponentInstance) {
if (instance._unmounted) { if (instance._unmounted) {
return return
@ -132,7 +142,7 @@ export function teardownComponentInstance(instance: ComponentInstance) {
teardownWatch(instance) teardownWatch(instance)
} }
export function normalizeComponentRoot( function normalizeComponentRoot(
vnode: any, vnode: any,
componentVNode: VNode | null componentVNode: VNode | null
): VNode { ): VNode {

View File

@ -16,15 +16,16 @@ import {
FunctionalComponent, FunctionalComponent,
ComponentClass ComponentClass
} from './component' } from './component'
import { updateProps, resolveProps } from './componentProps' import { updateProps } from './componentProps'
import { import {
renderInstanceRoot, renderInstanceRoot,
renderFunctionalRoot,
createComponentInstance, createComponentInstance,
teardownComponentInstance, teardownComponentInstance,
normalizeComponentRoot,
shouldUpdateFunctionalComponent shouldUpdateFunctionalComponent
} from './componentUtils' } from './componentUtils'
import { KeepAliveSymbol } from './optional/keepAlive' import { KeepAliveSymbol } from './optional/keepAlive'
import { pushContext, popContext } from './warning'
interface NodeOps { interface NodeOps {
createElement: (tag: string, isSVG?: boolean) => any createElement: (tag: string, isSVG?: boolean) => any
@ -118,10 +119,8 @@ export function createRenderer(options: RendererOptions) {
const { flags } = vnode const { flags } = vnode
if (flags & VNodeFlags.ELEMENT) { if (flags & VNodeFlags.ELEMENT) {
mountElement(vnode, container, contextVNode, isSVG, endNode) mountElement(vnode, container, contextVNode, isSVG, endNode)
} else if (flags & VNodeFlags.COMPONENT_STATEFUL) { } else if (flags & VNodeFlags.COMPONENT) {
mountStatefulComponent(vnode, container, contextVNode, isSVG, endNode) mountComponent(vnode, container, contextVNode, isSVG, endNode)
} else if (flags & VNodeFlags.COMPONENT_FUNCTIONAL) {
mountFunctionalComponent(vnode, container, contextVNode, isSVG, endNode)
} else if (flags & VNodeFlags.TEXT) { } else if (flags & VNodeFlags.TEXT) {
mountText(vnode, container, endNode) mountText(vnode, container, endNode)
} else if (flags & VNodeFlags.FRAGMENT) { } else if (flags & VNodeFlags.FRAGMENT) {
@ -198,7 +197,7 @@ export function createRenderer(options: RendererOptions) {
}) })
} }
function mountStatefulComponent( function mountComponent(
vnode: VNode, vnode: VNode,
container: RenderNode | null, container: RenderNode | null,
contextVNode: MountedVNode | null, contextVNode: MountedVNode | null,
@ -206,6 +205,27 @@ export function createRenderer(options: RendererOptions) {
endNode: RenderNode | null endNode: RenderNode | null
) { ) {
vnode.contextVNode = contextVNode 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) { if (vnode.flags & VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE) {
// kept-alive // kept-alive
activateComponentInstance(vnode, container, endNode) activateComponentInstance(vnode, container, endNode)
@ -228,14 +248,7 @@ export function createRenderer(options: RendererOptions) {
isSVG: boolean, isSVG: boolean,
endNode: RenderNode | null endNode: RenderNode | null
) { ) {
vnode.contextVNode = contextVNode const subTree = (vnode.children = renderFunctionalRoot(vnode))
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
))
mount(subTree, container, vnode as MountedVNode, isSVG, endNode) mount(subTree, container, vnode as MountedVNode, isSVG, endNode)
vnode.el = subTree.el as RenderNode vnode.el = subTree.el as RenderNode
} }
@ -443,6 +456,9 @@ export function createRenderer(options: RendererOptions) {
contextVNode: MountedVNode | null, contextVNode: MountedVNode | null,
isSVG: boolean isSVG: boolean
) { ) {
if (__DEV__) {
pushContext(nextVNode)
}
nextVNode.contextVNode = contextVNode nextVNode.contextVNode = contextVNode
const { tag, flags } = nextVNode const { tag, flags } = nextVNode
if (tag !== prevVNode.tag) { if (tag !== prevVNode.tag) {
@ -458,6 +474,9 @@ export function createRenderer(options: RendererOptions) {
isSVG isSVG
) )
} }
if (__DEV__) {
popContext()
}
} }
function patchStatefulComponent(prevVNode: MountedVNode, nextVNode: VNode) { function patchStatefulComponent(prevVNode: MountedVNode, nextVNode: VNode) {
@ -512,11 +531,7 @@ export function createRenderer(options: RendererOptions) {
} }
if (shouldUpdate) { if (shouldUpdate) {
const { props, attrs } = resolveProps(nextData, render.props) const nextTree = (nextVNode.children = renderFunctionalRoot(nextVNode))
const nextTree = (nextVNode.children = normalizeComponentRoot(
render(props, nextSlots || EMPTY_OBJ, attrs || EMPTY_OBJ),
nextVNode
))
patch(prevTree, nextTree, container, nextVNode as MountedVNode, isSVG) patch(prevTree, nextTree, container, nextVNode as MountedVNode, isSVG)
nextVNode.el = nextTree.el nextVNode.el = nextTree.el
} else if (prevTree.flags & VNodeFlags.COMPONENT) { } else if (prevTree.flags & VNodeFlags.COMPONENT) {
@ -630,7 +645,7 @@ export function createRenderer(options: RendererOptions) {
isSVG: boolean isSVG: boolean
) { ) {
const refNode = platformNextSibling(getVNodeLastEl(prevVNode)) const refNode = platformNextSibling(getVNodeLastEl(prevVNode))
reinsertVNode(prevVNode, container) removeVNode(prevVNode, container)
mount(nextVNode, container, contextVNode, isSVG, refNode) mount(nextVNode, container, contextVNode, isSVG, refNode)
} }
@ -657,10 +672,10 @@ export function createRenderer(options: RendererOptions) {
) )
break break
case ChildrenFlags.NO_CHILDREN: case ChildrenFlags.NO_CHILDREN:
reinsertVNode(prevChildren as MountedVNode, container) removeVNode(prevChildren as MountedVNode, container)
break break
default: default:
reinsertVNode(prevChildren as MountedVNode, container) removeVNode(prevChildren as MountedVNode, container)
mountArrayChildren( mountArrayChildren(
nextChildren as VNode[], nextChildren as VNode[],
container, container,
@ -781,7 +796,7 @@ export function createRenderer(options: RendererOptions) {
} }
} else if (prevLength > nextLength) { } else if (prevLength > nextLength) {
for (i = commonLength; i < prevLength; i++) { 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) { } else if (j > nextEnd) {
while (j <= prevEnd) { while (j <= prevEnd) {
reinsertVNode(prevChildren[j++], container) removeVNode(prevChildren[j++], container)
} }
} else { } else {
let prevStart = j let prevStart = j
@ -885,7 +900,7 @@ export function createRenderer(options: RendererOptions) {
if (canRemoveWholeContent) { if (canRemoveWholeContent) {
canRemoveWholeContent = false canRemoveWholeContent = false
while (i > prevStart) { while (i > prevStart) {
reinsertVNode(prevChildren[prevStart++], container) removeVNode(prevChildren[prevStart++], container)
} }
} }
if (pos > j) { if (pos > j) {
@ -902,10 +917,10 @@ export function createRenderer(options: RendererOptions) {
} }
} }
if (!canRemoveWholeContent && j > nextEnd) { if (!canRemoveWholeContent && j > nextEnd) {
reinsertVNode(prevVNode, container) removeVNode(prevVNode, container)
} }
} else if (!canRemoveWholeContent) { } else if (!canRemoveWholeContent) {
reinsertVNode(prevVNode, container) removeVNode(prevVNode, container)
} }
} }
} else { } else {
@ -927,7 +942,7 @@ export function createRenderer(options: RendererOptions) {
if (canRemoveWholeContent) { if (canRemoveWholeContent) {
canRemoveWholeContent = false canRemoveWholeContent = false
while (i > prevStart) { while (i > prevStart) {
reinsertVNode(prevChildren[prevStart++], container) removeVNode(prevChildren[prevStart++], container)
} }
} }
nextVNode = nextChildren[j] nextVNode = nextChildren[j]
@ -943,10 +958,10 @@ export function createRenderer(options: RendererOptions) {
patch(prevVNode, nextVNode, container, contextVNode, isSVG) patch(prevVNode, nextVNode, container, contextVNode, isSVG)
patched++ patched++
} else if (!canRemoveWholeContent) { } else if (!canRemoveWholeContent) {
reinsertVNode(prevVNode, container) removeVNode(prevVNode, container)
} }
} else if (!canRemoveWholeContent) { } else if (!canRemoveWholeContent) {
reinsertVNode(prevVNode, container) removeVNode(prevVNode, container)
} }
} }
} }
@ -1074,7 +1089,7 @@ export function createRenderer(options: RendererOptions) {
null null
) )
} else if (childFlags === ChildrenFlags.SINGLE_VNODE) { } else if (childFlags === ChildrenFlags.SINGLE_VNODE) {
reinsertVNode(children as MountedVNode, vnode.tag as RenderNode) removeVNode(children as MountedVNode, vnode.tag as RenderNode)
} }
} }
if (ref) { 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) unmount(vnode)
const { el, flags, children, childFlags } = vnode const { el, flags, children, childFlags } = vnode
if (container && el) { if (container && el) {
if (flags & VNodeFlags.FRAGMENT) { if (flags & VNodeFlags.FRAGMENT) {
switch (childFlags) { switch (childFlags) {
case ChildrenFlags.SINGLE_VNODE: case ChildrenFlags.SINGLE_VNODE:
reinsertVNode(children as MountedVNode, container) removeVNode(children as MountedVNode, container)
break break
case ChildrenFlags.NO_CHILDREN: case ChildrenFlags.NO_CHILDREN:
platformRemoveChild(container, el) platformRemoveChild(container, el)
break break
default: default:
for (let i = 0; i < (children as MountedVNode[]).length; i++) { for (let i = 0; i < (children as MountedVNode[]).length; i++) {
reinsertVNode((children as MountedVNode[])[i], container) removeVNode((children as MountedVNode[])[i], container)
} }
} }
} else { } else {
@ -1130,7 +1145,7 @@ export function createRenderer(options: RendererOptions) {
platformClearContent(container) platformClearContent(container)
} else { } else {
for (let i = 0; i < children.length; i++) { 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, instance: ComponentInstance,
isSVG: boolean isSVG: boolean
) { ) {
if (__DEV__ && instance.$parentVNode) {
pushContext(instance.$parentVNode as VNode)
}
const prevVNode = instance.$vnode const prevVNode = instance.$vnode
if (instance.beforeUpdate) { if (instance.beforeUpdate) {
@ -1275,6 +1293,10 @@ export function createRenderer(options: RendererOptions) {
} }
}) })
} }
if (__DEV__ && instance.$parentVNode) {
popContext()
}
} }
function unmountComponentInstance(instance: ComponentInstance) { function unmountComponentInstance(instance: ComponentInstance) {
@ -1380,7 +1402,7 @@ export function createRenderer(options: RendererOptions) {
patch(prevVNode, vnode, container, null, false) patch(prevVNode, vnode, container, null, false)
container.vnode = vnode container.vnode = vnode
} else { } else {
reinsertVNode(prevVNode, container) removeVNode(prevVNode, container)
container.vnode = null container.vnode = null
} }
} }

View File

@ -1,4 +1,7 @@
import { ComponentInstance } from './component' import { ComponentInstance } from './component'
import { warn } from './warning'
import { VNode } from './vdom'
import { VNodeFlags } from './flags'
export const enum ErrorTypes { export const enum ErrorTypes {
BEFORE_CREATE = 1, BEFORE_CREATE = 1,
@ -11,7 +14,6 @@ export const enum ErrorTypes {
DESTROYED, DESTROYED,
ERROR_CAPTURED, ERROR_CAPTURED,
RENDER, RENDER,
RENDER_ERROR,
WATCH_CALLBACK, WATCH_CALLBACK,
NATIVE_EVENT_HANDLER, NATIVE_EVENT_HANDLER,
COMPONENT_EVENT_HANDLER COMPONENT_EVENT_HANDLER
@ -28,7 +30,6 @@ const ErrorTypeStrings: Record<number, string> = {
[ErrorTypes.DESTROYED]: 'destroyed lifecycle hook', [ErrorTypes.DESTROYED]: 'destroyed lifecycle hook',
[ErrorTypes.ERROR_CAPTURED]: 'errorCaptured lifecycle hook', [ErrorTypes.ERROR_CAPTURED]: 'errorCaptured lifecycle hook',
[ErrorTypes.RENDER]: 'render function', [ErrorTypes.RENDER]: 'render function',
[ErrorTypes.RENDER_ERROR]: 'renderError function',
[ErrorTypes.WATCH_CALLBACK]: 'watcher callback', [ErrorTypes.WATCH_CALLBACK]: 'watcher callback',
[ErrorTypes.NATIVE_EVENT_HANDLER]: 'native event handler', [ErrorTypes.NATIVE_EVENT_HANDLER]: 'native event handler',
[ErrorTypes.COMPONENT_EVENT_HANDLER]: 'component event handler' [ErrorTypes.COMPONENT_EVENT_HANDLER]: 'component event handler'
@ -36,21 +37,39 @@ const ErrorTypeStrings: Record<number, string> = {
export function handleError( export function handleError(
err: Error, err: Error,
instance: ComponentInstance, instance: ComponentInstance | VNode,
type: ErrorTypes type: ErrorTypes
) { ) {
let cur = instance const isFunctional = (instance as VNode)._isVNode
while (cur.$parent) { let cur: ComponentInstance | null = null
cur = cur.$parent 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 const handler = cur.errorCaptured
if (handler) { if (handler) {
try { 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 if (captured) return
} catch (err2) { } catch (err2) {
logError(err2, ErrorTypes.ERROR_CAPTURED) logError(err2, ErrorTypes.ERROR_CAPTURED)
} }
} }
cur = cur.$parent
} }
logError(err, type) logError(err, type)
} }
@ -58,7 +77,7 @@ export function handleError(
function logError(err: Error, type: ErrorTypes) { function logError(err: Error, type: ErrorTypes) {
if (__DEV__) { if (__DEV__) {
const info = ErrorTypeStrings[type] const info = ErrorTypeStrings[type]
console.warn(`Unhandled error${info ? ` in ${info}` : ``}:`) warn(`Unhandled error${info ? ` in ${info}` : ``}`)
console.error(err) console.error(err)
} else { } else {
throw err throw err

View File

@ -1,22 +1,14 @@
import { ComponentType, ComponentClass, FunctionalComponent } from './component' import { ComponentType, ComponentClass, FunctionalComponent } from './component'
import { EMPTY_OBJ } from './utils' import { EMPTY_OBJ } from './utils'
import { VNode } from './vdom'
// TODO push vnodes instead let stack: VNode[] = []
// 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.
// in createRenderer, parentComponent should be replced by ctx export function pushContext(vnode: VNode) {
// $parent logic should also accomodate stack.push(vnode)
let stack: ComponentType[] = []
export function pushComponent(c: ComponentType) {
stack.push(c)
} }
export function popComponent() { export function popContext() {
stack.pop() stack.pop()
} }
@ -26,33 +18,42 @@ export function warn(msg: string) {
} }
function getComponentTrace(): string { function getComponentTrace(): string {
const current = stack[stack.length - 1] let current: VNode | null | undefined = stack[stack.length - 1]
if (!current) { if (!current) {
return '' return '\nat <Root/>'
} }
// we can't just use the stack itself, because it will be incomplete
// during updates // we can't just use the stack because it will be incomplete during updates
// check recursive // that did not start from the root. Re-construct the parent chain using
// contextVNode information.
const normlaizedStack: Array<{ const normlaizedStack: Array<{
type: ComponentType type: VNode
recurseCount: number recurseCount: number
}> = [] }> = []
stack.forEach(c => {
const last = normlaizedStack[normlaizedStack.length - 1] while (current) {
if (last && last.type === c) { const last = normlaizedStack[0]
if (last && last.type === current) {
last.recurseCount++ last.recurseCount++
} else { } else {
normlaizedStack.push({ type: c, recurseCount: 0 }) normlaizedStack.unshift({
type: current,
recurseCount: 0
})
} }
}) current = current.contextVNode
}
return ( return (
`\n\nfound in\n\n` + `\nat ` +
normlaizedStack normlaizedStack
.map(({ type, recurseCount }, i) => { .map(({ type, recurseCount }, i) => {
const padding = i === 0 ? '---> ' : ' '.repeat(5 + i * 2) const padding = i === 0 ? '' : ' '.repeat(i + 1)
const postfix = const postfix =
recurseCount > 0 ? `... (${recurseCount} recursive calls)` : `` recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``
return padding + formatComponentName(type) + postfix return (
padding + formatComponentName(type.tag as ComponentType) + postfix
)
}) })
.join('\n') .join('\n')
) )
@ -66,12 +67,14 @@ function formatComponentName(c: ComponentType, includeFile?: boolean): string {
let name: string let name: string
let file: string | null = null let file: string | null = null
if (c.prototype.render) { if (c.prototype && c.prototype.render) {
// stateful
const cc = c as ComponentClass const cc = c as ComponentClass
const options = cc.options || EMPTY_OBJ const options = cc.options || EMPTY_OBJ
name = options.displayName || cc.name name = options.displayName || cc.name
file = options.__file file = options.__file
} else { } else {
// functional
const fc = c as FunctionalComponent const fc = c as FunctionalComponent
name = fc.displayName || fc.name name = fc.displayName || fc.name
} }