feat: error handling for lifecycle hooks
This commit is contained in:
parent
fd018b83b5
commit
3d681f8bcd
@ -15,7 +15,7 @@ describe('vnode', () => {
|
||||
|
||||
test.todo('normalizeVNode')
|
||||
|
||||
test.todo('node type inference')
|
||||
test.todo('node type/shapeFlag inference')
|
||||
|
||||
test.todo('cloneVNode')
|
||||
|
||||
|
@ -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(
|
||||
name: keyof LifecycleHooks,
|
||||
type: LifecycleHooks,
|
||||
hook: Function,
|
||||
target: ComponentInstance | null | void = currentInstance
|
||||
target: ComponentInstance | null = currentInstance
|
||||
) {
|
||||
if (target) {
|
||||
// TODO inject a error-handling wrapped version of the hook
|
||||
// TODO also set currentInstance when calling the hook
|
||||
;(target[name] || (target[name] = [])).push(hook)
|
||||
} else {
|
||||
// TODO warn
|
||||
// wrap user hook with error handling logic
|
||||
const withErrorHandling = applyErrorHandling(hook, target, type)
|
||||
;(target[type] || (target[type] = [])).push((...args: any[]) => {
|
||||
// Set currentInstance during hook invocation.
|
||||
// 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) {
|
||||
injectHook('bm', hook, target)
|
||||
export function onBeforeMount(
|
||||
hook: Function,
|
||||
target: ComponentInstance | null = currentInstance
|
||||
) {
|
||||
injectHook(LifecycleHooks.BEFORE_MOUNT, hook, target)
|
||||
}
|
||||
|
||||
export function onMounted(hook: Function, target?: ComponentInstance) {
|
||||
injectHook('m', hook, target)
|
||||
export function onMounted(
|
||||
hook: Function,
|
||||
target: ComponentInstance | null = currentInstance
|
||||
) {
|
||||
injectHook(LifecycleHooks.MOUNTED, hook, target)
|
||||
}
|
||||
|
||||
export function onBeforeUpdate(hook: Function, target?: ComponentInstance) {
|
||||
injectHook('bu', hook, target)
|
||||
export function onBeforeUpdate(
|
||||
hook: Function,
|
||||
target: ComponentInstance | null = currentInstance
|
||||
) {
|
||||
injectHook(LifecycleHooks.BEFORE_UPDATE, hook, target)
|
||||
}
|
||||
|
||||
export function onUpdated(hook: Function, target?: ComponentInstance) {
|
||||
injectHook('u', hook, target)
|
||||
export function onUpdated(
|
||||
hook: Function,
|
||||
target: ComponentInstance | null = currentInstance
|
||||
) {
|
||||
injectHook(LifecycleHooks.UPDATED, hook, target)
|
||||
}
|
||||
|
||||
export function onBeforeUnmount(hook: Function, target?: ComponentInstance) {
|
||||
injectHook('bum', hook, target)
|
||||
export function onBeforeUnmount(
|
||||
hook: Function,
|
||||
target: ComponentInstance | null = currentInstance
|
||||
) {
|
||||
injectHook(LifecycleHooks.BEFORE_UNMOUNT, hook, target)
|
||||
}
|
||||
|
||||
export function onUnmounted(hook: Function, target?: ComponentInstance) {
|
||||
injectHook('um', hook, target)
|
||||
export function onUnmounted(
|
||||
hook: Function,
|
||||
target: ComponentInstance | null = currentInstance
|
||||
) {
|
||||
injectHook(LifecycleHooks.UNMOUNTED, hook, target)
|
||||
}
|
||||
|
||||
export function onRenderTriggered(hook: Function, target?: ComponentInstance) {
|
||||
injectHook('rtg', hook, target)
|
||||
export function onRenderTriggered(
|
||||
hook: Function,
|
||||
target: ComponentInstance | null = currentInstance
|
||||
) {
|
||||
injectHook(LifecycleHooks.RENDER_TRIGGERED, hook, target)
|
||||
}
|
||||
|
||||
export function onRenderTracked(hook: Function, target?: ComponentInstance) {
|
||||
injectHook('rtc', hook, target)
|
||||
export function onRenderTracked(
|
||||
hook: Function,
|
||||
target: ComponentInstance | null = currentInstance
|
||||
) {
|
||||
injectHook(LifecycleHooks.RENDER_TRACKED, hook, target)
|
||||
}
|
||||
|
||||
export function onErrorCaptured(hook: Function, target?: ComponentInstance) {
|
||||
injectHook('ec', hook, target)
|
||||
export function onErrorCaptured(
|
||||
hook: Function,
|
||||
target: ComponentInstance | null = currentInstance
|
||||
) {
|
||||
injectHook(LifecycleHooks.ERROR_CAPTURED, hook, target)
|
||||
}
|
||||
|
@ -74,18 +74,20 @@ export interface FunctionalComponent<P = {}> {
|
||||
|
||||
type LifecycleHook = Function[] | null
|
||||
|
||||
export interface LifecycleHooks {
|
||||
bm: LifecycleHook // beforeMount
|
||||
m: LifecycleHook // mounted
|
||||
bu: LifecycleHook // beforeUpdate
|
||||
u: LifecycleHook // updated
|
||||
bum: LifecycleHook // beforeUnmount
|
||||
um: LifecycleHook // unmounted
|
||||
da: LifecycleHook // deactivated
|
||||
a: LifecycleHook // activated
|
||||
rtg: LifecycleHook // renderTriggered
|
||||
rtc: LifecycleHook // renderTracked
|
||||
ec: LifecycleHook // errorCaptured
|
||||
export const enum LifecycleHooks {
|
||||
BEFORE_CREATE = 'bc',
|
||||
CREATED = 'c',
|
||||
BEFORE_MOUNT = 'bm',
|
||||
MOUNTED = 'm',
|
||||
BEFORE_UPDATE = 'bu',
|
||||
UPDATED = 'u',
|
||||
BEFORE_UNMOUNT = 'bum',
|
||||
UNMOUNTED = 'um',
|
||||
DEACTIVATED = 'da',
|
||||
ACTIVATED = 'a',
|
||||
RENDER_TRIGGERED = 'rtg',
|
||||
RENDER_TRACKED = 'rtc',
|
||||
ERROR_CAPTURED = 'ec'
|
||||
}
|
||||
|
||||
interface SetupContext {
|
||||
@ -116,8 +118,22 @@ export type ComponentInstance<P = Data, S = Data> = {
|
||||
|
||||
// user namespace
|
||||
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
|
||||
// overload 1: direct setup function
|
||||
@ -177,7 +193,23 @@ export function createComponentInstance(
|
||||
renderProxy: null,
|
||||
propsProxy: 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,
|
||||
m: null,
|
||||
bu: null,
|
||||
@ -189,18 +221,6 @@ export function createComponentInstance(
|
||||
rtg: null,
|
||||
rtc: 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[]) => {
|
||||
const props = instance.vnode.props || EMPTY_OBJ
|
||||
@ -220,6 +240,10 @@ export let currentInstance: ComponentInstance | null = null
|
||||
export const getCurrentInstance: () => ComponentInstance | null = () =>
|
||||
currentInstance
|
||||
|
||||
export const setCurrentInstance = (instance: ComponentInstance | null) => {
|
||||
currentInstance = instance
|
||||
}
|
||||
|
||||
export function setupStatefulComponent(instance: ComponentInstance) {
|
||||
const Component = instance.type as ComponentOptions
|
||||
// 1. create render proxy
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user