深入源码,剖析 Vue3 是如何做错误处理的

错误处理

错误处理是框架设计的核心要素之一。框架的错误处理好坏,直接决定用户应用程序的健壮性以及用户开发应用时处理错误的心智负担。同时,Vue 作为一个基础地前端框架,也提供了统一地全局错误处理接口,用户可以通过该接口(errorhandler)注册自定义的错误处理程序来全局地处理框架产生的所有异常。

Vue3 中提供了两种处理异常的函数,同步异常处理函数(callWithErrorHandling)和异步异常处理函数(callWithAsyncErrorHandling)。异步异常处理函数是在同步异常处理函数的基础上实现的。

// packages/runtime-core/src/errorHandling.ts

export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
) {
  let res
  try {
    res = args ? fn(...args) : fn()
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}

本篇文章的源码均摘自 Vue.js 3.2.45

从整体上来看,callWithErrorHandling 函数的实现是比较简单的,就是对 try catch 的封装。catch 捕获到的异常统一由 handleError 函数来处理。

// packages/runtime-core/src/errorHandling.ts

export function handleError(
  err: unknown,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  throwInDev = true
) {
  // 省略其他代码
  logError(err, type, contextVNode, throwInDev)
}
function logError(
  err: unknown,
  type: ErrorTypes,
  contextVNode: VNode | null,
  throwInDev = true
) {
  if (__DEV__) {
    // 省略其他代码
  } else {
    // recover in prod to reduce the impact on end-user
    console.error(err)
  }
}

在 handleError 中会将捕获到的异常输出来。

// packages/runtime-core/src/errorHandling.ts

export function callWithAsyncErrorHandling(
  fn: Function | Function[],
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
): any[] {
  if (isFunction(fn)) {
    const res = callWithErrorHandling(fn, instance, type, args)
    if (res && isPromise(res)) {
      res.catch(err => {
        handleError(err, instance, type)
      })
    }
    return res
  }

  const values = []
  for (let i = 0; i < fn.length; i++) {
    values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
  }
  return values
}

异步异常处理函数(callWithAsyncErrorHandling),是基于同步异常处理函数(callWithErrorHandling)实现的,如果传入的参数是函数数组(Function[]),则会遍历这个函数数组,递归调用异步异常处理函数(callWithAsyncErrorHandling),将返回的结果保留到数组中(values),最后返回这个数组。

在异步异常处理函数中(callWithAsyncErrorHandling) ,它通过判断传入函数的返回值(res)是否为对象,并且是否有 then 、catch 回调来判断返回值(res)是否为 Promise 实例。这种判断是否为 Promise 实例的方式在我们平时的开发中也可以借鉴一下。

// packages/shared/src/index.ts

export const isFunction = (val: unknown): val is Function =>
  typeof val === 'function'

export const isObject = (val: unknown): val is Record<any, any> =>
  val !== null && typeof val === 'object'

// 判断是否为 Promise 实例
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
  return isObject(val) && isFunction(val.then) && isFunction(val.catch)
}

Vue3 还提供了 API (errorHandler)用于给用户注册捕获错误的全局处理函数,使用方法如下:

app.config.errorHandler = (err, instance, info) => {
  // 处理错误,例如:报告给一个服务
}

该 API (errorHandler)在 handleError 函数中实现

// packages/runtime-core/src/errorHandling.ts

export function handleError(
  err: unknown,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  throwInDev = true
) {
  const contextVNode = instance ? instance.vnode : null
  // 省略其他代码
  if (instance) {
    // 读取用户配置的全局错误处理函数
    const appErrorHandler = instance.appContext.config.errorHandler
    if (appErrorHandler) {
      // 如果存在用户配置的全局错误处理函数则放入 callWithErrorHandling 中执行
      callWithErrorHandling(
        appErrorHandler,
        null,
        ErrorCodes.APP_ERROR_HANDLER,
        [err, exposedInstance, errorInfo]
      )
      return
    }
  }
  // 省略其他代码
}

Vue3 会判断用户是否配置了全局错误处理函数,如果配置了则会丢给内置的同步异常处理函数执行(callWithErrorHandling)。由于用户配置的全局错误处理函数执行是给同步异常处理函数执行的,因此,用户在自定义全局错误处理函数时,要注意兼容异步错误的情况,即最好在自定义全局处理函数中,加上对异步错误代码的处理,因为 Vue3 内部没法捕获到异步的错误。

如果要做前端的异常监控,我们完全可以借助 errorHandler 函数,完成前端异常的上报。

Vue3 还提供了在捕获了后代组件传递的错误时调用的生命周期钩子(onErrorCaptured()):

function onErrorCaptured(callback: ErrorCapturedHook): void

type ErrorCapturedHook = (
  err: unknown,
  instance: ComponentPublicInstance | null,
  info: string
) => boolean | void

该生命周期钩子(onErrorCaptured()) 也是在 handleError 函数实现

// packages/runtime-core/src/errorHandling.ts

export function handleError(
  err: unknown,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  throwInDev = true
) {
  const contextVNode = instance ? instance.vnode : null
  if (instance) {
    // 1. 获取父组件实例
    let cur = instance.parent
    const exposedInstance = instance.proxy
    const errorInfo = __DEV__ ? ErrorTypeStrings[type] : type
    // 2. 开启 while 循环,不断向上遍历,取得父组件实例
    while (cur) {
      // 3. 从父组件实例中获取 onErrorCaptured 生命周期钩子
      const errorCapturedHooks = cur.ec
      if (errorCapturedHooks) {
        // 4. 遍历 onErrorCaptured 生命周期钩子数组
        for (let i = 0; i < errorCapturedHooks.length; i++) {
          if (
            // 5. 执行 onErrorCaptured 生命周期钩子,
            // onErrorCaptured 返回 false 则退出 while 循环,
            // 错误会停止向上传递
            errorCapturedHooks[i](err, exposedInstance, errorInfo) === false
          ) {
            return
          }
        }
      }
      cur = cur.parent
    }
  }
}

由于 Vue3 的组件可以多次注册同一个生命周期钩子,所以 Vue3 内部使用了数组来存储 onErrorCaptured 生命周期钩子。Vue3 内部会使用 while 循环,不断向上遍历取得父组件实例的 onErrorCaptured 生命周期钩子,然后遍历执行 onErrorCaptured 生命周期钩子,如果 onErrorCaptured 生命周期钩子返回 false ,则会退出 while 循环,停止向上传递错误。

错误码与错误信息

前端在与后端进行接口联调的时候,肯定见过各种 HTTP 的状态码,比如 404(无法找到资源)、500(服务器内部错误)等。状态码的好处是可以让用户快速地知道当前 HTTP 请求的状态,方便用户排查错误等。

因此,在 Vue 中,也定义了各种错误码(状态码),用来区分各种类型的错误,方便开发者(Vue 用户)排查错误。

// packages/runtime-core/src/errorHandling.ts

export const enum ErrorCodes {
  SETUP_FUNCTION,
  RENDER_FUNCTION,
  WATCH_GETTER,
  WATCH_CALLBACK,
  WATCH_CLEANUP,
  NATIVE_EVENT_HANDLER,
  COMPONENT_EVENT_HANDLER,
  VNODE_HOOK,
  DIRECTIVE_HOOK,
  TRANSITION_HOOK,
  APP_ERROR_HANDLER,
  APP_WARN_HANDLER,
  FUNCTION_REF,
  ASYNC_COMPONENT_LOADER,
  SCHEDULER
}

// 不同的状态码对应不同的错误信息
export const ErrorTypeStrings: Record<number | string, string> = {
  // 省略其他代码
  [ErrorCodes.SETUP_FUNCTION]: 'setup function',
  [ErrorCodes.RENDER_FUNCTION]: 'render function',
  [ErrorCodes.WATCH_GETTER]: 'watcher getter',
  [ErrorCodes.WATCH_CALLBACK]: 'watcher callback',
  [ErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function',
  [ErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler',
  [ErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler',
  [ErrorCodes.VNODE_HOOK]: 'vnode hook',
  [ErrorCodes.DIRECTIVE_HOOK]: 'directive hook',
  [ErrorCodes.TRANSITION_HOOK]: 'transition hook',
  [ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler',
  [ErrorCodes.APP_WARN_HANDLER]: 'app warnHandler',
  [ErrorCodes.FUNCTION_REF]: 'ref function',
  [ErrorCodes.ASYNC_COMPONENT_LOADER]: 'async component loader',
  [ErrorCodes.SCHEDULER]:
    'scheduler flush. This is likely a Vue internals bug. ' +
    'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/core'
}

Vue 会根据不同的错误码取不同的错误信息输出,方便用户排查错误。

// packages/runtime-core/src/errorHandling.ts

// 省略了一些无关的代码
function logError(
  err: unknown,
  type: ErrorTypes,
  contextVNode: VNode | null,
  throwInDev = true
) {
  if (__DEV__) {
    // 根据错误码取出错误信息输出
    const info = ErrorTypeStrings[type]
    warn(`Unhandled error${info ? ` during execution of ${info}` : ``}`)
  } else {
    console.error(err)
  }
}

同时为了控制生产环境框架的代码体积,利用了 Tree Shaking 机制,仅在开发环境(__DEV__)输出内部的错误信息。

总结

错误处理是框架设计的核心要素之一,Vue3 内部提供了两种处理异常的函数,同步异常处理函数(callWithErrorHanding)和异步异常处理函数(callWithErrorHanding),异步异常处理函数、给用户自定义的全局错误处理函数、捕获了子组件异常后调用的生命周期钩子(onErrorCaptured)均是基于同步异常处理函数(callWithErrorHanding)实现,而同步异常处理函数(callWithErrorHanding)本质是对 try catch 的封装。

Vue3 还提供了错误码和对应的错误信息来帮助开发者(Vue 的用户)快速地排查错误。

从错误处理到错误码和错误信息,可以看出 Vue 的错误处理做得是比较完善的 👍。

参考

  1. Vue3 如何实现全局异常处理?
  2. 《Vue.js 设计与实现》霍春阳·著

原文链接:https://juejin.cn/post/7329033936956260362 作者:云浪

(0)
上一篇 2024年1月29日 上午10:10
下一篇 2024年1月29日 上午10:20

相关推荐

发表回复

登录后才能评论