编辑推荐: |
错误捕获这个东西,不管是在框架层面,还是我们日常开发业务中都是比较重要的,但往往又被很多人忽略。本文主要讲解了Vue错误处理机制,希望对您的学习有所帮助。
本文来自于51CTO,由火龙果软件Linda编辑、推荐。 |
|
一、认识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 的流程分析):
(2)handleClick 的输出结果如下(文章结尾会以此进行 catch 的流程分析)
由此可见:
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 。源码中注释有写到,主要是避免处理错误时 组件 无限渲染
$parent 大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}`)
}
}
|
看看下图,如果我们不进行全局错误捕获,在开发环境的报错输出是否有点似曾相识呢?:point_down:
这里提个小问题:为什么 1 个错误打印 2 条报错信息?
哈哈哈~没错,其实就是 logError 函数的实现!!!这里再回顾一下, logError 先是调用
warn 打印 [Vue warn]: 开头的 Vue 包装过的错误提示信息,再通过 console.error
打印js的错误信息
简单总结一下:
handleErrorerrorCaptured errorCaptured
invokeWithErrorHandling :包装函数,通过 高阶函数 的编程私思路,通过接收一个函数参数,并在内部使用
try-catch 包裹后 执行 传入的函数;还提供更好的 异步错误处理 ,当执行函数返回了一个Promise对象,会在此对其实现进行错误捕获,最后也是通知到
handleError 中(如果我们未自己对返回的Promise进行catch操作)
globalHandleError :调用全局配置的 errorHandler 函数,如果在调用的过程中捕获到错误,则通过
logError 打印所捕获的错误,以 'config.errorHandler' 结尾
logError 。实现对未捕获的错误信息进行打印输出。开发环境会打印 2种 错误信息~
三、错误捕获流程分析
看完了错误捕获的源码实现,不如具体看看 Vue是怎么捕获到错误的 ,以此来加深下理解。命中错误捕获的方式有很多,这里以
文章开头的代码案例 作为命中分支进行调试,带你看看Vue是怎么实现 错误捕获 的~
1. created 阶段的错误捕获
温习一下 Vue 的整个组件化流程(整个生命周期)做了什么,如下图:
created的触发阶段是在init阶段,如下图:
由此可见,触发created钩子的是 callHook 方法,接下来看下 callHook 的实现:
遍历当前 vm 实例的 当前 hook 的所有 cb,并将其传入 invokeWithErrorHandling
函数中
invokeWithErrorHandling handleError 大Vue 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 去调用 invokeWithErrorHandling ,并且传入保存在 invoker
的 静态属性 fns 中的函数(也就是我们用户编写的 handleClick 函数)
如此一来,就跟 二、Vue错误捕获源码 中的 2. invokeWithErrorHandling
的执行一样了
最终会通过 handleError 实现向上冒泡执行上层组件的错误钩子,直至全局的错误捕获 这也是我们
点击事件 的错误捕获流程了~
写在最后,怎么样,是不是非常的简单呢? 错误捕获 这个东西,不管是在框架层面,还是我们日常开发业务中都是比较重要的,但往往又被很多人忽略(比如我)。总览下来,其实这一块也不难,在
Vue 源码的实现中,大家只要看过都能懂。 |