淺出Vue 錯(cuò)誤處理機(jī)制errorCaptured、errorHandler

引子

JavaScript本身是一個(gè)弱類型語(yǔ)言,項(xiàng)目中容易發(fā)生錯(cuò)誤,做好網(wǎng)頁(yè)錯(cuò)誤監(jiān)控,能幫助開發(fā)者迅速定位問題,保證線上穩(wěn)定。
vue項(xiàng)目需接入公司內(nèi)部監(jiān)控平臺(tái),本人之前vue errorHooks不甚了解, 決定探一探??

介紹 errorHandler、errorCaptured

文檔傳送門: errorHandlererrorCaptured

errorHandler

指定組件的渲染和觀察期間未捕獲錯(cuò)誤的處理函數(shù)。這個(gè)處理函數(shù)被調(diào)用時(shí),可獲取錯(cuò)誤信息和 Vue 實(shí)例

Vue.config.errorHandler = function (err, vm, info) {
  #處理錯(cuò)誤信息, 進(jìn)行錯(cuò)誤上報(bào)
  #err錯(cuò)誤對(duì)象
  #vm Vue實(shí)例
  #`info` 是 Vue 特定的錯(cuò)誤信息,比如錯(cuò)誤所在的生命周期鉤子
  #只在 2.2.0+ 可用
}

版本分割點(diǎn)

  • 2.2.0 起,捕獲組件生命周期鉤子里的錯(cuò)誤。同樣的,當(dāng)這個(gè)鉤子是 undefined 時(shí),被捕獲的錯(cuò)誤會(huì)通過 console.error 輸出而避免應(yīng)用崩潰
  • 2.4.0 起,也會(huì)捕獲 Vue 自定義事件處理函數(shù)內(nèi)部的錯(cuò)誤
  • 2.6.0 起,也會(huì)捕獲 v-on DOM 監(jiān)聽器內(nèi)部拋出的錯(cuò)誤。另外,如果任何被覆蓋的鉤子或處理函數(shù)返回一個(gè) Promise 鏈 (例如 async 函數(shù)),則來(lái)自其 Promise 鏈的錯(cuò)誤也會(huì)被處理

errorCaptured

當(dāng)捕獲一個(gè)來(lái)自子孫組件的錯(cuò)誤時(shí)被調(diào)用。此鉤子會(huì)收到三個(gè)參數(shù):錯(cuò)誤對(duì)象、發(fā)生錯(cuò)誤的組件實(shí)例以及一個(gè)包含錯(cuò)誤來(lái)源信息的字符串。此鉤子可以返回 false 以阻止該錯(cuò)誤繼續(xù)向上傳播

錯(cuò)誤傳播規(guī)則

  • 默認(rèn)情況下,如果全局的 config.errorHandler定義,所有的錯(cuò)誤仍會(huì)發(fā)送它,因此這些錯(cuò)誤仍然會(huì)向單一的分析服務(wù)的地方進(jìn)行匯報(bào)
  • 如果一個(gè)組件的繼承或父級(jí)從屬鏈路中存在多個(gè) errorCaptured 鉤子,則它們將會(huì)被相同的錯(cuò)誤逐個(gè)喚起。
  • 如果此 errorCaptured 鉤子自身拋出了一個(gè)錯(cuò)誤,則這個(gè)新錯(cuò)誤和原本被捕獲的錯(cuò)誤都會(huì)發(fā)送給全局的 config.errorHandler,不能捕獲異步promise內(nèi)部拋出的錯(cuò)誤和自身的錯(cuò)誤
  • 一個(gè) errorCaptured 鉤子能夠返回 false 以阻止錯(cuò)誤繼續(xù)向上傳播。本質(zhì)上是說(shuō)“這個(gè)錯(cuò)誤已經(jīng)被搞定了且應(yīng)該被忽略”。它會(huì)阻止其它任何會(huì)被這個(gè)錯(cuò)誤喚起的 errorCaptured 鉤子和全局的 config.errorHandler

錯(cuò)誤信息示例 errorHandler、errorCaptured

光說(shuō)不練,說(shuō)了白干,呈上結(jié)果供各位看官老爺查看

mounted hook 寫入未定義的變量,例如:a mounted() { a}

  • Vue.config.errorHandler err、vm、info


    image
  • Vue.config.errorHandler 拋出同樣的錯(cuò)誤 throw err
    globalHandleError函數(shù)有 e !== err 判斷防止log兩次錯(cuò)誤


    image
  • Vue.config.errorHandler 拋出新的錯(cuò)誤 throw new Error('你好毒')
image
  • errorCaptured (err, vm, info) => ?Boolean 類似于React 錯(cuò)誤處理邊界
<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 半天官網(wǎng)文檔,你是copy忍者嗎?,各位看官老爺,請(qǐng)往下面看,注意自己使用時(shí)的Vue版本,避免err抓取不到??

解讀error.js源碼

Vue 源碼中,異常處理的邏輯放在 /src/core/util/error.js 中

handleError、globalHandleError、invokeWithErrorHandling、logError

  • handleError
    在需要捕獲異常的地方調(diào)用。首先獲取到報(bào)錯(cuò)的組件,之后遞歸查找當(dāng)前組件的父組件,依次調(diào)用errorCaptured 方法。在遍歷調(diào)用完所有 errorCaptured 方法、或 errorCaptured 方法有報(bào)錯(cuò)時(shí),調(diào)用 globalHandleError 方法
  • globalHandleError
    調(diào)用全局的 errorHandler 方法,如果 errorHandler 方法自己又報(bào)錯(cuò)了呢?生產(chǎn)環(huán)境下會(huì)使用 console.error 在控制臺(tái)中輸出
  • invokeWithErrorHandling
    更好的異步錯(cuò)誤處理,當(dāng)時(shí)寫這篇文章時(shí),git history顯示小右哥,一周之前敲的代碼,瞬間透心涼,心飛揚(yáng)
  • logError

    判斷環(huán)境,選擇不同的拋錯(cuò)方式。非生產(chǎn)環(huán)境下,調(diào)用warn方法處理錯(cuò)誤

errorCaptured 和 errorHandler 的觸發(fā)時(shí)機(jī)都是相同的,不同的是 errorCaptured 發(fā)生在前,且如果某個(gè)組件的 errorCaptured 方法返回了 false,那么這個(gè)異常信息不會(huì)再向上冒泡也不會(huì)再調(diào)用 errorHandler 方法

/* @flow */
# Vue 全局配置,也就是上面的Vue.config
import config from '../config'
import { warn } from './debug'
# 判斷環(huán)境
import { inBrowser, inWeex } from './env'
# 判斷是否是Promise,通過val.then === 'function' && val.catch === 'function', val !=== null && val !== undefined
import { isPromise } from 'shared/util'
# 當(dāng)錯(cuò)誤函數(shù)處理錯(cuò)誤時(shí),停用deps跟蹤以避免可能出現(xiàn)的infinite rendering
# 解決以下出現(xiàn)的問題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指當(dāng)前報(bào)錯(cuò)的組件實(shí)例
    if (vm) {
      let cur = vm
      # 首先獲取到報(bào)錯(cuò)的組件,之后遞歸查找當(dāng)前組件的父組件,依次調(diào)用errorCaptured 方法。
      # 在遍歷調(diào)用完所有 errorCaptured 方法、或 errorCaptured 方法有報(bào)錯(cuò)時(shí),調(diào)用 globalHandleError 方法
      while ((cur = cur.$parent)) {
        const hooks = cur.$options.errorCaptured
        # 判斷是否存在errorCaptured鉤子函數(shù)
        if (hooks) {
        # 選項(xiàng)合并的策略,鉤子函數(shù)會(huì)被保存在一個(gè)數(shù)組中
          for (let i = 0; i < hooks.length; i++) {
            # 如果errorCaptured 鉤子執(zhí)行自身拋出了錯(cuò)誤,
            # 則用try{}catch{}捕獲錯(cuò)誤,將這個(gè)新錯(cuò)誤和原本被捕獲的錯(cuò)誤都會(huì)發(fā)送給全局的config.errorHandler
            # 調(diào)用globalHandleError方法
            try {
              # 當(dāng)前errorCaptured執(zhí)行,根據(jù)返回是否是false值
              # 是false,capture = true,阻止其它任何會(huì)被這個(gè)錯(cuò)誤喚起的 errorCaptured 鉤子和全局的 config.errorHandler
              # 是true capture = fale,組件的繼承或父級(jí)從屬鏈路中存在的多個(gè) errorCaptured 鉤子,會(huì)被相同的錯(cuò)誤逐個(gè)喚起
              # 調(diào)用對(duì)應(yīng)的鉤子函數(shù),處理錯(cuò)誤
              const capture = hooks[i].call(cur, err, vm, info) === false
              if (capture) return
            } catch (e) {
              globalHandleError(e, cur, 'errorCaptured hook')
            }
          }
        }
      }
    }
    # 除非禁止錯(cuò)誤向上傳播,否則都會(huì)調(diào)用全局的錯(cuò)誤處理函數(shù)
    globalHandleError(err, vm, info)
  } finally {
    popTarget()
  }
}
# 異步錯(cuò)誤處理函數(shù)
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    # 根據(jù)參數(shù)選擇不同的handle執(zhí)行方式
    res = args ? handler.apply(context, args) : handler.call(context)
    # handle返回結(jié)果存在
    # res._isVue an flag to avoid this being observed,如果傳入值的_isVue為ture時(shí)(即傳入的值是Vue實(shí)例本身)不會(huì)新建observer實(shí)例
    # isPromise(res) 判斷val.then === 'function' && val.catch === 'function', val !=== null && val !== undefined
    # !res._handled  _handle是Promise 實(shí)例的內(nèi)部變量之一,默認(rèn)是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
      # 避免嵌套調(diào)用時(shí)catch多次的觸發(fā)
      res._handled = true
    }
  } catch (e) {
    # 處理執(zhí)行錯(cuò)誤
    handleError(e, vm, info)
  }
  return res
}

#全局錯(cuò)誤處理
function globalHandleError (err, vm, info) {
  # 獲取全局配置,判斷是否設(shè)置處理函數(shù),默認(rèn)undefined
  # 已配置
  if (config.errorHandler) {
    # try{}catch{} 住全局錯(cuò)誤處理函數(shù)
    try {
      # 執(zhí)行設(shè)置的全局錯(cuò)誤處理函數(shù),handle error 想干啥就干啥??
      return config.errorHandler.call(null, err, vm, info)
    } catch (e) {
      # 如果開發(fā)者在errorHandler函數(shù)中手動(dòng)拋出同樣錯(cuò)誤信息throw err
      # 判斷err信息是否相等,避免log兩次
      # 如果拋出新的錯(cuò)誤信息throw err Error('你好毒'),將會(huì)一起log輸出
      if (e !== err) {
        logError(e, null, 'config.errorHandler')
      }
    }
  }
  # 未配置常規(guī)log輸出
  logError(err, vm, info)
}

# 錯(cuò)誤輸出函數(shù)
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
  }
}

歡樂時(shí)光

以上是本人對(duì)vue 錯(cuò)誤處理的淺顯理解,歡迎大家評(píng)論交流,共同進(jìn)步, enjoy !

參考文檔:

vue錯(cuò)誤api
vue錯(cuò)誤處理
Promise源碼剖析
vue/issues/7074

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容