任何一个框架,对于错误的处理都是一种必备的能力。在 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 链的错误也会被处理。
这是 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 中。
这样,就完成了对于异步函数的处理过程。
// 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 } 复制代码
//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 内部的原理有一点帮助。