--- /dev/null
+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')
+})
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
currentInstance,
setCurrentInstance
} from './component'
-import { callUserFnWithErrorHandling, ErrorTypeStrings } from './errorHandling'
+import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling'
import { warn } from './warning'
import { capitalize } from '@vue/shared'
// 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
})
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
| 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
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
-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 }
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
+ )
+ }
}
}
}
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)) {
// 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) {
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)
}
}
// 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
[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]:
type ErrorTypes = LifecycleHooks | UserExecutionContexts
-export function callUserFnWithErrorHandling(
+export function callWithErrorHandling(
fn: Function,
instance: ComponentInstance | null,
type: ErrorTypes,
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,
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
}
}
// 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'
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[] = []
-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
el: Element,
name: string,
prevValue: EventValue | null,
- nextValue: EventValue | null
+ nextValue: EventValue | null,
+ instance: ComponentInstance | null
) {
const invoker = prevValue && prevValue.invoker
if (nextValue) {
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
// 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
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,
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)
- }
-}