概念理解
错误捕获钩子(errorCaptured) :Vue 提供的生命周期钩子,用于捕获子组件、子组件生命周期钩子或事件处理函数中抛出的错误。当子组件发生错误时,父组件的 errorCaptured 钩子会被调用,可以决定是否阻止错误继续向上传播。
全局错误处理(errorHandler) :Vue 提供的全局配置选项,用于处理所有未被 errorCaptured 捕获的错误。这是错误处理的最后一道防线,如果全局错误处理也失败,错误会被输出到控制台。
错误传播机制 :采用向上冒泡 的机制,错误会从子组件向父组件逐级传播,直到被某个组件的 errorCaptured 钩子捕获(返回 false)或到达全局错误处理。
应用场景:
- 组件调试:开发时通过捕获错误并阻止传递来排查组件错误来源。
- 日志上报 :实际生产环境下会通过
errorHandler来收集组件错误信息并上报日志系统。 - 优雅降级 :当子组件渲染错误时,通过组件内错误钩子
errorCaptured可以用来展示其他 UI 信息,例如异常 UI 等。
Vue 2 错误钩子:
javascript
// 组件内
export default {
errorCaptured(err, vm, info) {
return false; // 阻止传播
},
};
// 全局钩子
Vue.config.errorHandler = (err, vm, info) => {};
Vue 3 错误钩子:
javascript
// 组件内钩子
<script setup>
import { onErrorCaptured } from 'vue'
onErrorCaptured((err, instance, info) => {
// instance 是公共对象,不是 Vue 2 直接把 vm 实例暴露出来
return false
})
</script>
// 全局错误钩子
const app = createApp(xxx)
app.config.errorHandler = (err, instance, info) => {
console.log('Global errorHandler:', err.message)
}
Vue 2 错误处理机制
错误捕获流程
Vue 2 的错误处理流程如下:
- 组件出错时会触发
handleError,此时先通过$parent往父级查找钩子函数。 - 遍历执行组件相关的
errorCaptured钩子函数。 - 如果其中之一的
errorCaptured钩子函数返回false,那么停止后续的错误捕获,包括全局和组件内的错误钩子。 errorCaptured执行过程中报错或主动抛错,globalHandleError会触发errorHandler全局错误处理钩子函数去处理errorCaptured hook。- 在完成所有遍历后,如果没有停止错误捕获,最终还是会执行全局钩子
errorHandler,这次是组件自身的错误捕获。 - 如果全局
errorHandler出错,最后会把错误抛给console.error输出。
源码实现
typescript
// src/core/util/error.ts
export function handleError(err: Error, vm: any, info: string) {
// See: https://github.com/vuejs/vuex/issues/1505
// 避免错误钩子无限错误渲染
pushTarget();
try {
if (vm) {
let cur = vm;
// 遍历父级,找到首个组件内错误钩子
while ((cur = cur.$parent)) {
// 获取对应的钩子数组
const hooks = cur.$options.errorCaptured;
// 遍历钩子执行
if (hooks) {
for (let i = 0; i < hooks.length; i++) {
try {
// 判断是否要捕获错误,捕获那么停止后续的错误函数执行
// capture 会阻止后续组件内钩子和全局钩子捕获错误
const capture = hooks[i].call(cur, err, vm, info) === false;
if (capture) return;
} catch (e: any) {
// 假如内部抛错,直接执行全局钩子函数
globalHandleError(e, cur, "errorCaptured hook");
}
}
}
}
}
// 如果没有捕获,那最终还是会传递给全局钩子
globalHandleError(err, vm, info);
} finally {
popTarget();
}
}
// 全局错误钩子执行
function globalHandleError(err, vm, info) {
if (config.errorHandler) {
try {
// info 包括组件来源、钩子错误来源等
return config.errorHandler.call(null, err, vm, info);
} catch (e: any) {
// 确定错误来源不是外部传入的 err
if (e !== err) {
logError(e, null, "config.errorHandler");
}
}
}
logError(err, vm, info);
}
// 捕获到特别异常错误,无法由 Vue 处理,直接交给控制台
function logError(err, vm, info) {
if (__DEV__) {
warn(`Error in ${info}: "${err.toString()}"`, vm);
}
/* istanbul ignore else */
if (inBrowser && typeof console !== "undefined") {
console.error(err);
} else {
throw err;
}
}
Vue 3 错误处理机制
Vue 3 和 Vue 2 的错误处理机制类似,都是:捕获钩子 → 全局捕获钩子 → console.error 输出。
主要区别在于:
- Vue 3 添加了结构化错误枚举类型,整合了更多的错误信息,代替了 Vue 2 的字符串语义。
- Vue 3 解耦了异步函数和同步函数的错误处理逻辑。
- Vue 2 中,假如
errorHandler()被定义并捕获了全局错误,console.error还是会输出,但 Vue 3 中errorHandler()会拦截后续console.error输出。 - Vue 2 是传入
vm实例来获取钩子,Vue 3 区分了公共实例和内部实例,公共实例需要通过instance.proxy来访问。
错误类型枚举
Vue 3 使用枚举来标识不同类型的错误:
typescript
// packages/runtime-core/src/errorHandling.ts
export enum ErrorCodes {
SETUP_FUNCTION,
RENDER_FUNCTION,
NATIVE_EVENT_HANDLER = 5,
COMPONENT_EVENT_HANDLER,
VNODE_HOOK,
DIRECTIVE_HOOK,
TRANSITION_HOOK,
APP_ERROR_HANDLER,
APP_WARN_HANDLER,
FUNCTION_REF,
ASYNC_COMPONENT_LOADER,
SCHEDULER,
COMPONENT_UPDATE,
APP_UNMOUNT_CLEANUP,
}
export type ErrorTypes = LifecycleHooks | ErrorCodes | WatchErrorCodes;
export const ErrorTypeStrings: Record<ErrorTypes, string> = {
[ErrorCodes.SETUP_FUNCTION]: "setup function",
[ErrorCodes.RENDER_FUNCTION]: "render function",
[ErrorCodes.NATIVE_EVENT_HANDLER]: "native event handler",
[ErrorCodes.COMPONENT_EVENT_HANDLER]: "component event handler",
[LifecycleHooks.ERROR_CAPTURED]: "errorCaptured hook",
// ... 其他错误类型
};
错误处理核心函数
和 Vue 2 类似,Vue 2 采用 invokeWithErrorHandling() 包装函数调用,但是把 Promise 和普通函数的处理耦合在一起,Vue 3 将两种情形的处理进行了解耦。
Vue 2 早期版本没有处理生命周期、watcher 里的异步任务,在
vue@2.7.x版本补齐了异步错误处理。
callWithErrorHandling
Vue 3 用于包装函数调用的代码更简洁:
typescript
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null | undefined,
type: ErrorTypes,
args?: unknown[]
): any {
try {
return args ? fn(...args) : fn();
} catch (err) {
handleError(err, instance, type);
}
}
callWithAsyncErrorHandling
用于处理异步函数和 Promise 的错误:
typescript
export function callWithAsyncErrorHandling(
fn: Function | Function[],
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
): any {
if (isFunction(fn)) {
const res = callWithErrorHandling(fn, instance, type, args);
if (res && isPromise(res)) {
res.catch((err) => {
handleError(err, instance, type);
});
}
return res;
}
// ... 处理函数数组的情况
}
源码实现
typescript
// packages/runtime-core/src/errorHandling.ts
/**
* 处理组件执行过程中产生的错误
*
* Vue 的错误处理机制遵循以下优先级顺序:
* 1. 组件树向上传播:从当前组件开始,向上遍历父组件链,调用 errorCaptured 钩子
* 2. 应用级错误处理器:如果配置了 errorHandler,则调用它
* 3. 最终日志记录:如果以上都未处理,则记录错误日志
*/
export function handleError(
err: unknown,
instance: ComponentInternalInstance | null | undefined,
type: ErrorTypes,
throwInDev = true
): void {
// 获取错误发生的上下文 VNode(用于错误日志定位)
const contextVNode = instance ? instance.vnode : null;
// 从应用配置中获取错误处理器和是否在生产环境抛出错误的配置
const { errorHandler, throwUnhandledErrorInProduction } =
(instance && instance.appContext.config) || EMPTY_OBJ;
// 如果有组件实例,尝试通过组件树向上传播错误
if (instance) {
// 从当前组件的父组件开始向上遍历
let cur = instance.parent;
// exposedInstance 是组件的渲染代理对象,用于保持与 Vue 2.x 的一致性
// 在 errorCaptured 钩子中,可以通过这个对象访问组件的属性和方法
const exposedInstance = instance.proxy;
// ...
// 向上遍历组件树,查找并调用 errorCaptured 钩子
while (cur) {
// cur.ec 是当前组件的 errorCaptured 钩子数组
const errorCapturedHooks = cur.ec;
if (errorCapturedHooks) {
// 遍历所有 errorCaptured 钩子并依次调用
for (let i = 0; i < errorCapturedHooks.length; i++) {
// 如果钩子返回 false,表示错误已被处理,停止向上传播
// Vue 2:使用 captured 捕获,Vue 3 这里是直接判断
if (
errorCapturedHooks[i](err, exposedInstance, errorInfo) === false
) {
return;
}
}
}
// 继续向上查找父组件
cur = cur.parent;
}
// 如果组件树中没有 errorCaptured 钩子处理错误,则使用全局错误处理 errorHandler
if (errorHandler) {
// 暂停响应式追踪,避免在错误处理过程中触发额外的副作用
pauseTracking();
// 调用应用级错误处理器
// 注意:这里传入 null 作为 instance,因为这是全局处理,不属于特定组件
callWithErrorHandling(errorHandler, null, ErrorCodes.APP_ERROR_HANDLER, [
err,
exposedInstance,
errorInfo,
]);
// 恢复响应式追踪
resetTracking();
// 全局已处理错误,直接返回
return;
}
}
// 如果错误没有被任何 errorCaptured 钩子或应用级处理器处理,
// 则记录错误日志(在开发环境可能会抛出错误,生产环境只打印到控制台)
logError(
err,
type,
contextVNode,
throwInDev,
throwUnhandledErrorInProduction
);
}
function logError(
err: unknown,
type: ErrorTypes,
contextVNode: VNode | null,
throwInDev = true,
throwInProd = false
) {
if (__DEV__) {
const info = ErrorTypeStrings[type];
if (contextVNode) {
pushWarningContext(contextVNode);
}
warn(`Unhandled error${info ? ` during execution of ${info}` : ``}`);
if (contextVNode) {
popWarningContext();
}
// 开发环境默认抛出错误,便于调试
if (throwInDev) {
throw err;
} else if (!__TEST__) {
console.error(err);
}
} else if (throwInProd) {
throw err;
} else {
// 生产环境恢复执行,减少对最终用户的影响
console.error(err);
}
}
思考
1. 为什么是父组件去捕获子组件错误,不是子组件自身?
看 Vue 源码实现,可以发现触发错误钩子都是先获取 parent 实例,然后再逐层向上查找错误钩子,而不是获取组件自身的钩子。
主要原因是子组件在出错时,组件状态可能已经不可靠。如果这时候有依赖 UI 渲染的场景,组件即使捕获了错误,也没法做 UI 降级的处理机制。
如果是在稳定的父组件中捕获错误,可以由父组件去兜底组件异常问题的处理流程。
总结
通过 errorCaptured/onErrorCaptured 钩子和全局 errorHandler 处理组件树中的错误。
采用向上冒泡的传播机制,优先级为:组件树向上传播 → 全局错误处理 → 最终 console.error。
同时 Vue 3 和 Vue 2 的差异:
- API 差异 :Vue 2 使用选项式 API 的
errorCaptured,Vue 3 使用组合式 API 的onErrorCaptured。 - 组件树遍历 :Vue 2 使用
vm.$parent,Vue 3 使用instance.parent。 - 钩子访问 :Vue 2 使用
cur.$options.errorCaptured,Vue 3 使用cur.ec(更简洁)。 - 暴露实例 :Vue 2 直接传递组件实例
vm,Vue 3 传递渲染代理instance.proxy。 - 错误信息:Vue 2 使用固定字符串,Vue 3 开发环境用字符串,生产环境用文档链接。
- 响应式控制 :Vue 2 使用
pushTarget/popTarget,Vue 3 使用pauseTracking/resetTracking。 - 类型支持:Vue 3 有更好的 TypeScript 类型支持,错误类型使用枚举。
- 全局配置 :Vue 2 使用
Vue.config.errorHandler,Vue 3 使用app.config.errorHandler。