一个阳光明媚的日子,面试官面试了一个小伙子。小伙子在介绍项目的时候说自己做了一个报错机制,前端用的是Vue的错误捕获。这时,面试官看了一眼简历,一行“熟悉Vue2源码”的字样映入眼帘。小伙介绍完,面试官说不错,那我们就来说说Vue的错误处理吧。青年瞪了他一眼,觉得这老头不是在按常理出牌,道:“这道题我不会做!”接下来~面试官:emmm...好吧,那下一个...面试结束后,小伙子立马打开了Vue源码,决定一探究竟...1.了解Vue错误处理1.errorHandler首先,你可以看看VueDocumentation里面的介绍。这里就不赘述了,直接使用,一起来看看打印效果吧。代码如下://main.jsVue.config.errorHandler=function(err,vm,info){console.log('全局捕获err>>>',err)console.log('全局捕获vm>>>',vm)console.log('Globalcaptureinfo>>>',info)}//App.vue...created(){constobj={}//直接在created的hook中尝试错误操作App组件,调用obj中不存在的fnobj.fn()},methods:{handleClick(){//绑定一个点击事件,触发constobj={}obj.fn()}}...(1)created输出结果如下(catch的过程文末分析):(2)handleClick的输出如下(catch的过程文末分析)可以看出:err可以获取到错误信息和栈信息vm可以获取到报错的vm实例(也就是对应的组件)infocreatedhookv-onhandler2.errorCaptured的老规矩,大家可以先看看vue文档的介绍,这里也是直接用例。代码如下://App.vue//模板引用子组件HelloWorld...errorCaptured(err,vm,info){//添加errorCapturedhook,并其余与上例一致console.log('父组件捕获err>>>',err,vm,info)}...//HelloWorld组件...created(){constchild={}//直接在子组件的created中抛出如果有错误,看打印效果child.fn()}...输出结果如下:可以看到,HelloWorld组件中报错是由App组件的errorCaptured和全局errorHandler共同捕获。是不是有点类似于我们事件中的冒泡?必须注意的是,errorCaptured是在捕获到后代组件的错误时调用的,也就是说,它不能捕获自己。可以做个实验验证一下,然后稍微修改一下上面的case,在HelloWorld中添加errorCapturedhook,在created中打印'subcomponentsalsouseerrorCapturedtocaptureerrors'...created(){console.log('subcomponent同样使用errorCaptured捕获错误')constchild={}//直接在子组件的created中抛出错误,看打印效果child.fn()},errorCaptured(err,vm,info){console.log('Subcomponentscapture',err,vm,info)}...从这里我们可以看出,除了在created中多打印了一行输出外,没有任何变化。三、一张图总结Vue错误捕获机制Errorcapture.png二、Vue错误捕获源码源码分析Vue版本为v2.6.14,代码位于src/core/util/error.js。一共有四个方法:handleError、invokeWithErrorHandling、globalHandleError、logError。接下来,就让我们一一认识吧~1。handleErrorVue中统一的错误处理函数,实现了将errorCaptured通知给全局errorHandler的功能。核心解释如下:errvminfopushTarget,popTarget。源码中的注释是写的,主要是为了避免处理错误时无限渲染组件。$parentlargeVuelargeVue$parentundefined获取errorCaptured。可能有朋友会有疑问,为什么这是一个数组,因为Vue在初始化的时候会合并hooks。比如我们在使用mixins的时候,组件中可能会有多个相同的hook。在初始化的时候,会将这些cbs合并成一个hooks数组,这样当hooks被触发时,capture就会被一个一个调用。如果为false,直接返回,不会去globalHandleError。源码如下://很明显,这个参数就是大家熟悉的err,vm,infofunctionhandleError(err:Error,vm:any,info:string){pushTarget()try{if(vm){letcur=vm//查找$parent直到它不存在//注意!cur一上来就赋值给cur.$parent,所以errorCaptured不会在当前组件的错误捕获中执行while((cur=cur.$parent)){//获取hookerrorCapturedconsthooks=cur.$options.errorCapturedif(hooks){for(leti=0;ihandleError(e,vm,info+`(Promise/async)`))//将_handled标志设置为true以避免在嵌套调用期间多次触发catchres._handled=true}}catch(e){//捕获错误后调用handleErrorhandleError(e,vm,info)}returnres}3.globalHandleError全局错误捕获。即我们将errorHandler的执行用try-catch包裹在全局配置的Vue.config.errorHandler的触发函数中。这里会执行我们全局的错误捕捉函数~如果errorHandler执行过程中出现错误,会通过logError捕捉并打印出来。(浏览器生产环境使用console.error打印logError)如果没有errorHandler。会直接使用logError进行错误打印functionglobalHandleError(err,vm,info){if(config.errorHandler){try{//调用全局errorHandler并返回returnconfig.errorHandler.call(null,err,vm,info)}catch(e){//翻译一下源码注释:如果用户故意在handler中抛出原始错误,不要记录两次if(e!==err){//在globalHandleError中捕获错误,传递logErroroutputlogError(e,null,'config.errorHandler')}}}//如果没有errorHandler全局捕获,这里执行,使用logErrorerrorlogError(err,vm,info)}4.logError实现打印错误信息(开发环境和线上会有所不同)warn。在开发环境中,会使用warn来打印错误。使用[Vuewarn]:启动console.error。在浏览器环境下,使用console.error输出捕获的错误//logError源码实现函数logError(err,vm,info){if(process.env.NODE_ENV!=='production'){//用于开发环境warn输出错误warn(`Errorin${info}:"${err.toString()}"`,vm)}/*istanbulignoreelse*/if((inBrowser||inWeex)&&typeofconsole!=='undefined'){//直接使用console.error打印错误信息console.error(err)}else{throwerr}}//简单看一下warn的实现warn=(msg,vm)=>{const跟踪=虚拟机?generateComponentTrace(vm):''if(config.warnHandler){config.warnHandler.call(null,msg,vm,trace)}elseif(hasConsole&&(!config.silent)){//这就是我们通常做的常见Vuewarn打印错误的由来!console.error(`[Vuewarn]:${msg}${trace}`)}}看下图,如果不捕获全局错误,开发环境的错误输出是不是很眼熟?:point_down:这是一个小问题:为什么1个错误打印2个错误消息?哈哈哈~没错,其实就是logError函数的实现!!!这里再回顾一下,logError是先调用warn打印[Vuewarn]:一开始被Vue包裹的错误信息,然后通过console打印出js的错误信息。order函数编程的私有思想,通过接收一个函数参数,在内部用try-catch包装后执行传入的函数;它还提供了更好的异步错误处理,当执行函数返回一个Promise对象时,会在这里进行错误捕获,最后通知给handleError(如果我们自己没有捕获返回的Promise)globalHandleError:调用errorHandler全局配置的函数,如果在调用过程中捕获到错误,将通过logError打印捕获错误,logError以'config.errorHandler'结尾,打印出未捕获的错误信息。开发环境会打印2种错误信息~3.错误捕获过程分析看完错误捕获的源码实现,最好详细看看Vue是如何捕获错误的,加深理解。有很多方法可以捕获命中错误。这里我们以文章开头的代码案例作为命中分支进行调试,带大家看看Vue是如何实现错误捕获的~1.创建阶段的错误捕获回顾Vue的整个组件化过程(整个生命周期),如下图:created的触发阶段在init阶段,如下图:可以看出callHook方法触发了created的钩子。接下来看callHook的实现:遍历当前vm实例所有cb的当前hook,传入invokeWithErrorHandling函数invokeWithErrorHandlinghandleErrorlargeVueerrorHandlerfunctionvarhandlers=vm.$options[hook];//info信息,这里是创建的hookvarinfo=hook+"hook";if(handlers){for(vari=0,j=handlers.length;i
//js代码methods:{handleClick(){console.log('点击事件错误捕获')constobj={}obj.fn()}}打包后代码如下:因此,在整个Vue的初始化过程中,我们绑定的点击事件会被updateDOMListeners处理,然后会调用updateListeners方法。我们来看看updateListeners的核心代码是干什么的?你不需要在这里深究原因!!!知道这个过程的调用顺序就够了,贴出来也是为了让大家理解的更清楚。有兴趣的可以等作者出一篇关于Vue事件的源码分析~functionupdateListeners(){//这里的cur是我们写在methods中的handleClickcur=on[name]=createFnInvoker(cur,vm);}可以知道我们的handleClick是通过createFnInvoker在这里包装返回的,而我们的错误捕获是在包装的createFnInvoker中实现的。让我们看看createFnInvoker做了什么functioncreateFnInvoker(fns,vm){functioninvoker(){vararguments$1=arguments;//从调用者的静态属性fns中获取方法varfns=invoker.fns;if(Array.isArray(fns)){//一个新的fns数组varcloned=fns.slice();for(vari=0;i