]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: error handling for lifecycle hooks
authorEvan You <yyx990803@gmail.com>
Fri, 30 Aug 2019 16:16:09 +0000 (12:16 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 30 Aug 2019 16:16:09 +0000 (12:16 -0400)
packages/runtime-core/__tests__/vnode.spec.ts
packages/runtime-core/src/apiLifecycle.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/errorHandling.ts

index e3b8ad9feb8e943007c265f2567a1b6215dc3645..6cdc6f7b62c028d09a22b96f806f0e9f6f87dbea 100644 (file)
@@ -15,7 +15,7 @@ describe('vnode', () => {
 
   test.todo('normalizeVNode')
 
-  test.todo('node type inference')
+  test.todo('node type/shapeFlag inference')
 
   test.todo('cloneVNode')
 
index c59760158afd6372495ce143f0f9f4ccb9f21279..d1e95cfc7cc2a36bf78cf00ad3eaa0de197aef70 100644 (file)
-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)
 }
index 8b27fe8631d5976a6adf2f709efaadb108a70db4..f4e0d204a2c1a69b7fd37700cb8f56bebe004189 100644 (file)
@@ -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
index 70b786d12ed055a08b57f5cf47f717bf6a266301..202051328861daf731fb631dad2b36acfa3093b2 100644 (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
+  }
+}