陽光明媚的一天,面試官面了一個小夥子。小夥子在介紹項目時,說做了個錯誤上報機制,前端用的是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()
}
}
...
-
的輸出結果如下(文章結尾會以此進行 created
的流程分析):catch
純幹貨!你不知道的Vue錯誤處理機制 -
的輸出結果如下(文章結尾會以此進行 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()
}
...
輸出結果如下:
可以看到,
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)
}
...
由此可知,除了多列印一行
created
中的輸出,其他均無變化。
大廠技術 堅持周更 精選好文
3. 一圖總結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
-
。Vue 元件樹中建立父子關系的屬性,可以通過該屬性不斷向上查找頂層元件——$parent
(也就是我們初始化時候new Vue的那個),大Vue
的 大Vue
是 $parent
undefined
- 擷取
。可能有小夥伴有疑問這裡為什麼是個數組,因為 Vue 初始化的時候會對 hook 做合并處理。比如說我們用到 errorCaptured
的時候,元件中可能會出現多個相同的 hook,初始化時會把這些 mixins
都合并在一個 hook 的數組裡,以便觸發鈎子的時候一一調用cb
-
。如果為false的時候,直接 return,不會走到 capture
中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
,由它處理向上通知到全局的邏輯。核心解讀如下:
- 參數
。傳入的執行函數,在内部對其調用,并對其傳回值進行Promise的判斷handler
-
。使用 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 條報錯資訊?
image.png
哈哈哈~沒錯,其實就是
logError
函數的實作!!!這裡再回顧一下,
logError
先是調用
warn
列印
[Vue warn]:
開頭的
Vue
包裝過的錯誤提示資訊,再通過
console.error
列印js的錯誤資訊
簡單總結一下:
-
:統一的錯誤捕獲函數。實作子元件到頂層元件錯誤捕獲後對 handleError
hook 的冒泡調用,執行完全部的 errorCaptured
鈎子後最終執行全局錯誤捕獲函數 globalHandleError。errorCaptured
-
:包裝函數,通過高階函數的程式設計私思路,通過接收一個函數參數,并在内部使用 invokeWithErrorHandling
包裹後執行傳入的函數;還提供更好的異步錯誤處理,當執行函數傳回了一個Promise對象,會在此對其實作進行錯誤捕獲,最後也是通知到 try-catch
中(如果我們未自己對傳回的Promise進行catch操作)handleError
-
:調用全局配置的 errorHandler 函數,如果在調用的過程中捕獲到錯誤,則通過 globalHandleError
列印所捕獲的錯誤,以 'config.errorHandler' 結尾logError
-
。實作對未捕獲的錯誤資訊進行列印輸出。開發環境會列印2種錯誤資訊~logError
三、錯誤捕獲流程分析
看完了錯誤捕獲的源碼實作,不如具體看看Vue是怎麼捕獲到錯誤的,以此來加深下了解。命中錯誤捕獲的方式有很多,這裡以 文章開頭的代碼案例 作為命中分支進行調試,帶你看看Vue是怎麼實作 錯誤捕獲 的~
1. created
階段的錯誤捕獲
created
溫習一下 Vue 的整個元件化流程(整個生命周期)做了什麼,如下圖:
created的觸發階段是在init階段,如下圖:
由此可見,觸發created鈎子的是
callHook
方法,接下來看下
callHook
的實作:
- 周遊目前 vm 執行個體的目前 hook 的所有 cb,并将其傳入
函數中invokeWithErrorHandling
-
内會調用 cb,這時會 catch 到錯誤,然後執行 invokeWithErrorHandling
。而此時是在 App 元件中,再往上是 handleError
且已經使用 大Vue
進行全局錯誤捕獲,是以會觸發到一系列 console.log 的“全局捕獲”errorHandler
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初始化的過程中,會對我們綁定的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
,并且傳入儲存在 invoker 的靜态屬性 fns 中的函數(也就是我們使用者編寫的 invokeWithErrorHandling
函數)handleClick
- 如此一來,就跟 二、Vue錯誤捕獲源碼 中的
的執行一樣了2. invokeWithErrorHandling
- 最終會通過
實作向上冒泡執行上層元件的錯誤鈎子,直至全局的錯誤捕獲 這也是我們 點選事件 的錯誤捕獲流程了~handleError
寫在最後,怎麼樣,是不是非常的簡單呢?錯誤捕獲這個東西,不管是在架構層面,還是我們日常開發業務中都是比較重要的,但往往又被很多人忽略(比如我)。總覽下來,其實這一塊也不難,在
Vue
源碼的實作中,大家隻要看過都能懂。總之~學多一點沒壞處吧,面試問到了也不慌,雖然不是
Vue
面試的核心重點,但是問到了能答出來肯定是個加分項,那如果一點都答不上來,那可能會減一點分,特别是項目中寫了Vue錯誤捕獲相關的~畢竟這個比起 響應式 那些簡單多了,哈哈哈~
❤️ H5-Dooring,讓H5制作更簡單
目前H5-Dooring架構更新, 已支援多種搭建布局模式, 如網格布局, 自由布局, 可以一鍵切換布局模式:
歡迎體驗: http://h5.dooring.cn/h5_plus
❤️ 謝謝支援
以上便是本次分享的全部内容,希望對你有所幫助^_^
喜歡的話别忘了 分享、點贊、收藏 三連哦~。