Vue 错误捕获及其原理
一、errorHandler 全局错误处理
errorHandler 用于为应用内抛出的未捕获错误指定一个全局处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例。
1、参数类型
错误处理器 errorHandler 接收三个参数:
- 错误对象
- 触发该错误的组件实例
- 指出错误来源类型信息的字符串
interface AppConfig {
errorHandler?: (
err: unknown,
instance: ComponentPublicInstance | null,
// info 是一个 Vue 特定的错误信息
// 例如:错误是在哪个生命周期的钩子上抛出的
info: string
) => void
}
errorHandler 可以从下面这些来源中捕获错误:
- 组件渲染器
- 事件处理器
- 生命周期钩子
- setup() 函数
- 侦听器
- 自定义指令钩子
- 过渡 (Transition) 钩子
2、基本用法
app.config.errorHandler = (err, instance, info) => {
// 处理错误,例如:报告给一个服务
}
二、errorCaptured 生命周期钩子
errorCaptured 生命周期钩子在捕获了后代组件传递的错误时调用。
1、参数类型ㅤ
这个钩子带有三个实参:
- 错误对象
- 触发该错误的组件实例
- 说明错误来源类型的信息字符串
interface ComponentOptions {
errorCaptured?(
this: ComponentPublicInstance,
err: unknown,
instance: ComponentPublicInstance | null,
info: string
): boolean | void
}
捕获来源同 errorHandler ↑
可以在 errorCaptured()
中更改组件状态来为用户显示一个错误状态。需要注意的是,如果错误状态渲染为导致本次错误的内容,会进入无限的渲染循环中。
2、错误传递规则
- 默认情况下,所有的错误都会被发送到 app.config.errorHandler (前提是这个函数已经定义) 统一报告给分析服务;
- 如果组件的继承链或组件链上存在多个
errorCaptured
钩子,对于同一个错误,这些钩子会向上传递一一调用; errorCaptured
钩子可以通过返回 false 来阻止错误继续向上传递,即表示“这个错误已经被处理了,应当被忽略”,它将阻止其他的errorCaptured
钩子或 app.config.errorHandler 因这个错误而被调用。
三、错误捕获原理
Vue 异常处理的源码如下:
src/core/util/error.ts
// https://github.com/vuejs/vue/blob/main/src/core/util/error.ts
import config from '../config' // Vue 全局配置,即 app.config
import { warn } from './debug'
import { inBrowser } from './env'
import { isPromise } from 'shared/util'
import { pushTarget, popTarget } from '../observer/dep'
export function handleError(err: Error, vm: any, info: string) {
pushTarget()
try {
// vm 即当前报错的组件实例
if (vm) {
let cur = vm
// 首先获取到报错的组件
// 之后递归查找当前组件的父组件,依次调用 errorCaptured 方法
// 如果遍历调用完所有 errorCaptured 或 errorCaptured 有报错
// 则调用 globalHandleError 方法
while ((cur = cur.$parent)) {
const hooks = cur.$options.errorCaptured
// 判断是否存在 errorCaptured 钩子函数
if (hooks) {
// 选项合并的策略,钩子函数会被保存在一个数组中
for (let i = 0; i < hooks.length; i++) {
// 如果 errorCaptured 钩子执行自身抛出了错误
// 则用 try catch 捕获错误
// 将这个新错误和原本被捕获的错误都发给全局的 config.errorHandler
// 调用 globalHandleError 方法
try {
// 当前 errorCaptured 执行,根据返回是否为 false 做处理
// 如果是 false
// 则阻止其它任何会被这个错误唤起的 errorCaptured 钩子和全局的 config.errorHandler
// 如果不是 false
// 则组件的继承或父级从属链路中存在的多个 errorCaptured 钩子,会被相同的错误逐个唤起
// 调用对应的钩子函数,处理错误
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()
}
}
// 异步错误处理函数
export function invokeWithErrorHandling(
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
// 根据参数选择不同的 handle 执行方式
res = args ? handler.apply(context, args) : handler.call(context)
// 1. handle 返回结果存在
// 2. 传入的值是 Vue 实例本身
// 3. 结果是 Promise
// 4. onRejected
if (res && !res._isVue && isPromise(res) && !(res as any)._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
// 避免嵌套调用时 catch 多次的触发
;(res as any)._handled = true
}
} catch (e: any) {
// 处理执行错误
handleError(e, vm, info)
}
return res
}
// 全局错误处理
function globalHandleError(err, vm, info) {
// 获取全局配置,判断是否设置处理函数,默认为 undefined
if (config.errorHandler) {
// try catch 住全局错误处理函数
try {
// 执行设置的全局错误处理函数
return config.errorHandler.call(null, err, vm, info)
} catch (e: any) {
// 如果在 errorHandler 中手动抛出同样错误信息
// 则判断 err 信息是否相等,避免重复 log
if (e !== err) {
logError(e, null, 'config.errorHandler')
}
}
}
// 未配置常规 log 输出
logError(err, vm, info)
}
// 错误输出函数
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 可通过 errorHandler 来全局处理错误,通过 errorCaptured
生命周期钩子来捕获组件错误。
Vue 异常处理的原理是:
通过 handleError 方法来捕获异常,获取报错的组件,递归查找其父组件,依次调用 errorCaptured
钩子函数,在遍历执行完或遇到报错时,调用 errorHandler 全局错误处理函数,最后输出错误信息。