Vue 的错误处理机制

4,803 阅读5分钟

任何一个框架,对于错误的处理都是一种必备的能力。在 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构造函数与初始化过程

(二):数据响应式与实现

(三):数组的响应式处理

(四):Vue的异步更新队列

(五):虚拟DOM的引入

(六):数据更新算法--patch算法

(七):组件化机制的实现

(八):计算属性与侦听属性

(九):编译过程的 optimize 阶段

Vue 更多系列:

Vue的错误处理机制

以手写代码的方式解析 Vue 的工作过程

Vue Router的手写实现