Vue 错误处理机制源码理解

概念理解

错误捕获钩子(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 的错误处理流程如下:

  1. 组件出错时会触发 handleError,此时先通过 $parent 往父级查找钩子函数。
  2. 遍历执行组件相关的 errorCaptured 钩子函数。
  3. 如果其中之一的 errorCaptured 钩子函数返回 false,那么停止后续的错误捕获,包括全局和组件内的错误钩子。
  4. errorCaptured 执行过程中报错或主动抛错,globalHandleError 会触发 errorHandler 全局错误处理钩子函数去处理 errorCaptured hook
  5. 在完成所有遍历后,如果没有停止错误捕获,最终还是会执行全局钩子 errorHandler,这次是组件自身的错误捕获。
  6. 如果全局 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

参考内容

相关推荐
ejjdhdjdjdjdjjsl2 小时前
Winform初步认识
开发语言·javascript·ecmascript
普通码农2 小时前
PowerShell 神操作:输入「p」直接当「pnpm」用,敲命令速度翻倍!
前端·后端·程序员
2501_942818913 小时前
AI 多模态全栈项目实战:Vue3 + Node 打造 TTS+ASR 全家桶!
vue.js·人工智能·node.js
Komorebi゛3 小时前
【Vue3+Element Plus】el-dialog弹窗点击遮罩层无法关闭弹窗问题记录
前端·vue.js·elementui
vim怎么退出3 小时前
一次线上样式问题复盘:当你钻进 CSS 牛角尖时,问题可能根本不在 CSS
前端·css
echo_e3 小时前
手搓前端虚拟列表
前端
用泥种荷花3 小时前
【LangChain学习笔记】创建智能体
前端
再吃一根胡萝卜3 小时前
在 Ant Design Vue 的 a-table 中将特定数据行固定在底部
前端