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>
render(props: Readonly<P>, 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<P = {}, D = {}> extends InternalComponent {
errorCaptured?(): (
err: Error,
type: ErrorTypes,
target: ComponentInstance
instance: ComponentInstance | null,
vnode: VNode
) => boolean | void
activated?(): void
deactivated?(): void

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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<number, string> = {
[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<number, string> = {
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

View File

@ -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 <Root/>'
}
// 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
}