]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: error handling for setup / render / watch / event handlers
authorEvan You <yyx990803@gmail.com>
Fri, 30 Aug 2019 19:05:39 +0000 (15:05 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 30 Aug 2019 19:05:39 +0000 (15:05 -0400)
packages/runtime-core/__tests__/errorHandling.spec.ts [new file with mode: 0644]
packages/runtime-core/src/apiInject.ts
packages/runtime-core/src/apiLifecycle.ts
packages/runtime-core/src/apiWatch.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/errorHandling.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/warning.ts
packages/runtime-dom/src/modules/events.ts
packages/runtime-dom/src/patchProp.ts
packages/shared/src/index.ts

diff --git a/packages/runtime-core/__tests__/errorHandling.spec.ts b/packages/runtime-core/__tests__/errorHandling.spec.ts
new file mode 100644 (file)
index 0000000..9dfaa74
--- /dev/null
@@ -0,0 +1,17 @@
+describe('error handling', () => {
+  test.todo('in lifecycle hooks')
+
+  test.todo('in onErrorCaptured')
+
+  test.todo('in setup function')
+
+  test.todo('in render function')
+
+  test.todo('in watch (simple usage)')
+
+  test.todo('in watch (with source)')
+
+  test.todo('in native event handler')
+
+  test.todo('in component event handler')
+})
index 7ba6ade7ec891ceab61a9aa0be9832e86e214c80..ba9d7dd1176cf2755e53758187714fc6024eabde 100644 (file)
@@ -5,7 +5,9 @@ export interface InjectionKey<T> extends Symbol {}
 
 export function provide<T>(key: InjectionKey<T> | string, value: T) {
   if (!currentInstance) {
-    // TODO warn
+    if (__DEV__) {
+      warn(`provide() is used without an active component instance.`)
+    }
   } else {
     let provides = currentInstance.provides
     // by default an instance inherits its parent's provides object
index ed4a4e4fc8c3b1bf691c9d642362a4b675d61210..4ffdd0e5e7080d739db29b1163cb96a7fbdd1816 100644 (file)
@@ -4,7 +4,7 @@ import {
   currentInstance,
   setCurrentInstance
 } from './component'
-import { callUserFnWithErrorHandling, ErrorTypeStrings } from './errorHandling'
+import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling'
 import { warn } from './warning'
 import { capitalize } from '@vue/shared'
 
@@ -19,7 +19,7 @@ function injectHook(
       // 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 = callUserFnWithErrorHandling(hook, target, type, args)
+      const res = callWithAsyncErrorHandling(hook, target, type, args)
       setCurrentInstance(null)
       return res
     })
index 1a956c7344e1f922338482920aa16f7521d5727f..94f3d69d335915ee924463c94b77fab2fbe15656 100644 (file)
@@ -9,6 +9,11 @@ import { queueJob, queuePostFlushCb } from './scheduler'
 import { EMPTY_OBJ, isObject, isArray, isFunction } from '@vue/shared'
 import { recordEffect } from './apiReactivity'
 import { getCurrentInstance } from './component'
+import {
+  UserExecutionContexts,
+  callWithErrorHandling,
+  callWithAsyncErrorHandling
+} from './errorHandling'
 
 export interface WatchOptions {
   lazy?: boolean
@@ -78,22 +83,57 @@ function doWatch(
     | null,
   { lazy, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
 ): StopHandle {
-  const baseGetter = isArray(source)
-    ? () => source.map(s => (isRef(s) ? s.value : s()))
-    : isRef(source)
-      ? () => source.value
-      : () => {
-          if (cleanup) {
-            cleanup()
-          }
-          return source(registerCleanup)
-        }
-  const getter = deep ? () => traverse(baseGetter()) : baseGetter
+  const instance = getCurrentInstance()
 
-  let cleanup: any
+  let getter: Function
+  if (isArray(source)) {
+    getter = () =>
+      source.map(
+        s =>
+          isRef(s)
+            ? s.value
+            : callWithErrorHandling(
+                s,
+                instance,
+                UserExecutionContexts.WATCH_GETTER
+              )
+      )
+  } else if (isRef(source)) {
+    getter = () => source.value
+  } else if (cb) {
+    // getter with cb
+    getter = () =>
+      callWithErrorHandling(
+        source,
+        instance,
+        UserExecutionContexts.WATCH_GETTER
+      )
+  } else {
+    // no cb -> simple effect
+    getter = () => {
+      if (cleanup) {
+        cleanup()
+      }
+      return callWithErrorHandling(
+        source,
+        instance,
+        UserExecutionContexts.WATCH_CALLBACK,
+        [registerCleanup]
+      )
+    }
+  }
+
+  if (deep) {
+    const baseGetter = getter
+    getter = () => traverse(baseGetter())
+  }
+
+  let cleanup: Function
   const registerCleanup: CleanupRegistrator = (fn: () => void) => {
     // TODO wrap the cleanup fn for error handling
-    cleanup = runner.onStop = fn
+    cleanup = runner.onStop = () => {
+      callWithErrorHandling(fn, instance, UserExecutionContexts.WATCH_CLEANUP)
+    }
   }
 
   let oldValue = isArray(source) ? [] : undefined
@@ -105,16 +145,17 @@ function doWatch(
           if (cleanup) {
             cleanup()
           }
-          // TODO handle error (including ASYNC)
-          try {
-            cb(newValue, oldValue, registerCleanup)
-          } catch (e) {}
+          callWithAsyncErrorHandling(
+            cb,
+            instance,
+            UserExecutionContexts.WATCH_CALLBACK,
+            [newValue, oldValue, registerCleanup]
+          )
           oldValue = newValue
         }
       }
     : void 0
 
-  const instance = getCurrentInstance()
   const scheduler =
     flush === 'sync'
       ? invoke
index f4e0d204a2c1a69b7fd37700cb8f56bebe004189..a7292cf28c10754402bd1d57e8d1b9fab49f1b12 100644 (file)
@@ -1,11 +1,18 @@
-import { VNode, normalizeVNode, VNodeChild } from './vnode'
+import { VNode, normalizeVNode, VNodeChild, createVNode, Empty } from './vnode'
 import { ReactiveEffect, UnwrapRef, reactive, readonly } from '@vue/reactivity'
-import { EMPTY_OBJ, isFunction, capitalize, invokeHandlers } from '@vue/shared'
+import { EMPTY_OBJ, isFunction, capitalize, NOOP, isArray } from '@vue/shared'
 import { RenderProxyHandlers } from './componentProxy'
 import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
 import { Slots } from './componentSlots'
 import { PatchFlags } from './patchFlags'
 import { ShapeFlags } from './shapeFlags'
+import { warn } from './warning'
+import {
+  UserExecutionContexts,
+  handleError,
+  callWithErrorHandling,
+  callWithAsyncErrorHandling
+} from './errorHandling'
 
 export type Data = { [key: string]: unknown }
 
@@ -226,7 +233,23 @@ export function createComponentInstance(
       const props = instance.vnode.props || EMPTY_OBJ
       const handler = props[`on${event}`] || props[`on${capitalize(event)}`]
       if (handler) {
-        invokeHandlers(handler, args)
+        if (isArray(handler)) {
+          for (let i = 0; i < handler.length; i++) {
+            callWithAsyncErrorHandling(
+              handler[i],
+              instance,
+              UserExecutionContexts.COMPONENT_EVENT_HANDLER,
+              args
+            )
+          }
+        } else {
+          callWithAsyncErrorHandling(
+            handler,
+            instance,
+            UserExecutionContexts.COMPONENT_EVENT_HANDLER,
+            args
+          )
+        }
       }
     }
   }
@@ -261,7 +284,12 @@ export function setupStatefulComponent(instance: ComponentInstance) {
       setup.length > 1 ? createSetupContext(instance) : null)
 
     currentInstance = instance
-    const setupResult = setup.call(null, propsProxy, setupContext)
+    const setupResult = callWithErrorHandling(
+      setup,
+      instance,
+      UserExecutionContexts.SETUP_FUNCTION,
+      [propsProxy, setupContext]
+    )
     currentInstance = null
 
     if (isFunction(setupResult)) {
@@ -272,9 +300,12 @@ export function setupStatefulComponent(instance: ComponentInstance) {
       // assuming a render function compiled from template is present.
       instance.data = reactive(setupResult || {})
       if (__DEV__ && !Component.render) {
-        // TODO warn missing render fn
+        warn(
+          `Component is missing render function. Either provide a template or ` +
+            `return a render function from setup().`
+        )
       }
-      instance.render = Component.render as RenderFunction
+      instance.render = (Component.render || NOOP) as RenderFunction
     }
   } else {
     if (__DEV__ && !Component.render) {
@@ -327,23 +358,32 @@ export function renderComponentRoot(instance: ComponentInstance): VNode {
     refs,
     emit
   } = instance
-  if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
-    return normalizeVNode(
-      (instance.render as RenderFunction).call(renderProxy, props, setupContext)
-    )
-  } else {
-    // functional
-    const render = Component as FunctionalComponent
-    return normalizeVNode(
-      render.length > 1
-        ? render(props, {
-            attrs,
-            slots,
-            refs,
-            emit
-          })
-        : render(props, null as any)
-    )
+  try {
+    if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
+      return normalizeVNode(
+        (instance.render as RenderFunction).call(
+          renderProxy,
+          props,
+          setupContext
+        )
+      )
+    } else {
+      // functional
+      const render = Component as FunctionalComponent
+      return normalizeVNode(
+        render.length > 1
+          ? render(props, {
+              attrs,
+              slots,
+              refs,
+              emit
+            })
+          : render(props, null as any)
+      )
+    }
+  } catch (err) {
+    handleError(err, instance, UserExecutionContexts.RENDER_FUNCTION)
+    return createVNode(Empty)
   }
 }
 
index 703384b2debc08de962d3faf0aee9272c672edd2..cb38ad8508eed2af52c3823b7ddfa9d41f7c2db5 100644 (file)
@@ -5,8 +5,11 @@ 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,
+  SETUP_FUNCTION = 1,
+  RENDER_FUNCTION,
+  WATCH_GETTER,
   WATCH_CALLBACK,
+  WATCH_CLEANUP,
   NATIVE_EVENT_HANDLER,
   COMPONENT_EVENT_HANDLER,
   SCHEDULER
@@ -26,8 +29,11 @@ export const ErrorTypeStrings: Record<number | string, string> = {
   [LifecycleHooks.ERROR_CAPTURED]: 'errorCaptured hook',
   [LifecycleHooks.RENDER_TRACKED]: 'renderTracked hook',
   [LifecycleHooks.RENDER_TRIGGERED]: 'renderTriggered hook',
+  [UserExecutionContexts.SETUP_FUNCTION]: 'setup function',
   [UserExecutionContexts.RENDER_FUNCTION]: 'render function',
+  [UserExecutionContexts.WATCH_GETTER]: 'watcher getter',
   [UserExecutionContexts.WATCH_CALLBACK]: 'watcher callback',
+  [UserExecutionContexts.WATCH_CLEANUP]: 'watcher cleanup function',
   [UserExecutionContexts.NATIVE_EVENT_HANDLER]: 'native event handler',
   [UserExecutionContexts.COMPONENT_EVENT_HANDLER]: 'component event handler',
   [UserExecutionContexts.SCHEDULER]:
@@ -37,7 +43,7 @@ export const ErrorTypeStrings: Record<number | string, string> = {
 
 type ErrorTypes = LifecycleHooks | UserExecutionContexts
 
-export function callUserFnWithErrorHandling(
+export function callWithErrorHandling(
   fn: Function,
   instance: ComponentInstance | null,
   type: ErrorTypes,
@@ -46,17 +52,27 @@ export function callUserFnWithErrorHandling(
   let res: any
   try {
     res = args ? fn(...args) : fn()
-    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
 }
 
+export function callWithAsyncErrorHandling(
+  fn: Function,
+  instance: ComponentInstance | null,
+  type: ErrorTypes,
+  args?: any[]
+) {
+  const res = callWithErrorHandling(fn, instance, type, args)
+  if (res != null && !res._isVue && typeof res.then === 'function') {
+    ;(res as Promise<any>).catch(err => {
+      handleError(err, instance, type)
+    })
+  }
+  return res
+}
+
 export function handleError(
   err: Error,
   instance: ComponentInstance | null,
@@ -68,7 +84,13 @@ export function handleError(
     const errorCapturedHooks = cur.ec
     if (errorCapturedHooks !== null) {
       for (let i = 0; i < errorCapturedHooks.length; i++) {
-        if (errorCapturedHooks[i](err, type, contextVNode)) {
+        if (
+          errorCapturedHooks[i](
+            err,
+            instance && instance.renderProxy,
+            contextVNode
+          )
+        ) {
           return
         }
       }
index f2a36c984d7059857b51ab6e981a8cb6fdc6fa3d..795fab7cf06cbec1c458e9b22c5bf9f3cf0b0190 100644 (file)
@@ -29,11 +29,16 @@ export { getCurrentInstance } from './component'
 
 // For custom renderers
 export { createRenderer } from './createRenderer'
+export {
+  handleError,
+  callWithErrorHandling,
+  callWithAsyncErrorHandling
+} from './errorHandling'
 
 // Types -----------------------------------------------------------------------
 
 export { VNode } from './vnode'
-export { FunctionalComponent } from './component'
+export { FunctionalComponent, ComponentInstance } from './component'
 export { RendererOptions } from './createRenderer'
 export { Slot, Slots } from './componentSlots'
 export { PropType, ComponentPropsOptions } from './componentProps'
index 257a46f67a127969edfa37e8d02646a90010201f..3a8e59a4ffc75b810edeced4346098228477485b 100644 (file)
@@ -27,6 +27,10 @@ export function warn(msg: string, ...args: any[]) {
   if (!trace.length) {
     return
   }
+  // avoid spamming test output
+  if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
+    return
+  }
   if (trace.length > 1 && console.groupCollapsed) {
     console.groupCollapsed('at', ...formatTraceEntry(trace[0]))
     const logs: string[] = []
index 60c9136eac43ef3274a62a50fff5a4d5311e1587..8793ad948dc83dc7eeaea1419c94ad818f8f7fce 100644 (file)
@@ -1,4 +1,9 @@
-import { invokeHandlers } from '@vue/shared'
+import { isArray } from '@vue/shared'
+import {
+  ComponentInstance,
+  callWithAsyncErrorHandling
+} from '@vue/runtime-core'
+import { UserExecutionContexts } from 'packages/runtime-core/src/errorHandling'
 
 interface Invoker extends Function {
   value: EventValue
@@ -39,7 +44,8 @@ export function patchEvent(
   el: Element,
   name: string,
   prevValue: EventValue | null,
-  nextValue: EventValue | null
+  nextValue: EventValue | null,
+  instance: ComponentInstance | null
 ) {
   const invoker = prevValue && prevValue.invoker
   if (nextValue) {
@@ -49,14 +55,14 @@ export function patchEvent(
       nextValue.invoker = invoker
       invoker.lastUpdated = getNow()
     } else {
-      el.addEventListener(name, createInvoker(nextValue))
+      el.addEventListener(name, createInvoker(nextValue, instance))
     }
   } else if (invoker) {
     el.removeEventListener(name, invoker as any)
   }
 }
 
-function createInvoker(value: any) {
+function createInvoker(value: any, instance: ComponentInstance | null) {
   const invoker = ((e: Event) => {
     // async edge case #6566: inner click event triggers patch, event handler
     // attached to outer element during patch, and triggered again. This
@@ -65,7 +71,24 @@ function createInvoker(value: any) {
     // and the handler would only fire if the event passed to it was fired
     // AFTER it was attached.
     if (e.timeStamp >= invoker.lastUpdated) {
-      invokeHandlers(invoker.value, [e])
+      const args = [e]
+      if (isArray(value)) {
+        for (let i = 0; i < value.length; i++) {
+          callWithAsyncErrorHandling(
+            value[i],
+            instance,
+            UserExecutionContexts.NATIVE_EVENT_HANDLER,
+            args
+          )
+        }
+      } else {
+        callWithAsyncErrorHandling(
+          value,
+          instance,
+          UserExecutionContexts.NATIVE_EVENT_HANDLER,
+          args
+        )
+      }
     }
   }) as any
   invoker.value = value
index 6148708cd9a3512000c0e2e452c6fc7c5dcfdd05..51f780e987e1c6adbf3bce8a3263e7efaccd7624 100644 (file)
@@ -26,7 +26,13 @@ export function patchProp(
       break
     default:
       if (isOn(key)) {
-        patchEvent(el, key.slice(2).toLowerCase(), prevValue, nextValue)
+        patchEvent(
+          el,
+          key.slice(2).toLowerCase(),
+          prevValue,
+          nextValue,
+          parentComponent
+        )
       } else if (!isSVG && key in el) {
         patchDOMProp(
           el,
index 9bace428c18843b75659c43dfb7d8f3e0cfbbc31..9e857a64ef145326bff0a05e571b306ef8886174 100644 (file)
@@ -40,16 +40,3 @@ export const hyphenate = (str: string): string => {
 export const capitalize = (str: string): string => {
   return str.charAt(0).toUpperCase() + str.slice(1)
 }
-
-export function invokeHandlers(
-  handlers: Function | Function[],
-  args: any[] = EMPTY_ARR
-) {
-  if (isArray(handlers)) {
-    for (let i = 0; i < handlers.length; i++) {
-      handlers[i].apply(null, args)
-    }
-  } else {
-    handlers.apply(null, args)
-  }
-}