純干貨!你不知道的Vue錯(cuò)誤處理機(jī)制
陽(yáng)光明媚的一天,面試官面了一個(gè)小伙子。小伙子在介紹項(xiàng)目時(shí),說(shuō)做了個(gè)錯(cuò)誤上報(bào)機(jī)制,前端用的是Vue的錯(cuò)誤捕獲。這時(shí)面試官瞟了一眼簡(jiǎn)歷,一行“熟悉Vue2源碼”的字眼印入眼簾。待小伙介紹完后,面試官說(shuō)不錯(cuò)不錯(cuò),那你說(shuō)說(shuō)Vue的錯(cuò)誤處理吧。小伙子雙眼一瞪,心想這老鐵不按常理出牌,說(shuō):這題不會(huì)!下一個(gè)~面試官:emmm... 行,那就下一個(gè)...
面試結(jié)束,小伙子立馬打開(kāi)Vue源碼,決定一探究竟...
一、認(rèn)識(shí)Vue錯(cuò)誤處理
1. errorHandler
首先,可以看看Vue文檔對(duì)其的介紹。這里不贅述太多,直接使用,一起看看打印結(jié)果。代碼如下:
//?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鉤子中嘗試錯(cuò)誤操作,調(diào)用obj中不存在的fn
??obj.fn()
},
methods:?{
??handleClick?()?{
????//?綁定一個(gè)click事件,點(diǎn)擊時(shí)觸發(fā)
????const?obj?=?{}
????obj.fn()
??}
}
...
created的輸出結(jié)果如下(文章結(jié)尾會(huì)以此進(jìn)行catch的流程分析):
handleClick的輸出結(jié)果如下(文章結(jié)尾會(huì)以此進(jìn)行catch的流程分析)
由此可見(jiàn):
err可獲取錯(cuò)誤信息、堆棧信息vm可獲取報(bào)錯(cuò)的vm實(shí)例(也就是對(duì)應(yīng)的組件)info可獲取特定錯(cuò)誤信息。如生命周期信息created hook,事件信息v-on handler
2. errorCaptured
老規(guī)矩,可以先看Vue文檔的介紹,這里也是直接放上使用案例。代碼如下:
//?App.vue
??//?模版中引用子組件?HelloWorld
??<HelloWorld?/>
</template>
...
errorCaptured(err,?vm,?info)?{
??//?添加errorCaptured鉤子,其余跟上述案例一致
??console.log('父組件捕獲?err?>>>',?err,?vm,?info)
}
...
//?HelloWorld組件
...
created?()?{
??const?child?=?{}
??//?直接在子組件的?created?中拋出錯(cuò)誤,看看打印效果
??child.fn()
}
...
輸出結(jié)果如下:
可以看到,HelloWorld 組件中的報(bào)錯(cuò)既給App組件的 errorCaptured 捕獲,也給全局的 errorHandler 所捕獲。是不是有點(diǎn)類似我們事件中的 冒泡 呢?
一定要注意,errorCaptured 是捕獲一個(gè)來(lái)自 后代組件 的錯(cuò)誤時(shí)被調(diào)用,也就是說(shuō)不能捕捉到自身的。可以做個(gè)實(shí)驗(yàn)驗(yàn)證一下,接著上述的案例稍作改造,在 HelloWorld 中加入 errorCaptured 鉤子,并在 created 中打印 ‘子組件也用 errorCaptured 捕獲錯(cuò)誤’
...
created()?{
??console.log('子組件也用?errorCaptured?捕獲錯(cuò)誤')
??const?child?=?{}
??//?直接在子組件的?created?中拋出錯(cuò)誤,看看打印效果
??child.fn()
},
errorCaptured(err,?vm,?info)?{
??console.log('子組件捕獲',?err,?vm,?info)
}
...
由此可知,除了多打印一行 created 中的輸出,其他均無(wú)變化。
3. 一圖總結(jié)Vue錯(cuò)誤捕獲機(jī)制

二、Vue錯(cuò)誤捕獲源碼
源碼分析的 Vue 版本是 v2.6.14,代碼位于 src/core/util/error.js。共四個(gè)方法:handleError、invokeWithErrorHandling、globalHandleError,logError,接下來(lái),我們一個(gè)一個(gè)的來(lái)認(rèn)識(shí)他們~
1. handleError
Vue 中的錯(cuò)誤統(tǒng)一處理函數(shù),在此函數(shù)中實(shí)現(xiàn)向上通知 errorCaptured 直到全局 errorHandler 的功能。核心解讀如下:
參數(shù) err、vm、infopushTarget、popTarget。源碼中注釋有寫(xiě)到,主要是避免處理錯(cuò)誤時(shí) 組件 無(wú)限渲染$parent。Vue 組件樹(shù)中建立父子關(guān)系的屬性,可以通過(guò)該屬性不斷向上查找頂層組件——大Vue(也就是我們初始化時(shí)候new Vue的那個(gè)),大Vue的$parent是undefined獲取 errorCaptured。可能有小伙伴有疑問(wèn)這里為什么是個(gè)數(shù)組,因?yàn)?Vue 初始化的時(shí)候會(huì)對(duì) hook 做合并處理。比如說(shuō)我們用到mixins的時(shí)候,組件中可能會(huì)出現(xiàn)多個(gè)相同的 hook,初始化時(shí)會(huì)把這些cb都合并在一個(gè) hook 的數(shù)組里,以便觸發(fā)鉤子的時(shí)候一一調(diào)用capture。如果為false的時(shí)候,直接 return,不會(huì)走到globalHandleError中
源碼如下:
//?很明顯,這個(gè)參數(shù)的就是我們熟悉的?err、vm、info
function?handleError?(err:?Error,?vm:?any,?info:?string)?{
??pushTarget()
??try?{
????if?(vm)?{
??????let?cur?=?vm
??????//?向上查找$parent,直到不存在
??????//?注意了!一上來(lái) cur 就賦值給 cur.$parent,所以 errorCaptured 不會(huì)在當(dāng)前組件的錯(cuò)誤捕獲中執(zhí)行
??????while?((cur?=?cur.$parent))?{
????????//?獲取鉤子errorCaptured
????????const?hooks?=?cur.$options.errorCaptured
????????if?(hooks)?{
??????????for?(let?i?=?0;?i?????????????try?{
??????????????//?執(zhí)行errorCaptured
??????????????const?capture?=?hooks[i].call(cur,?err,?vm,?info)?===?false
??????????????//?errorCaptured返回false,直接return,外層的globalHandleError不會(huì)執(zhí)行
??????????????if?(capture)?return
????????????}?catch?(e)?{
??????????????//?如果在執(zhí)行errorCaptured的時(shí)候捕獲到錯(cuò)誤,會(huì)執(zhí)行g(shù)lobalHandleError,此時(shí)的info為:errorCaptured hook
??????????????globalHandleError(e,?cur,?'errorCaptured?hook')
????????????}
??????????}
????????}
??????}
????}
????//?外層,全局捕獲,只要上面不return掉,就會(huì)執(zhí)行
????globalHandleError(err,?vm,?info)
??}?finally?{
????popTarget()
??}
}
2. invokeWithErrorHandling
一個(gè)包裝函數(shù),內(nèi)部使用try-catch 包裹傳入的函數(shù),且有更好的處理異步錯(cuò)誤的能力。可處理 生命周期 、 事件 等回調(diào)函數(shù)的錯(cuò)誤捕獲。可處理返回值是Promise的異步錯(cuò)誤捕獲。捕獲到錯(cuò)誤后,統(tǒng)一派發(fā)給 handleError ,由它處理向上通知到全局的邏輯。核心解讀如下:
參數(shù) handler。傳入的執(zhí)行函數(shù),在內(nèi)部對(duì)其調(diào)用,并對(duì)其返回值進(jìn)行Promise的判斷try-catch。使用 try-catch 包裹并執(zhí)行傳入的函數(shù),捕獲錯(cuò)誤后調(diào)用handleError。(是不是有點(diǎn)高階函數(shù)那味呢~)handleError。捕獲錯(cuò)誤后也是調(diào)用 handleError 方法對(duì)錯(cuò)誤進(jìn)行向上通知
function?invokeWithErrorHandling?(
??handler:?Function,
??context:?any,
??args:?null?|?any[],
??vm:?any,
??info:?string
)?{
??let?res
??try?{
????//?處理handle的參數(shù)并調(diào)用
????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標(biāo)志置為true,避免嵌套調(diào)用時(shí)多次觸發(fā)catch
??????res._handled?=?true
????}
??}?catch?(e)?{
????//?捕獲錯(cuò)誤后調(diào)用?handleError
????handleError(e,?vm,?info)
??}
??return?res
}
3. globalHandleError
全局錯(cuò)誤捕獲。也就是我們?cè)谌峙渲玫?Vue.config.errorHandler的觸發(fā)函數(shù)
內(nèi)部用 try-catch包裹errorHandler的執(zhí)行。在這里就會(huì)執(zhí)行我們?nèi)值腻e(cuò)誤捕獲函數(shù)~如果執(zhí)行 errorHandler中存在錯(cuò)誤則被捕獲后通過(guò)logError打印。(logError在瀏覽器的生產(chǎn)環(huán)境的使用console.error打印)如果沒(méi)有 errorHandler。則會(huì)直接使用logError進(jìn)行錯(cuò)誤打印
function?globalHandleError?(err,?vm,?info)?{
??if?(config.errorHandler)?{
????try?{
??????//?調(diào)用全局的?errorHandler?并return
??????return?config.errorHandler.call(null,?err,?vm,?info)
????}?catch?(e)?{
??????//?翻譯源碼注釋:如果用戶故意在處理程序中拋出原始錯(cuò)誤,不要記錄兩次??????
??????if?(e?!==?err)?{
????????//?對(duì)在?globalHandleError?中的錯(cuò)誤進(jìn)行捕獲,通過(guò)?logError?輸出
????????logError(e,?null,?'config.errorHandler')
??????}
????}
??}
??//?如果沒(méi)有?errorHandler?全局捕獲,則執(zhí)行到這里,用?logError?錯(cuò)誤
??logError(err,?vm,?info)
}
4. logError
實(shí)現(xiàn)對(duì)錯(cuò)誤信息的打印(開(kāi)發(fā)環(huán)境、線上會(huì)有所不同)
warn。開(kāi)發(fā)環(huán)境中會(huì)使用 warn 打印錯(cuò)誤。以[Vue warn]:開(kāi)頭console.error。瀏覽器環(huán)境中使用console.error對(duì)捕獲的錯(cuò)誤進(jìn)行輸出
//?logError源碼實(shí)現(xiàn)
function?logError?(err,?vm,?info)?{
??if?(process.env.NODE_ENV?!==?'production')?{
????//?開(kāi)發(fā)環(huán)境中使用?warn?對(duì)錯(cuò)誤進(jìn)行輸出
????warn(`Error?in?${info}:?"${err.toString()}"`,?vm)
??}
??/*?istanbul?ignore?else?*/
??if?((inBrowser?||?inWeex)?&&?typeof?console?!==?'undefined')?{
????//?直接用?console.error?打印錯(cuò)誤信息
????console.error(err)
??}?else?{
????throw?err
??}
}
//?簡(jiǎn)單看看?warn?的實(shí)現(xiàn)
warn?=?(msg,?vm)?=>?{
??const?trace?=?vm???generateComponentTrace(vm)?:?''
??if?(config.warnHandler)?{
????config.warnHandler.call(null,?msg,?vm,?trace)
??}?else?if?(hasConsole?&&?(!config.silent))?{
????//?這就是我們平時(shí)常見(jiàn)的 Vue warn 打印報(bào)錯(cuò)的由來(lái)了!
????console.error(`[Vue?warn]:?${msg}${trace}`)
??}
}
看看下圖,如果我們不進(jìn)行全局錯(cuò)誤捕獲,在開(kāi)發(fā)環(huán)境的報(bào)錯(cuò)輸出是否有點(diǎn)似曾相識(shí)呢???
這里提個(gè)小問(wèn)題:為什么 1 個(gè)錯(cuò)誤打印 2 條報(bào)錯(cuò)信息?

哈哈哈~沒(méi)錯(cuò),其實(shí)就是 logError 函數(shù)的實(shí)現(xiàn)!!!這里再回顧一下,logError 先是調(diào)用 warn 打印 [Vue warn]: 開(kāi)頭的 Vue 包裝過(guò)的錯(cuò)誤提示信息,再通過(guò) console.error 打印js的錯(cuò)誤信息
簡(jiǎn)單總結(jié)一下:
handleError:統(tǒng)一的錯(cuò)誤捕獲函數(shù)。實(shí)現(xiàn)子組件到頂層組件錯(cuò)誤捕獲后對(duì)errorCapturedhook 的冒泡調(diào)用,執(zhí)行完全部的errorCaptured鉤子后最終執(zhí)行全局錯(cuò)誤捕獲函數(shù) globalHandleError。invokeWithErrorHandling:包裝函數(shù),通過(guò)高階函數(shù)的編程私思路,通過(guò)接收一個(gè)函數(shù)參數(shù),并在內(nèi)部使用try-catch包裹后執(zhí)行傳入的函數(shù);還提供更好的異步錯(cuò)誤處理,當(dāng)執(zhí)行函數(shù)返回了一個(gè)Promise對(duì)象,會(huì)在此對(duì)其實(shí)現(xiàn)進(jìn)行錯(cuò)誤捕獲,最后也是通知到handleError中(如果我們未自己對(duì)返回的Promise進(jìn)行catch操作)globalHandleError:調(diào)用全局配置的 errorHandler 函數(shù),如果在調(diào)用的過(guò)程中捕獲到錯(cuò)誤,則通過(guò)logError打印所捕獲的錯(cuò)誤,以 'config.errorHandler' 結(jié)尾logError。實(shí)現(xiàn)對(duì)未捕獲的錯(cuò)誤信息進(jìn)行打印輸出。開(kāi)發(fā)環(huán)境會(huì)打印2種錯(cuò)誤信息~
三、錯(cuò)誤捕獲流程分析
看完了錯(cuò)誤捕獲的源碼實(shí)現(xiàn),不如具體看看Vue是怎么捕獲到錯(cuò)誤的,以此來(lái)加深下理解。命中錯(cuò)誤捕獲的方式有很多,這里以 文章開(kāi)頭的代碼案例 作為命中分支進(jìn)行調(diào)試,帶你看看Vue是怎么實(shí)現(xiàn) 錯(cuò)誤捕獲 的~
1. created 階段的錯(cuò)誤捕獲
溫習(xí)一下 Vue 的整個(gè)組件化流程(整個(gè)生命周期)做了什么,如下圖:
created的觸發(fā)階段是在init階段,如下圖:
由此可見(jiàn),觸發(fā)created鉤子的是 callHook 方法,接下來(lái)看下 callHook 的實(shí)現(xiàn):
遍歷當(dāng)前 vm 實(shí)例的當(dāng)前 hook 的所有 cb,并將其傳入 invokeWithErrorHandling函數(shù)中invokeWithErrorHandling內(nèi)會(huì)調(diào)用 cb,這時(shí)會(huì) catch 到錯(cuò)誤,然后執(zhí)行handleError。而此時(shí)是在 App 組件中,再往上是大Vue且已經(jīng)使用errorHandler進(jìn)行全局錯(cuò)誤捕獲,所以會(huì)觸發(fā)到一系列 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???????//?直接調(diào)用invokeWithErrorHandling,傳入對(duì)應(yīng)的?cb
??????invokeWithErrorHandling(handlers[i],?vm,?null,?vm,?info);
????}
??}
??if?(vm._hasHookEvent)?{
????vm.$emit('hook:'?+?hook);
??}
??popTarget();
}
2. 點(diǎn)擊事件的錯(cuò)誤捕獲
案例代碼跟 一、認(rèn)識(shí)Vue錯(cuò)誤處理 中的 errorHandler 的 click是一樣的,這里只是多一行console.log,方便大家看下打包后的代碼加深理解。因?yàn)檫@部分會(huì)涉及到Vue源碼中的另外一個(gè)點(diǎn)——事件。當(dāng)然,這里不進(jìn)行展開(kāi),大家大致了解即可。筆者會(huì)另外寫(xiě)一個(gè)篇章來(lái)介紹 Vue 的事件的源碼解析~
//?模版代碼
??<div?id="app">
????<button?@click="handleClick">clickbutton>
??div>
</template>
//?js代碼
methods:?{
??handleClick?()?{
????console.log('點(diǎn)擊事件錯(cuò)誤捕獲')
????const?obj?=?{}
????obj.fn()
??}
}
打包后代碼長(zhǎng)這樣:
由此,在整個(gè)Vue初始化的過(guò)程中,會(huì)對(duì)我們綁定的click事件進(jìn)行 updateDOMListeners 的處理,然后又會(huì)調(diào)用到 updateListeners 這個(gè)方法,我們來(lái)看下 updateListeners 核心的代碼做了什么?這里大家不用深究原因哈!!!知道這個(gè)流程的調(diào)用順序即可,因?yàn)樘鰜?lái)也是讓你們理解得更清晰一點(diǎn)。如果感興趣的話可以等筆者出一篇關(guān)于Vue事件的源碼分析哈~
function?updateListeners?()?{
??//?這里的?cur?就是我們寫(xiě)在?methods?中的?handleClick
??cur?=?on[name]?=?createFnInvoker(cur,?vm);
}
可以知道,這里通過(guò) createFnInvoker 對(duì) 我們的 handleClick 進(jìn)行了一層包裝再返回,而我們的錯(cuò)誤捕獲就是在包裝的 createFnInvoker 中實(shí)現(xiàn)的。我們接著看看 createFnInvoker 做了什么
function?createFnInvoker?(fns,?vm)?{
??function?invoker?()?{
????var?arguments$1?=?arguments;
????//?從?invoker?的靜態(tài)屬性?fns?獲取方法
????var?fns?=?invoker.fns;
????if?(Array.isArray(fns))?{
??????//?一個(gè)fns的新數(shù)組
??????var?cloned?=?fns.slice();
??????for?(var?i?=?0;?i?????????//?對(duì)fns使用?invokeWithErrorHandling?進(jìn)行包裝
????????invokeWithErrorHandling(cloned[i],?null,?arguments$1,?vm,?"v-on?handler");
??????}
????}?else?{
??????//?這里也是一樣的,只是對(duì)單一的fns使用?invokeWithErrorHandling?進(jìn)行包裝
??????return?invokeWithErrorHandling(fns,?null,?arguments,?vm,?"v-on?handler")
????}
??}
??//?這里的fns,就是上面的cur,也就是我們的handleClick方法
??invoker.fns?=?fns;
??//?返回一個(gè)?invoker?,我們點(diǎn)擊觸發(fā)的其實(shí)是這個(gè)函數(shù)
??return?invoker
}
總結(jié)一下:
每當(dāng)我們點(diǎn)擊的時(shí)候,表面是觸發(fā)了 handleClick,其實(shí)是觸發(fā)了一個(gè)裝飾器invoker再由 invoker去調(diào)用invokeWithErrorHandling,并且傳入保存在 invoker 的靜態(tài)屬性 fns 中的函數(shù)(也就是我們用戶編寫(xiě)的handleClick函數(shù))如此一來(lái),就跟 二、Vue錯(cuò)誤捕獲源碼 中的 2. invokeWithErrorHandling的執(zhí)行一樣了最終會(huì)通過(guò) handleError實(shí)現(xiàn)向上冒泡執(zhí)行上層組件的錯(cuò)誤鉤子,直至全局的錯(cuò)誤捕獲 這也是我們 點(diǎn)擊事件 的錯(cuò)誤捕獲流程了~
寫(xiě)在最后,怎么樣,是不是非常的簡(jiǎn)單呢?錯(cuò)誤捕獲這個(gè)東西,不管是在框架層面,還是我們?nèi)粘i_(kāi)發(fā)業(yè)務(wù)中都是比較重要的,但往往又被很多人忽略(比如我)。總覽下來(lái),其實(shí)這一塊也不難,在 Vue 源碼的實(shí)現(xiàn)中,大家只要看過(guò)都能懂。總之~學(xué)多一點(diǎn)沒(méi)壞處吧,面試問(wèn)到了也不慌,雖然不是 Vue 面試的核心重點(diǎn),但是問(wèn)到了能答出來(lái)肯定是個(gè)加分項(xiàng),那如果一點(diǎn)都答不上來(lái),那可能會(huì)減一點(diǎn)分,特別是項(xiàng)目中寫(xiě)了Vue錯(cuò)誤捕獲相關(guān)的~畢竟這個(gè)比起 響應(yīng)式 那些簡(jiǎn)單多了,哈哈哈~
