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('normalizeVNode')
|
||||||
|
|
||||||
test.todo('node type inference')
|
test.todo('node type/shapeFlag inference')
|
||||||
|
|
||||||
test.todo('cloneVNode')
|
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(
|
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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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