feat: improve warning component trace

This commit is contained in:
Evan You 2018-10-15 14:39:16 -04:00
parent d4cd3fb352
commit 2507ad2b44

View File

@ -1,9 +1,17 @@
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' import { VNode } from './vdom'
import { Data } from './componentOptions'
let stack: VNode[] = [] let stack: VNode[] = []
type TraceEntry = {
type: VNode
recurseCount: number
}
type ComponentTraceStack = TraceEntry[]
export function pushWarningContext(vnode: VNode) { export function pushWarningContext(vnode: VNode) {
stack.push(vnode) stack.push(vnode)
} }
@ -13,30 +21,50 @@ export function popWarningContext() {
} }
export function warn(msg: string, ...args: any[]) { export function warn(msg: string, ...args: any[]) {
// TODO warn handler? // TODO warn handler
console.warn(`[Vue warn]: ${msg}${getComponentTrace()}`, ...args) console.warn(`[Vue warn]: ${msg}`, ...args)
const trace = getComponentTrace()
if (console.groupCollapsed) {
trace.forEach((entry, i) => {
const formatted = formatTraceEntry(entry, i)
if (i === 0) {
console.groupCollapsed('at', ...formatted)
} else {
console.log(...formatted)
}
})
console.groupEnd()
} else {
const logs: string[] = []
trace.forEach((entry, i) => {
const formatted = formatTraceEntry(entry, i)
if (i === 0) {
logs.push('at', ...formatted)
} else {
logs.push('\n', ...formatted)
}
})
console.log(...logs)
}
} }
function getComponentTrace(): string { function getComponentTrace(): ComponentTraceStack {
let current: VNode | null | undefined = stack[stack.length - 1] let current: VNode | null | undefined = stack[stack.length - 1]
if (!current) { if (!current) {
return '' return []
} }
// we can't just use the stack because it will be incomplete during updates // 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 // that did not start from the root. Re-construct the parent chain using
// contextVNode information. // contextVNode information.
const normlaizedStack: Array<{ const normlaizedStack: ComponentTraceStack = []
type: VNode
recurseCount: number
}> = []
while (current) { while (current) {
const last = normlaizedStack[0] const last = normlaizedStack[0]
if (last && last.type === current) { if (last && last.type === current) {
last.recurseCount++ last.recurseCount++
} else { } else {
normlaizedStack.unshift({ normlaizedStack.push({
type: current, type: current,
recurseCount: 0 recurseCount: 0
}) })
@ -44,35 +72,33 @@ function getComponentTrace(): string {
current = current.contextVNode current = current.contextVNode
} }
return ( return normlaizedStack
`\nat ` + }
normlaizedStack
.map(({ type, recurseCount }, i) => { function formatTraceEntry(
const padding = i === 0 ? '' : ' '.repeat(i + 1) { type, recurseCount }: TraceEntry,
const postfix = depth: number = 0
recurseCount > 0 ? `... (${recurseCount} recursive calls)` : `` ): string[] {
return ( const padding = depth === 0 ? '' : ' '.repeat(depth * 2 + 1)
padding + formatComponentName(type.tag as ComponentType) + postfix const postfix =
) recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``
}) const open = padding + `<${formatComponentName(type.tag as ComponentType)}`
.join('\n') const close = `>` + postfix
) return type.data ? [open, ...formatProps(type.data), close] : [open + close]
} }
const classifyRE = /(?:^|[-_])(\w)/g const classifyRE = /(?:^|[-_])(\w)/g
const classify = (str: string): string => const classify = (str: string): string =>
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '') str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
function formatComponentName(c: ComponentType, includeFile?: boolean): string { function formatComponentName(c: ComponentType, file?: string): string {
let name: string let name: string
let file: string | null = null
if (c.prototype && c.prototype.render) { if (c.prototype && c.prototype.render) {
// stateful // 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
} else { } else {
// functional // functional
const fc = c as FunctionalComponent const fc = c as FunctionalComponent
@ -86,6 +112,18 @@ function formatComponentName(c: ComponentType, includeFile?: boolean): string {
} }
} }
const filePostfix = file && includeFile !== false ? ` at ${file}` : '' return classify(name)
return `<${classify(name)}>` + filePostfix }
function formatProps(props: Data) {
const res = []
for (const key in props) {
const value = props[key]
if (typeof value === 'string') {
res.push(`${key}=${JSON.stringify(value)}`)
} else {
res.push(`${key}=`, value)
}
}
return res
} }