feat: warning traces & error handling for functional render
This commit is contained in:
parent
3a7bbecb22
commit
5327abb249
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user