zl程序教程

您现在的位置是:首页 >  前端

当前栏目

浅析Vue中的2种错误处理方式、源码分析及2种错误捕获流程

Vue错误流程源码 分析 方式 浅析 捕获
2023-09-11 14:19:54 时间

一、认识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()
  }
}
...

  createdhandleClick的输出结果如下

  由此可见:

  • 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 钩子,但是在 HelloWorld  组件报错时,该钩子里的内容并没有打印

3、Vue错误捕获机制图

二、Vue 错误捕获源码

  源码分析的 Vue 版本是 v2.6.14,代码位于src/core/util/error.js。共四个方法:handleErrorinvokeWithErrorHandlingglobalHandleErrorlogError,接下来,我们一个一个的来认识他们~

 1、handleError:Vue 中的错误统一处理函数

  在此函数中实现向上通知 errorCaptured 直到全局 errorHandler 的功能。源码如下:

// 很明显,这个参数的就是我们熟悉的 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()
  }
}

  核心解读如下:

  • 参数 errvminfo
  • pushTargetpopTarget。源码中注释有写到,主要是避免处理错误时 组件 无限渲染
  • $parent。Vue 组件树中建立父子关系的属性,可以通过该属性不断向上查找顶层组件——大Vue(也就是我们初始化时候new Vue的那个),大Vue $parent 是 undefined
  • 获取errorCaptured。可能有小伙伴有疑问这里为什么是个数组,因为 Vue 初始化的时候会对 hook 做合并处理。比如说我们用到 mixins 的时候,组件中可能会出现多个相同的 hook,初始化时会把这些 cb 都合并在一个 hook 的数组里,以便触发钩子的时候一一调用
  • capture。如果为false的时候,直接 return,不会走到 globalHandleError 中

2、invokeWithErrorHandling:一个包装函数,内部使用 try-catch 包裹传入的函数,且有更好的处理异步错误的能力。

  可处理 生命周期 、 事件 等回调函数的错误捕获。可处理返回值是Promise的异步错误捕获。捕获到错误后,统一派发给 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 条报错信息?

  其实就是 logError 函数的实现,这里再回顾一下:logError 先是调用 warn 打印 [Vue warn]: 开头的 Vue 包装过的错误提示信息,再通过 console.error 打印 js 的错误信息。

5、总结:源码 4 个函数

  handleError:统一的错误捕获函数。实现子组件到顶层组件错误捕获后,对 errorCaptured hook 的冒泡调用,执行完全部的 errorCaptured 钩子后,最终执行全局错误捕获函数 globalHandleError。

  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内会调用 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初始化的过程中,会对我们绑定的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 实现向上冒泡执行上层组件的错误钩子,直至全局的错误捕获 这也是我们 点击事件 的错误捕获流程了~

  学习文章:https://mp.weixin.qq.com/s/hgD3nOwLAh2fiEO2HWcqIA

  感觉还是不错的,个人总结下:

1、2 个错误处理方式:全局配置 errorHandler、捕获后代组件的错误钩子 errorCaptured

2、源码 4 个方法:

  handleError 统一的错误捕获函数,实现错误冒泡

  invokeWithErrorHandling:包装函数,使用高阶函数思想提供更好的错误处理(包括异步函数)

  globalErrorHandler:调用全局配置的 errorHandler 函数

  logError:实现对未捕获错误进行打印(这里就要搞清楚为什么开发环境报错会打印2次)

2、2 种错误捕获流程

  created、mounted等钩子函数阶段错误捕获:主要是 callhook 函数,会遍历所有hook的cb,并传入invokeWithErrorHandling处理

  click等绑定事件的错误捕获:在Vue初始化过程中,会对绑定的事件进行 updateDOMListeners 处理,然后会调用到 updateListeners 方法,该方法通过 createFnInvoker 对绑定的事件进行了一层 invokeWithErrorHandling 包装后再返回