天天看點

純幹貨!你不知道的Vue錯誤處理機制

陽光明媚的一天,面試官面了一個小夥子。小夥子在介紹項目時,說做了個錯誤上報機制,前端用的是Vue的錯誤捕獲。這時面試官瞟了一眼履歷,一行“熟悉Vue2源碼”的字眼印入眼簾。待小夥介紹完後,面試官說不錯不錯,那你說說Vue的錯誤處理吧。小夥子雙眼一瞪,心想這老鐵不按常理出牌,說:這題不會!下一個~面試官:emmm... 行,那就下一個...

面試結束,小夥子立馬打開Vue源碼,決定一探究竟...

一、認識Vue錯誤處理

1. errorHandler

首先,可以看看Vue文檔對其的介紹。這裡不贅述太多,直接使用,一起看看列印結果。代碼如下:

// main.js
Vue.config.errorHandler = function (err, vm, info) {
  console.log('全局捕獲 err >>>', err)
  console.log('全局捕獲 vm >>>', vm)
  console.log('全局捕獲 info >>>', info)
}

// App.vue
...
created () {
  const obj = {}
  // 直接在App元件的created鈎子中嘗試錯誤操作,調用obj中不存在的fn
  obj.fn()
},
methods: {
  handleClick () {
    // 綁定一個click事件,點選時觸發
    const obj = {}
    obj.fn()
  }
}
...      
  1. ​created​

    ​ 的輸出結果如下(文章結尾會以此進行 ​

    ​catch​

    ​ 的流程分析):
    純幹貨!你不知道的Vue錯誤處理機制
  2. ​handleClick​

    ​ 的輸出結果如下(文章結尾會以此進行 ​

    ​catch​

    ​ 的流程分析)
    純幹貨!你不知道的Vue錯誤處理機制
    由此可見:
  • ​err​

    ​ 可擷取錯誤資訊、堆棧資訊
  • ​vm​

    ​ 可擷取報錯的vm執行個體(也就是對應的元件)
  • ​info​

    ​ 可擷取特定錯誤資訊。如生命周期資訊 ​

    ​created hook​

    ​,事件資訊 ​

    ​v-on handler​

2. errorCaptured

老規矩,可以先看Vue文檔的介紹,這裡也是直接放上使用案例。代碼如下:

// App.vue
<template>
  // 模版中引用子元件 HelloWorld
  <HelloWorld />
</template>
...
errorCaptured(err, vm, info) {
  // 添加errorCaptured鈎子,其餘跟上述案例一緻
  console.log('父元件捕獲 err >>>', err, vm, info)
}
...

// HelloWorld元件
...
created () {
  const child = {}
  // 直接在子元件的 created 中抛出錯誤,看看列印效果
  child.fn()
}
...      

輸出結果如下:

純幹貨!你不知道的Vue錯誤處理機制

可以看到,​​

​HelloWorld​

​​ 元件中的報錯既給App元件的 ​

​errorCaptured​

​​ 捕獲,也給全局的 ​

​errorHandler​

​ 所捕獲。是不是有點類似我們事件中的 冒泡 呢?

一定要注意,​

​errorCaptured​

​ 是捕獲一個來自 後代元件 的錯誤時被調用,也就是說不能捕捉到自身的。可以做個實驗驗證一下,接着上述的案例稍作改造,在 ​

​HelloWorld​

​​ 中加入 ​

​errorCaptured​

​​ 鈎子,并在 ​

​created​

​ 中列印 ‘子元件也用 errorCaptured 捕獲錯誤’

...
created() {
  console.log('子元件也用 errorCaptured 捕獲錯誤')
  const child = {}
  // 直接在子元件的 created 中抛出錯誤,看看列印效果
  child.fn()
},
errorCaptured(err, vm, info) {
  console.log('子元件捕獲', err, vm, info)
}
...      
純幹貨!你不知道的Vue錯誤處理機制

由此可知,除了多列印一行 ​​

​created​

​ 中的輸出,其他均無變化。

大廠技術  堅持周更  精選好文

3. 一圖總結Vue錯誤捕獲機制

純幹貨!你不知道的Vue錯誤處理機制

錯誤捕獲.png

二、Vue錯誤捕獲源碼

源碼分析的 ​

​Vue​

​​ 版本是 ​

​v2.6.14​

​​,代碼位于 ​

​src/core/util/error.js​

​​。共四個方法:​

​handleError​

​​、​

​invokeWithErrorHandling​

​​、​

​globalHandleError​

​​,​

​logError​

​,接下來,我們一個一個的來認識他們~

1. handleError

Vue 中的錯誤統一處理函數,在此函數中實作向上通知 ​

​errorCaptured​

​​ 直到全局 ​

​errorHandler​

​ 的功能。核心解讀如下:

  • 參數 ​

    ​err​

    ​、​

    ​vm​

    ​、​

    ​info​

  • ​pushTarget​

    ​、​

    ​popTarget​

    ​。源碼中注釋有寫到,主要是避免處理錯誤時 元件 無限渲染
  • ​$parent​

    ​。Vue 元件樹中建立父子關系的屬性,可以通過該屬性不斷向上查找頂層元件——​

    ​大Vue​

    ​(也就是我們初始化時候new Vue的那個),​

    ​大Vue​

    ​的 ​

    ​$parent​

    ​ 是 ​

    ​undefined​

  • 擷取​

    ​errorCaptured​

    ​。可能有小夥伴有疑問這裡為什麼是個數組,因為 Vue 初始化的時候會對 hook 做合并處理。比如說我們用到 ​

    ​mixins​

    ​ 的時候,元件中可能會出現多個相同的 hook,初始化時會把這些 ​

    ​cb​

    ​ 都合并在一個 hook 的數組裡,以便觸發鈎子的時候一一調用
  • ​capture​

    ​。如果為false的時候,直接 return,不會走到 ​

    ​globalHandleError​

    ​ 中

源碼如下:

// 很明顯,這個參數的就是我們熟悉的 err、vm、info
function handleError (err: Error, vm: any, info: string) {
  pushTarget()
  try {
    if (vm) {
      let cur = vm
      // 向上查找$parent,直到不存在
      // 注意了!一上來 cur 就指派給 cur.$parent,是以 errorCaptured 不會在目前元件的錯誤捕獲中執行
      while ((cur = cur.$parent)) {
        // 擷取鈎子errorCaptured
        const hooks = cur.$options.errorCaptured
        if (hooks) {
          for (let i = 0; i < hooks.length; i++) {
            try {
              // 執行errorCaptured
              const capture = hooks[i].call(cur, err, vm, info) === false
              // errorCaptured傳回false,直接return,外層的globalHandleError不會執行
              if (capture) return
            } catch (e) {
              // 如果在執行errorCaptured的時候捕獲到錯誤,會執行globalHandleError,此時的info為:errorCaptured hook
              globalHandleError(e, cur, 'errorCaptured hook')
            }
          }
        }
      }
    }
    // 外層,全局捕獲,隻要上面不return掉,就會執行
    globalHandleError(err, vm, info)
  } finally {
    popTarget()
  }
}      

2. invokeWithErrorHandling

一個包裝函數,内部使用​

​try-catch​

​ 包裹傳入的函數,且有更好的處理異步錯誤的能力。可處理 生命周期 、 事件 等回調函數的錯誤捕獲。可處理傳回值是Promise的異步錯誤捕獲。捕獲到錯誤後,統一派發給 ​

​handleError​

​ ,由它處理向上通知到全局的邏輯。核心解讀如下:

  • 參數 ​

    ​handler​

    ​ 。傳入的執行函數,在内部對其調用,并對其傳回值進行Promise的判斷
  • ​try-catch​

    ​。使用 try-catch 包裹并執行傳入的函數,捕獲錯誤後調用 ​

    ​handleError​

    ​ 。(是不是有點高階函數那味呢~)
  • ​handleError​

    ​。捕獲錯誤後也是調用 handleError 方法對錯誤進行向上通知
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)
    // 判斷傳回是否為Promise 且 未被catch(!res._handled)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // _handled标志置為true,避免嵌套調用時多次觸發catch
      res._handled = true
    }
  } catch (e) {
    // 捕獲錯誤後調用 handleError
    handleError(e, vm, info)
  }
  return res
}      

3. globalHandleError

全局錯誤捕獲。也就是我們在全局配置的 ​

​Vue.config.errorHandler​

​的觸發函數

  • 内部用 ​

    ​try-catch​

    ​ 包裹 ​

    ​errorHandler​

    ​ 的執行。在這裡就會執行我們全局的錯誤捕獲函數~
  • 如果執行 ​

    ​errorHandler​

    ​ 中存在錯誤則被捕獲後通過 ​

    ​logError​

    ​ 列印。(​

    ​logError​

    ​ 在浏覽器的生産環境的使用 ​

    ​console.error​

    ​ 列印)
  • 如果沒有 ​

    ​errorHandler​

    ​。則會直接使用 ​

    ​logError​

    ​ 進行錯誤列印
function globalHandleError (err, vm, info) {
  if (config.errorHandler) {
    try {
      // 調用全局的 errorHandler 并return
      return config.errorHandler.call(null, err, vm, info)
    } catch (e) {
      // 翻譯源碼注釋:如果使用者故意在處理程式中抛出原始錯誤,不要記錄兩次      
      if (e !== err) {
        // 對在 globalHandleError 中的錯誤進行捕獲,通過 logError 輸出
        logError(e, null, 'config.errorHandler')
      }
    }
  }
  // 如果沒有 errorHandler 全局捕獲,則執行到這裡,用 logError 錯誤
  logError(err, vm, info)
}      

4. logError

實作對錯誤資訊的列印(開發環境、線上會有所不同)

  • ​warn​

    ​。開發環境中會使用 warn 列印錯誤。以 ​

    ​[Vue warn]:​

    ​ 開頭
  • ​console.error​

    ​。浏覽器環境中使用 ​

    ​console.error​

    ​ 對捕獲的錯誤進行輸出
// logError源碼實作
function logError (err, vm, info) {
  if (process.env.NODE_ENV !== 'production') {
    // 開發環境中使用 warn 對錯誤進行輸出
    warn(`Error in ${info}: "${err.toString()}"`, vm)
  }
  /* istanbul ignore else */
  if ((inBrowser || inWeex) && typeof console !== 'undefined') {
    // 直接用 console.error 列印錯誤資訊
    console.error(err)
  } else {
    throw err
  }
}

// 簡單看看 warn 的實作
warn = (msg, vm) => {
  const trace = vm ? generateComponentTrace(vm) : ''
  if (config.warnHandler) {
    config.warnHandler.call(null, msg, vm, trace)
  } else if (hasConsole && (!config.silent)) {
    // 這就是我們平時常見的 Vue warn 列印報錯的由來了!
    console.error(`[Vue warn]: ${msg}${trace}`)
  }
}      

看看下圖,如果我們不進行全局錯誤捕獲,在開發環境的報錯輸出是否有點似曾相識呢?????

  • 這裡提個小問題:為什麼 1 個錯誤列印 2 條報錯資訊?
純幹貨!你不知道的Vue錯誤處理機制

image.png

哈哈哈~沒錯,其實就是 ​

​logError​

​​ 函數的實作!!!這裡再回顧一下,​

​logError​

​​ 先是調用 ​

​warn​

​​ 列印 ​

​[Vue warn]:​

​​ 開頭的 ​

​Vue​

​​ 包裝過的錯誤提示資訊,再通過 ​

​console.error​

​ 列印js的錯誤資訊

簡單總結一下:

  1. ​handleError​

    ​:統一的錯誤捕獲函數。實作子元件到頂層元件錯誤捕獲後對 ​

    ​errorCaptured​

    ​ hook 的冒泡調用,執行完全部的 ​

    ​errorCaptured​

    ​ 鈎子後最終執行全局錯誤捕獲函數 globalHandleError。
  2. ​invokeWithErrorHandling​

    ​:包裝函數,通過高階函數的程式設計私思路,通過接收一個函數參數,并在内部使用 ​

    ​try-catch​

    ​ 包裹後執行傳入的函數;還提供更好的異步錯誤處理,當執行函數傳回了一個Promise對象,會在此對其實作進行錯誤捕獲,最後也是通知到 ​

    ​handleError​

    ​ 中(如果我們未自己對傳回的Promise進行catch操作)
  3. ​globalHandleError​

    ​:調用全局配置的 errorHandler 函數,如果在調用的過程中捕獲到錯誤,則通過 ​

    ​logError​

    ​ 列印所捕獲的錯誤,以 'config.errorHandler' 結尾
  4. ​logError​

    ​。實作對未捕獲的錯誤資訊進行列印輸出。開發環境會列印2種錯誤資訊~

三、錯誤捕獲流程分析

看完了錯誤捕獲的源碼實作,不如具體看看Vue是怎麼捕獲到錯誤的,以此來加深下了解。命中錯誤捕獲的方式有很多,這裡以 文章開頭的代碼案例 作為命中分支進行調試,帶你看看Vue是怎麼實作 錯誤捕獲 的~

1.​

​created​

​ 階段的錯誤捕獲

溫習一下 Vue 的整個元件化流程(整個生命周期)做了什麼,如下圖:

純幹貨!你不知道的Vue錯誤處理機制

created的觸發階段是在init階段,如下圖:

純幹貨!你不知道的Vue錯誤處理機制

由此可見,觸發created鈎子的是 ​​

​callHook​

​​ 方法,接下來看下 ​

​callHook​

​ 的實作:

  • 周遊目前 vm 執行個體的目前 hook 的所有 cb,并将其傳入 ​

    ​invokeWithErrorHandling​

    ​ 函數中
  • ​invokeWithErrorHandling​

    ​内會調用 cb,這時會 catch 到錯誤,然後執行 ​

    ​handleError​

    ​。而此時是在 App 元件中,再往上是 ​

    ​大Vue​

    ​ 且已經使用 ​

    ​errorHandler​

    ​ 進行全局錯誤捕獲,是以會觸發到一系列 console.log 的“全局捕獲”
function callHook (vm, hook) {
  pushTarget();
  var handlers = vm.$options[hook];
  // info資訊,這裡是 created hook
  var info = hook + " hook";
  if (handlers) {
    for (var i = 0, j = handlers.length; i < j; i++) {
      // 直接調用invokeWithErrorHandling,傳入對應的 cb
      invokeWithErrorHandling(handlers[i], vm, null, vm, info);
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook);
  }
  popTarget();
}      

2. 點選事件的錯誤捕獲

案例代碼跟 一、認識Vue錯誤處理 中的 ​

​errorHandler​

​ 的 click是一樣的,這裡隻是多一行console.log,友善大家看下打包後的代碼加深了解。因為這部分會涉及到Vue源碼中的另外一個點——事件。當然,這裡不進行展開,大家大緻了解即可。筆者會另外寫一個篇章來介紹 Vue 的事件的源碼解析~

// 模版代碼
<template>
  <div id="app">
    <button @click="handleClick">click</button>
  </div>
</template>

// js代碼
methods: {
  handleClick () {
    console.log('點選事件錯誤捕獲')
    const obj = {}
    obj.fn()
  }
}      

打包後代碼長這樣:

純幹貨!你不知道的Vue錯誤處理機制

由此,在整個Vue初始化的過程中,會對我們綁定的click事件進行 ​​

​updateDOMListeners​

​​ 的處理,然後又會調用到 ​

​updateListeners​

​​ 這個方法,我們來看下 ​

​updateListeners​

​ 核心的代碼做了什麼?這裡大家不用深究原因哈!!!知道這個流程的調用順序即可,因為帖出來也是讓你們了解得更清晰一點。如果感興趣的話可以等筆者出一篇關于Vue事件的源碼分析哈~

function updateListeners () {
  // 這裡的 cur 就是我們寫在 methods 中的 handleClick
  cur = on[name] = createFnInvoker(cur, vm);
}      

可以知道,這裡通過 ​

​createFnInvoker​

​​ 對 我們的 ​

​handleClick​

​ 進行了一層包裝再傳回,而我們的錯誤捕獲就是在包裝的 createFnInvoker 中實作的。我們接着看看 createFnInvoker 做了什麼

function createFnInvoker (fns, vm) {
  function invoker () {
    var arguments$1 = arguments;
    // 從 invoker 的靜态屬性 fns 擷取方法
    var fns = invoker.fns;
    if (Array.isArray(fns)) {
      // 一個fns的新數組
      var cloned = fns.slice();
      for (var i = 0; i < cloned.length; i++) {
        // 對fns使用 invokeWithErrorHandling 進行包裝
        invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler");
      }
    } else {
      // 這裡也是一樣的,隻是對單一的fns使用 invokeWithErrorHandling 進行包裝
      return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler")
    }
  }
  // 這裡的fns,就是上面的cur,也就是我們的handleClick方法
  invoker.fns = fns;
  // 傳回一個 invoker ,我們點選觸發的其實是這個函數
  return invoker
}      

總結一下:

  • 每當我們點選的時候,表面是觸發了 ​

    ​handleClick​

    ​ ,其實是觸發了一個裝飾器 ​

    ​invoker​

  • 再由 ​

    ​invoker​

    ​ 去調用 ​

    ​invokeWithErrorHandling​

    ​ ,并且傳入儲存在 invoker 的靜态屬性 fns 中的函數(也就是我們使用者編寫的 ​

    ​handleClick​

    ​ 函數)
  • 如此一來,就跟 二、Vue錯誤捕獲源碼 中的 ​

    ​2. invokeWithErrorHandling​

    ​ 的執行一樣了
  • 最終會通過 ​

    ​handleError​

    ​ 實作向上冒泡執行上層元件的錯誤鈎子,直至全局的錯誤捕獲 這也是我們 點選事件 的錯誤捕獲流程了~

寫在最後,怎麼樣,是不是非常的簡單呢?錯誤捕獲這個東西,不管是在架構層面,還是我們日常開發業務中都是比較重要的,但往往又被很多人忽略(比如我)。總覽下來,其實這一塊也不難,在 ​

​Vue​

​ 源碼的實作中,大家隻要看過都能懂。總之~學多一點沒壞處吧,面試問到了也不慌,雖然不是 ​

​Vue​

​ 面試的核心重點,但是問到了能答出來肯定是個加分項,那如果一點都答不上來,那可能會減一點分,特别是項目中寫了Vue錯誤捕獲相關的~畢竟這個比起 響應式 那些簡單多了,哈哈哈~

❤️ H5-Dooring,讓H5制作更簡單

目前H5-Dooring架構更新, 已支援多種搭建布局模式, 如網格布局, 自由布局, 可以一鍵切換布局模式:

純幹貨!你不知道的Vue錯誤處理機制

歡迎體驗: http://h5.dooring.cn/h5_plus

❤️ 謝謝支援

以上便是本次分享的全部内容,希望對你有所幫助^_^

喜歡的話别忘了 分享、點贊、收藏 三連哦~。