浅出Vue 错误处理机制errorCaptured、errorHandler

引子

JavaScript本身是一个弱类型语言,项目中容易发生错误,做好网页错误监控,能帮助开发者迅速定位问题,保证线上稳定。
vue项目需接入公司内部监控平台,本人之前vue errorHooks不甚了解, 决定探一探🖼

介绍 errorHandler、errorCaptured

文档传送门: errorHandlererrorCaptured

errorHandler

指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例

Vue.config.errorHandler = function (err, vm, info) {
  #处理错误信息, 进行错误上报
  #err错误对象
  #vm Vue实例
  #`info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  #只在 2.2.0+ 可用
}

版本分割点

  • 2.2.0 起,捕获组件生命周期钩子里的错误。同样的,当这个钩子是 undefined 时,被捕获的错误会通过 console.error 输出而避免应用崩溃
  • 2.4.0 起,也会捕获 Vue 自定义事件处理函数内部的错误
  • 2.6.0 起,也会捕获 v-on DOM 监听器内部抛出的错误。另外,如果任何被覆盖的钩子或处理函数返回一个 Promise 链 (例如 async 函数),则来自其 Promise 链的错误也会被处理

errorCaptured

当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播

错误传播规则

  • 默认情况下,如果全局的 config.errorHandler定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报
  • 如果一个组件的继承或父级从属链路中存在多个 errorCaptured 钩子,则它们将会被相同的错误逐个唤起。
  • 如果此 errorCaptured 钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的 config.errorHandler,不能捕获异步promise内部抛出的错误和自身的错误
  • 一个 errorCaptured 钩子能够返回 false 以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的 errorCaptured 钩子和全局的 config.errorHandler

错误信息示例 errorHandler、errorCaptured

光说不练,说了白干,呈上结果供各位看官老爷查看

mounted hook 写入未定义的变量,例如:a mounted() { a}

  • Vue.config.errorHandler err、vm、info


    image
  • Vue.config.errorHandler 抛出同样的错误 throw err
    globalHandleError函数有 e !== err 判断防止log两次错误


    image
  • Vue.config.errorHandler 抛出新的错误 throw new Error('你好毒')
image
  • errorCaptured (err, vm, info) => ?Boolean 类似于React 错误处理边界
<error-boundary>
  <another-component/>
</error-boundary>
Vue.component('ErrorBoundary', {
  data: () => ({ error: null }),
  errorCaptured (err, vm, info) {
    this.error = `${err.stack}\n\nfound in ${info} of component`
    return false
  },
  render (h) {
    if (this.error) {
      return h('pre', { style: { color: 'red' }}, this.error)
    }
    // ignoring edge cases for the sake of demonstration
    return this.$slots.default[0]
  }
})

正文

copy 半天官网文档,你是copy忍者吗☺,各位看官老爷,请往下面看,注意自己使用时的Vue版本,避免err抓取不到🖤

解读error.js源码

Vue 源码中,异常处理的逻辑放在 /src/core/util/error.js 中

handleError、globalHandleError、invokeWithErrorHandling、logError

  • handleError
    在需要捕获异常的地方调用。首先获取到报错的组件,之后递归查找当前组件的父组件,依次调用errorCaptured 方法。在遍历调用完所有 errorCaptured 方法、或 errorCaptured 方法有报错时,调用 globalHandleError 方法
  • globalHandleError
    调用全局的 errorHandler 方法,如果 errorHandler 方法自己又报错了呢?生产环境下会使用 console.error 在控制台中输出
  • invokeWithErrorHandling
    更好的异步错误处理,当时写这篇文章时,git history显示小右哥,一周之前敲的代码,瞬间透心凉,心飞扬
  • logError

    判断环境,选择不同的抛错方式。非生产环境下,调用warn方法处理错误

errorCaptured 和 errorHandler 的触发时机都是相同的,不同的是 errorCaptured 发生在前,且如果某个组件的 errorCaptured 方法返回了 false,那么这个异常信息不会再向上冒泡也不会再调用 errorHandler 方法

/* @flow */
# Vue 全局配置,也就是上面的Vue.config
import config from '../config'
import { warn } from './debug'
# 判断环境
import { inBrowser, inWeex } from './env'
# 判断是否是Promise,通过val.then === 'function' && val.catch === 'function', val !=== null && val !== undefined
import { isPromise } from 'shared/util'
# 当错误函数处理错误时,停用deps跟踪以避免可能出现的infinite rendering
# 解决以下出现的问题https://github.com/vuejs/vuex/issues/1505的问题
import { pushTarget, popTarget } from '../observer/dep'

export function handleError (err: Error, vm: any, info: string) {
  // Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
  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,capture = true,阻止其它任何会被这个错误唤起的 errorCaptured 钩子和全局的 config.errorHandler
              # 是true capture = fale,组件的继承或父级从属链路中存在的多个 errorCaptured 钩子,会被相同的错误逐个唤起
              # 调用对应的钩子函数,处理错误
              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()
  }
}
# 异步错误处理函数
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)
    # handle返回结果存在
    # res._isVue an flag to avoid this being observed,如果传入值的_isVue为ture时(即传入的值是Vue实例本身)不会新建observer实例
    # isPromise(res) 判断val.then === 'function' && val.catch === 'function', val !=== null && val !== undefined
    # !res._handled  _handle是Promise 实例的内部变量之一,默认是false,代表onFulfilled,onRejected是否被处理
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      # avoid catch triggering multiple times when nested calls
      # 避免嵌套调用时catch多次的触发
      res._handled = true
    }
  } catch (e) {
    # 处理执行错误
    handleError(e, vm, info)
  }
  return res
}

#全局错误处理
function globalHandleError (err, vm, info) {
  # 获取全局配置,判断是否设置处理函数,默认undefined
  # 已配置
  if (config.errorHandler) {
    # try{}catch{} 住全局错误处理函数
    try {
      # 执行设置的全局错误处理函数,handle error 想干啥就干啥💗
      return config.errorHandler.call(null, err, vm, info)
    } catch (e) {
      # 如果开发者在errorHandler函数中手动抛出同样错误信息throw err
      # 判断err信息是否相等,避免log两次
      # 如果抛出新的错误信息throw err Error('你好毒'),将会一起log输出
      if (e !== err) {
        logError(e, null, 'config.errorHandler')
      }
    }
  }
  # 未配置常规log输出
  logError(err, vm, info)
}

# 错误输出函数
function logError (err, vm, info) {
  if (process.env.NODE_ENV !== 'production') {
    warn(`Error in ${info}: "${err.toString()}"`, vm)
  }
  /* istanbul ignore else */
  if ((inBrowser || inWeex) && typeof console !== 'undefined') {
    console.error(err)
  } else {
    throw err
  }
}

欢乐时光

以上是本人对vue 错误处理的浅显理解,欢迎大家评论交流,共同进步, enjoy !

参考文档:

vue错误api
vue错误处理
Promise源码剖析
vue/issues/7074

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335