任何一个框架,对于错误的处理都是一种必备的能力。在 Vue 中,则是定义了一套对应的错误处理规则给到使用者。且在源代码级别,对部分必要的过程做了一定的错误处理。
全局设置错误处理
在 Vue 全局设置的 API 中,我们可以设置全局错误处理函数,用法如下:
Vue.config.errorHandler = function (err, vm, info) {
// handle error
// `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
// 只在 2.2.0+ 可用
}
该函数可以作为指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例。
如果我们想要针对自己的应用,对错误做统一的收集与处理(如上报后台系统),那么该 API 是一个极好的嵌入点。
不过值得注意的是,在不同 Vue 版本中,该全局 API 作用的范围会有所不同:
从 2.2.0 起,这个钩子也会捕获组件生命周期钩子里的错误。同样的,当这个钩子是
undefined
时,被捕获的错误会通过console.error
输出而避免应用崩溃。
从 2.4.0 起,这个钩子也会捕获 Vue 自定义事件处理函数内部的错误了。
从 2.6.0 起,这个钩子也会捕获
v-on
DOM 监听器内部抛出的错误。另外,如果任何被覆盖的钩子或处理函数返回一个 Promise 链 (例如 async 函数),则来自其 Promise 链的错误也会被处理。
生命周期钩子 errorCaptured
这是 2.5.0 新增的一个生命钩子函数。可以点击这里查看详细。
它被调用的时机在于:当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false
以阻止该错误继续向上传播。
错误传播规则
参考官网的解释:
- 默认情况下,如果全局的
config.errorHandler
被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报。 - 如果一个组件的继承或父级从属链路中存在多个
errorCaptured
钩子,则它们将会被相同的错误逐个唤起。 - 如果此
errorCaptured
钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的config.errorHandler
。 - 一个
errorCaptured
钩子能够返回false
以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的errorCaptured
钩子和全局的config.errorHandler
。
内置的错误处理函数
在 Vue 2.6.10 的源码中,文件src/core/util/error.js
中定义了对于 Vue 内部自身使用的几个错误处理函数。针对同步异常与异步异常,有不同处理方式。我们详细来看:
处理同步异常
处理同步异常的函数是 handleError(err: Error, vm: any, info: string)
。详细实现:
export function handleError (err: Error, vm: any, info: string) {
// Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
// 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 {
const capture = hooks[i].call(cur, err, vm, info) === false
if (capture) return
} catch (e) {
globalHandleError(e, cur, 'errorCaptured hook')
}
}
}
}
}
globalHandleError(err, vm, info)
} finally {
popTarget()
}
}
该代码对上面提到的“错误传播规则”做了实现。如果一个组件的继承或父级从属链路中存在多个 errorCaptured
钩子,则它们将会被相同的错误逐个唤起。 errorCaptured
钩子能够返回 false
以阻止错误继续向上传播。最后,通过调用 globalHandleError()
方法:
function globalHandleError (err, vm, info) {
if (config.errorHandler) {
try {
return config.errorHandler.call(null, err, vm, info)
} catch (e) {
// if the user intentionally throws the original error in the handler,
// do not log it twice
if (e !== err) {
logError(e, null, 'config.errorHandler')
}
}
}
logError(err, vm, info)
}
globalHandleError()
方法最终调用的是全局的 config.errorHandler()
方法。
处理异步异常
对于异步异常怎么处理呢?也好办,将异步处理的函数包裹一层,当异步处理函数在执行过程中出现错误的时候,将异常捕获并处理。具体的实现在invokeWithErrorHandling()
方法:
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
代码中,对包裹函数的返回是否是异步函数做了isPromise()
的判断:
export function isPromise (val: any): boolean {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
符合异步函数的条件之后,将上文提到的handleError
写入到 promise.catch 中。
这样,就完成了对于异步函数的处理过程。
源码在何处使用了异步异常捕获
hook钩子函数
// src/core/instance/lifecycle.js
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
自定义事件的处理函数
//src/core/instance/events.js
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
v-on 监听器
//src/core/vdom/helpers/update-listeners.js
export function createFnInvoker (fns: Function | Array<Function>, vm: ?Component): Function {
function invoker () {
const fns = invoker.fns
if (Array.isArray(fns)) {
const cloned = fns.slice()
for (let i = 0; i < cloned.length; i++) {
invokeWithErrorHandling(cloned[i], null, arguments, vm, `v-on handler`)
}
} else {
// return handler return value for single handlers
return invokeWithErrorHandling(fns, null, arguments, vm, `v-on handler`)
}
}
invoker.fns = fns
return invoker
}
这几个地方,其实分别对应的就是一开头所提到的全局 API Vue.config.errorHandler 作用的范围。
总结
在这篇文章,我们了解了 Vue 的错误处理机制。章节内容不多,希望对于读者了解 Vue 内部的原理有一点帮助。
vue源码解读文章目录:
Vue 更多系列: