Vue 的錯誤處理機制

任何一個框架,對於錯誤的處理都是一種必備的能力。在 Vue 中,則是定義了一套對應的錯誤處理規則給到使用者。且在源代碼級別,對部分必要的過程作了必定的錯誤處理。vue

全局設置錯誤處理

在 Vue 全局設置的 API 中,咱們能夠設置全局錯誤處理函數,用法以下:git

Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的錯誤信息,好比錯誤所在的生命週期鉤子
  // 只在 2.2.0+ 可用
}
複製代碼

該函數能夠做爲指定組件的渲染和觀察期間未捕獲錯誤的處理函數。這個處理函數被調用時,可獲取錯誤信息和 Vue 實例。github

若是咱們想要針對本身的應用,對錯誤作統一的收集與處理(如上報後臺系統),那麼該 API 是一個極好的嵌入點。算法

不過值得注意的是,在不一樣 Vue 版本中,該全局 API 做用的範圍會有所不一樣:vuex

從 2.2.0 起,這個鉤子也會捕獲組件生命週期鉤子裏的錯誤。一樣的,當這個鉤子是 undefined 時,被捕獲的錯誤會經過 console.error 輸出而避免應用崩潰。api

從 2.4.0 起,這個鉤子也會捕獲 Vue 自定義事件處理函數內部的錯誤了。數組

從 2.6.0 起,這個鉤子也會捕獲 v-on DOM 監聽器內部拋出的錯誤。另外,若是任何被覆蓋的鉤子或處理函數返回一個 Promise 鏈 (例如 async 函數),則來自其 Promise 鏈的錯誤也會被處理。promise

生命週期鉤子 errorCaptured

這是 2.5.0 新增的一個生命鉤子函數。能夠點擊這裏查看詳細app

它被調用的時機在於:當捕獲一個來自子孫組件的錯誤時被調用。此鉤子會收到三個參數:錯誤對象、發生錯誤的組件實例以及一個包含錯誤來源信息的字符串。此鉤子能夠返回 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的手寫實現

相關文章
相關標籤/搜索