diff --git a/packages/runtime-core/__tests__/errorHandling.spec.ts b/packages/runtime-core/__tests__/errorHandling.spec.ts new file mode 100644 index 000000000..9dfaa7491 --- /dev/null +++ b/packages/runtime-core/__tests__/errorHandling.spec.ts @@ -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') +}) diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index 7ba6ade7e..ba9d7dd11 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -5,7 +5,9 @@ export interface InjectionKey extends Symbol {} export function provide(key: InjectionKey | 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 diff --git a/packages/runtime-core/src/apiLifecycle.ts b/packages/runtime-core/src/apiLifecycle.ts index ed4a4e4fc..4ffdd0e5e 100644 --- a/packages/runtime-core/src/apiLifecycle.ts +++ b/packages/runtime-core/src/apiLifecycle.ts @@ -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 }) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 1a956c734..94f3d69d3 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -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 diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index f4e0d204a..a7292cf28 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -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) } } diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index 703384b2d..cb38ad850 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -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 = { [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 = { 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).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).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 } } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index f2a36c984..795fab7cf 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -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' diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts index 257a46f67..3a8e59a4f 100644 --- a/packages/runtime-core/src/warning.ts +++ b/packages/runtime-core/src/warning.ts @@ -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[] = [] diff --git a/packages/runtime-dom/src/modules/events.ts b/packages/runtime-dom/src/modules/events.ts index 60c9136ea..8793ad948 100644 --- a/packages/runtime-dom/src/modules/events.ts +++ b/packages/runtime-dom/src/modules/events.ts @@ -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 diff --git a/packages/runtime-dom/src/patchProp.ts b/packages/runtime-dom/src/patchProp.ts index 6148708cd..51f780e98 100644 --- a/packages/runtime-dom/src/patchProp.ts +++ b/packages/runtime-dom/src/patchProp.ts @@ -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, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 9bace428c..9e857a64e 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -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) - } -}