feat: error handling for lifecycle hooks

This commit is contained in:
Evan You 2019-08-30 12:16:09 -04:00
parent fd018b83b5
commit 3d681f8bcd
4 changed files with 225 additions and 54 deletions

View File

@ -15,7 +15,7 @@ describe('vnode', () => {
test.todo('normalizeVNode') test.todo('normalizeVNode')
test.todo('node type inference') test.todo('node type/shapeFlag inference')
test.todo('cloneVNode') test.todo('cloneVNode')

View File

@ -1,51 +1,101 @@
import { ComponentInstance, LifecycleHooks, currentInstance } from './component' import {
ComponentInstance,
LifecycleHooks,
currentInstance,
setCurrentInstance
} from './component'
import { applyErrorHandling, ErrorTypeStrings } from './errorHandling'
import { warn } from './warning'
import { capitalize } from '@vue/shared'
function injectHook( function injectHook(
name: keyof LifecycleHooks, type: LifecycleHooks,
hook: Function, hook: Function,
target: ComponentInstance | null | void = currentInstance target: ComponentInstance | null = currentInstance
) { ) {
if (target) { if (target) {
// TODO inject a error-handling wrapped version of the hook // wrap user hook with error handling logic
// TODO also set currentInstance when calling the hook const withErrorHandling = applyErrorHandling(hook, target, type)
;(target[name] || (target[name] = [])).push(hook) ;(target[type] || (target[type] = [])).push((...args: any[]) => {
} else { // Set currentInstance during hook invocation.
// TODO warn // This assumes the hook does not synchronously trigger other hooks, which
// can only be false when the user does something really funky.
setCurrentInstance(target)
const res = withErrorHandling(...args)
setCurrentInstance(null)
return res
})
} else if (__DEV__) {
const apiName = `on${capitalize(
ErrorTypeStrings[name].replace(/ hook$/, '')
)}`
warn(
`${apiName} is called when there is no active component instance to be ` +
`associated with. ` +
`Lifecycle injection APIs can only be used during execution of setup().`
)
} }
} }
export function onBeforeMount(hook: Function, target?: ComponentInstance) { export function onBeforeMount(
injectHook('bm', hook, target) hook: Function,
target: ComponentInstance | null = currentInstance
) {
injectHook(LifecycleHooks.BEFORE_MOUNT, hook, target)
} }
export function onMounted(hook: Function, target?: ComponentInstance) { export function onMounted(
injectHook('m', hook, target) hook: Function,
target: ComponentInstance | null = currentInstance
) {
injectHook(LifecycleHooks.MOUNTED, hook, target)
} }
export function onBeforeUpdate(hook: Function, target?: ComponentInstance) { export function onBeforeUpdate(
injectHook('bu', hook, target) hook: Function,
target: ComponentInstance | null = currentInstance
) {
injectHook(LifecycleHooks.BEFORE_UPDATE, hook, target)
} }
export function onUpdated(hook: Function, target?: ComponentInstance) { export function onUpdated(
injectHook('u', hook, target) hook: Function,
target: ComponentInstance | null = currentInstance
) {
injectHook(LifecycleHooks.UPDATED, hook, target)
} }
export function onBeforeUnmount(hook: Function, target?: ComponentInstance) { export function onBeforeUnmount(
injectHook('bum', hook, target) hook: Function,
target: ComponentInstance | null = currentInstance
) {
injectHook(LifecycleHooks.BEFORE_UNMOUNT, hook, target)
} }
export function onUnmounted(hook: Function, target?: ComponentInstance) { export function onUnmounted(
injectHook('um', hook, target) hook: Function,
target: ComponentInstance | null = currentInstance
) {
injectHook(LifecycleHooks.UNMOUNTED, hook, target)
} }
export function onRenderTriggered(hook: Function, target?: ComponentInstance) { export function onRenderTriggered(
injectHook('rtg', hook, target) hook: Function,
target: ComponentInstance | null = currentInstance
) {
injectHook(LifecycleHooks.RENDER_TRIGGERED, hook, target)
} }
export function onRenderTracked(hook: Function, target?: ComponentInstance) { export function onRenderTracked(
injectHook('rtc', hook, target) hook: Function,
target: ComponentInstance | null = currentInstance
) {
injectHook(LifecycleHooks.RENDER_TRACKED, hook, target)
} }
export function onErrorCaptured(hook: Function, target?: ComponentInstance) { export function onErrorCaptured(
injectHook('ec', hook, target) hook: Function,
target: ComponentInstance | null = currentInstance
) {
injectHook(LifecycleHooks.ERROR_CAPTURED, hook, target)
} }

View File

@ -74,18 +74,20 @@ export interface FunctionalComponent<P = {}> {
type LifecycleHook = Function[] | null type LifecycleHook = Function[] | null
export interface LifecycleHooks { export const enum LifecycleHooks {
bm: LifecycleHook // beforeMount BEFORE_CREATE = 'bc',
m: LifecycleHook // mounted CREATED = 'c',
bu: LifecycleHook // beforeUpdate BEFORE_MOUNT = 'bm',
u: LifecycleHook // updated MOUNTED = 'm',
bum: LifecycleHook // beforeUnmount BEFORE_UPDATE = 'bu',
um: LifecycleHook // unmounted UPDATED = 'u',
da: LifecycleHook // deactivated BEFORE_UNMOUNT = 'bum',
a: LifecycleHook // activated UNMOUNTED = 'um',
rtg: LifecycleHook // renderTriggered DEACTIVATED = 'da',
rtc: LifecycleHook // renderTracked ACTIVATED = 'a',
ec: LifecycleHook // errorCaptured RENDER_TRIGGERED = 'rtg',
RENDER_TRACKED = 'rtc',
ERROR_CAPTURED = 'ec'
} }
interface SetupContext { interface SetupContext {
@ -116,8 +118,22 @@ export type ComponentInstance<P = Data, S = Data> = {
// user namespace // user namespace
user: { [key: string]: any } user: { [key: string]: any }
} & SetupContext &
LifecycleHooks // lifecycle
[LifecycleHooks.BEFORE_CREATE]: LifecycleHook
[LifecycleHooks.CREATED]: LifecycleHook
[LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
[LifecycleHooks.MOUNTED]: LifecycleHook
[LifecycleHooks.BEFORE_UPDATE]: LifecycleHook
[LifecycleHooks.UPDATED]: LifecycleHook
[LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook
[LifecycleHooks.UNMOUNTED]: LifecycleHook
[LifecycleHooks.RENDER_TRACKED]: LifecycleHook
[LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook
[LifecycleHooks.ACTIVATED]: LifecycleHook
[LifecycleHooks.DEACTIVATED]: LifecycleHook
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
} & SetupContext
// createComponent // createComponent
// overload 1: direct setup function // overload 1: direct setup function
@ -177,7 +193,23 @@ export function createComponentInstance(
renderProxy: null, renderProxy: null,
propsProxy: null, propsProxy: null,
setupContext: null, setupContext: null,
effects: null,
provides: parent ? parent.provides : {},
// setup context properties
data: EMPTY_OBJ,
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
slots: EMPTY_OBJ,
refs: EMPTY_OBJ,
// user namespace for storing whatever the user assigns to `this`
user: {},
// lifecycle hooks
// not using enums here because it results in computed properties
bc: null,
c: null,
bm: null, bm: null,
m: null, m: null,
bu: null, bu: null,
@ -189,18 +221,6 @@ export function createComponentInstance(
rtg: null, rtg: null,
rtc: null, rtc: null,
ec: null, ec: null,
effects: null,
provides: parent ? parent.provides : {},
// public properties
data: EMPTY_OBJ,
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
slots: EMPTY_OBJ,
refs: EMPTY_OBJ,
// user namespace for storing whatever the user assigns to `this`
user: {},
emit: (event: string, ...args: unknown[]) => { emit: (event: string, ...args: unknown[]) => {
const props = instance.vnode.props || EMPTY_OBJ const props = instance.vnode.props || EMPTY_OBJ
@ -220,6 +240,10 @@ export let currentInstance: ComponentInstance | null = null
export const getCurrentInstance: () => ComponentInstance | null = () => export const getCurrentInstance: () => ComponentInstance | null = () =>
currentInstance currentInstance
export const setCurrentInstance = (instance: ComponentInstance | null) => {
currentInstance = instance
}
export function setupStatefulComponent(instance: ComponentInstance) { export function setupStatefulComponent(instance: ComponentInstance) {
const Component = instance.type as ComponentOptions const Component = instance.type as ComponentOptions
// 1. create render proxy // 1. create render proxy

View File

@ -1 +1,98 @@
// TODO import { VNode } from './vnode'
import { ComponentInstance, LifecycleHooks } from './component'
import { warn, pushWarningContext, popWarningContext } from './warning'
// contexts where user provided function may be executed, in addition to
// lifecycle hooks.
export const enum UserExecutionContexts {
RENDER_FUNCTION = 1,
WATCH_CALLBACK,
NATIVE_EVENT_HANDLER,
COMPONENT_EVENT_HANDLER,
SCHEDULER
}
export const ErrorTypeStrings: Record<number | string, string> = {
[LifecycleHooks.BEFORE_CREATE]: 'beforeCreate hook',
[LifecycleHooks.CREATED]: 'created hook',
[LifecycleHooks.BEFORE_MOUNT]: 'beforeMount hook',
[LifecycleHooks.MOUNTED]: 'mounted hook',
[LifecycleHooks.BEFORE_UPDATE]: 'beforeUpdate hook',
[LifecycleHooks.UPDATED]: 'updated',
[LifecycleHooks.BEFORE_UNMOUNT]: 'beforeUnmount hook',
[LifecycleHooks.UNMOUNTED]: 'unmounted hook',
[LifecycleHooks.ACTIVATED]: 'activated hook',
[LifecycleHooks.DEACTIVATED]: 'deactivated hook',
[LifecycleHooks.ERROR_CAPTURED]: 'errorCaptured hook',
[LifecycleHooks.RENDER_TRACKED]: 'renderTracked hook',
[LifecycleHooks.RENDER_TRIGGERED]: 'renderTriggered hook',
[UserExecutionContexts.RENDER_FUNCTION]: 'render function',
[UserExecutionContexts.WATCH_CALLBACK]: 'watcher callback',
[UserExecutionContexts.NATIVE_EVENT_HANDLER]: 'native event handler',
[UserExecutionContexts.COMPONENT_EVENT_HANDLER]: 'component event handler',
[UserExecutionContexts.SCHEDULER]:
'scheduler flush. This may be a Vue internals bug. ' +
'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue'
}
type ErrorTypes = LifecycleHooks | UserExecutionContexts
// takes a user-provided function and returns a verison that handles potential
// errors (including async)
export function applyErrorHandling<T extends Function>(
fn: T,
instance: ComponentInstance | null,
type: ErrorTypes
): T {
return function errorHandlingWrapper(...args: any[]) {
let res: any
try {
res = fn(...args)
if (res && !res._isVue && typeof res.then === 'function') {
;(res as Promise<any>).catch(err => {
handleError(err, instance, type)
})
}
} catch (err) {
handleError(err, instance, type)
}
return res
} as any
}
export function handleError(
err: Error,
instance: ComponentInstance | null,
type: ErrorTypes
) {
const contextVNode = instance ? instance.vnode : null
let cur: ComponentInstance | null = instance && instance.parent
while (cur) {
const errorCapturedHooks = cur.ec
if (errorCapturedHooks !== null) {
for (let i = 0; i < errorCapturedHooks.length; i++) {
if (errorCapturedHooks[i](err, type, contextVNode)) {
return
}
}
}
cur = cur.parent
}
logError(err, type, contextVNode)
}
function logError(err: Error, type: ErrorTypes, contextVNode: VNode | null) {
if (__DEV__) {
const info = ErrorTypeStrings[type]
if (contextVNode) {
pushWarningContext(contextVNode)
}
warn(`Unhandled error${info ? ` during execution of ${info}` : ``}`)
console.error(err)
if (contextVNode) {
popWarningContext()
}
} else {
throw err
}
}