core-util.md 13 KB

core/util 目录下的工具方法全解

debug.js 文件代码说明

该文件主要导出四个函数,如下:

export let warn = noop
export let tip = noop
export let generateComponentTrace = (noop: any) // work around flow check
export let formatComponentName = (noop: any)

这四个变量都被初始化为空函数·

接下来是这样一段代码:

if (process.env.NODE_ENV !== 'production') {
  // ...
}

这些代码被包含在一个环境判断的语句块内,这说明,这些代码只有在非生产环境才会生效,而在这些代码中我们能看到如下语句:

if (process.env.NODE_ENV !== 'production') {
  // 其他代码...

  warn = (msg, vm) => {
    // ...
  }

  tip = (msg, vm) => {
    // ...
  }

  formatComponentName = (vm, includeFile) => {
    // ...
  }

  generateComponentTrace = vm => {
    // ...
  }
}

上面的代码是简化过的,可以发现,在非生产环境下分别对 warntipformatComponentName 以及 generateComponentTrace 进行了赋值,且值都为函数,接下来我们分别看一下这四个函数的作用,不过在这之前,我们需要介绍三个变量,也就是 if 语句块最开始的三个变量:

const hasConsole = typeof console !== 'undefined'
const classifyRE = /(?:^|[-_])(\w)/g
const classify = str => str
  .replace(classifyRE, c => c.toUpperCase())
  .replace(/[-_]/g, '')

其中 hasConsole 用来检测宿主环境的 console 是否可用,classifyRE 是一个正则表达式:/(?:^|[-_])(\w)/g,用于 classify 函数,classify 函数的作用是将一个字符串的首字母以及中横线转为驼峰的,代码很简单相信大家都能看得懂,classify 的使用如下:

console.log(classify('aaa-bbb-ccc')) // AaaBbbCcc
warn
tip
formatComponentName

env.js 文件代码说明

nativeWatch

源码如下:

// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch
  • 描述:在 Firefox 中原生提供了 Object.prototype.watch 函数,所以当运行在 Firefox 中时 nativeWatch 为原生提供的函数,在其他浏览器中 nativeWatchundefined。这个变量主要用于 Vue 处理 watch 选项时与其冲突。

error.js 文件代码说明

该文件只导出一个函数:handleError,在看这个函数的实现之前,我们需要回顾一下 Vue 的文档,我们知道 Vue 提供了一个全局配置 errorHandler,用来捕获组件生命周期函数等的内部错误,使用方法如下:

Vue.config.errorHandler = function (err, vm, info) {
  // ...
}

我们通过设置 Vue.config.errorHandler 为一个函数,实现对特定错误的捕获。具体使用可以查看官方文档。而接下来要讲的 handleError 函数就是用来实现 Vue.config.errorHandler 这一配置功能的,我们看看是怎么做的。

handleError

源码如下:

export function handleError (err: Error, vm: any, info: string) {
  if (vm) {
    let cur = vm
    while ((cur = cur.$parent)) {
      const hooks = cur.$options.errorCaptured
      if (hooks) {
        for (let i = 0; i < hooks.length; i++) {
          try {
            const capture = hooks[i].call(cur, err, vm, info) === false
            if (capture) return
          } catch (e) {
            globalHandleError(e, cur, 'errorCaptured hook')
          }
        }
      }
    }
  }
  globalHandleError(err, vm, info)
}
  • 描述:用于错误处理

  • 参数:

    • {Error} err catch 到的错误对象,我们可以看到 Vue 源码中是这样使用的:

      try {
      ...
      } catch (e) {
      handleError(e, vm, `${hook} hook`)
      }
      
    • {any} vm 这里应该传递 Vue 实例

    • {String} info Vue 特定的错误提示信息

  • 源码分析

首先迎合一下使用场景,在 Vue 的源码中 handleError 函数的使用一般如下:

try {
  handlers[i].call(vm)
} catch (e) {
  handleError(e, vm, `${hook} hook`)
}

上面是声明周期钩子回调执行时的代码,由于声明周期钩子是开发者自定义的函数,这个函数的执行是很可能存在运行时错误的,所以这里需要 try catch 包裹,且在发生错误的时候,在 catch 语句块中捕获错误,然后使用 handleError 进行错误处理。知道了这些,我们再看看 handleError 到底怎么处理的,源码上面已经贴出来了,首先是一个 if 判断:

if (vm) {
  let cur = vm
  while ((cur = cur.$parent)) {
    if (cur.$options.errorCaptured) {
      try {
        const propagate = cur.$options.errorCaptured.call(cur, err, vm, info)
        if (!propagate) return
      } catch (e) {
        globalHandleError(e, cur, 'errorCaptured hook')
      }
    }
  }
}

那这段代码是干嘛的呢?我们先不管,回头来说。在判断语句后面直接调用了 globalHandleError 函数,且将三个参数透传了过去:

globalHandleError(err, vm, info)

globalHandleError 函数就定义在 handleError 函数的下面,源码如下:

function globalHandleError (err, vm, info) {
  if (config.errorHandler) {
    try {
      return config.errorHandler.call(null, err, vm, info)
    } catch (e) {
      logError(e, null, 'config.errorHandler')
    }
  }
  logError(err, vm, info)
}

globalHandleError 函数首先判断 config.errorHandler 是否为真,如果为真则调用 config.errorHandler 并将参数透传,这里的 config.errorHandler 就是 Vue 全局API提供的用于自定义错误处理的配置我们前面讲过。由于这个错误处理函数也是开发者自定义的,所以可能出现运行时错误,这个时候就需要使用 try catch 语句块包裹起来,当错误发生时,使用 logError 函数打印错误,当然啦,如果你们有配置 config.errorHandler 也就是说 config.errorHandler 此时为假,那么将使用默认的错误处理函数,也就是 logError 进行错误处理。

所以 globalHandleError 是用来检测你是否自定义了 config.errorHandler 的,如果有则用之,如果没有就是用 logError

那么 logError 是什么呢?这个函数定义在 globalHandleError 函数的下面,源码如下:

function logError (err, vm, info) {
  if (process.env.NODE_ENV !== 'production') {
    warn(`Error in ${info}: "${err.toString()}"`, vm)
  }
  /* istanbul ignore else */
  if (inBrowser && typeof console !== 'undefined') {
    console.error(err)
  } else {
    throw err
  }
}

可以看到,在非生产环境下,先使用 warn 函数报一个警告,然后判断是否在浏览器环境且 console 是否可用,如果可用则使用 console.error 打印错误,没有则直接 throw err

所以 logError 才真正打印错误的函数,且实现也比较简单。这其实已经达到了 handleError 的目的了,但是大家注意我们此时忽略了一段代码,就是 handleError 函数开头的一段代码:

if (vm) {
  let cur = vm
  while ((cur = cur.$parent)) {
    const hooks = cur.$options.errorCaptured
    if (hooks) {
      for (let i = 0; i < hooks.length; i++) {
        try {
          const capture = hooks[i].call(cur, err, vm, info) === false
          if (capture) return
        } catch (e) {
          globalHandleError(e, cur, 'errorCaptured hook')
        }
      }
    }
  }
}

那么这个 if 判断是干嘛的呢?这其实是在支持新的 Vue 选项 errorCaptured。在编写该文章的时候 Vue 的文档还没有跟新,实际上你可以这样写代码:

var vm = new Vue({
  errorCaptured: function (err, vm, info) {
    console.log(err)
    console.log(vm)
    console.log(info)
  }
})

errorCaptured 选项可以用来捕获子代组件的错误,当子组件有错误被 handleError 函数处理时,父组件可以通过该选项捕获错误。这个选项与生命周期钩子并列。

举一个例子,如下代码:

var ChildComponent = {
  template: '<div>child component</div>',
  beforeCreate: function () {
    JSON.parse("};")
  }
}

var vm = new Vue({
  components: {
    ChildComponent
  },
  errorCaptured: function (err, vm, info) {
    console.log(err)
    console.log(vm)
    console.log(info)
  }
})

上面的代码中,首先我们定义了一个子组件 ChildComponent,并且在 ChildComponentbeforeCreate 钩子中写了如下代码:

JSON.parse("};")

这明显会报错嘛,然后我们在父组件中使用了 errorCaptured 选项,这样是可以捕获到错误的。

接下来我们就看看 Vue 是怎么实现的,原理就在这段代码中:

if (vm) {
  let cur = vm
  while ((cur = cur.$parent)) {
    const hooks = cur.$options.errorCaptured
    if (hooks) {
      for (let i = 0; i < hooks.length; i++) {
        try {
          const capture = hooks[i].call(cur, err, vm, info) === false
          if (capture) return
        } catch (e) {
          globalHandleError(e, cur, 'errorCaptured hook')
        }
      }
    }
  }
}

首先看这个 while 循环:

while ((cur = cur.$parent))

这是一个链表遍历嘛,逐层寻找父级组件,如果父级组件使用了 errorCaptured 选项,则调用之,就怎么简单。当然啦,作为生命周期钩子,errorCaptured 选项在内部时以一个数组的形式存在的,所以需要 for 循环遍历,另外钩子执行的语句是被包裹在 try catch 语句块中的。

这里有两点需要注意:

  • 第一、既然是逐层寻找父级,那意味着,如果一个子组件报错,那么其使用了 errorCaptured 的所有父代组件都可以捕获得到。
  • 第二、注意这句话:

    if (capture) return
    

其中 capture 是钩子调用的返回值与 false 的全等比较的结果,也就是说,如果 errorCaptured 钩子函数返回假,那么 capture 为真直接 return,程序不会走 if 语句块后面的 globalHandleError,否则除了 errorCaptured 被调用外,if 语句块后面的 globalHandleError 也会被调用。

lang.js 文件代码说明

emptyObject
  • 源码如下:

    export const emptyObject = Object.freeze({})
    

options.js 文件代码说明

perf.js 文件代码说明

这个文件导出两个变量,分别是 markmeasure

export let mark
export let measure

接着是下面这段代码:

if (process.env.NODE_ENV !== 'production') {
  const perf = inBrowser && window.performance
  /* istanbul ignore if */
  if (
    perf &&
    perf.mark &&
    perf.measure &&
    perf.clearMarks &&
    perf.clearMeasures
  ) {
    mark = tag => perf.mark(tag)
    measure = (name, startTag, endTag) => {
      perf.measure(name, startTag, endTag)
      perf.clearMarks(startTag)
      perf.clearMarks(endTag)
      perf.clearMeasures(name)
    }
  }
}

首先判断环境,如果不是生产环境,则继续,否则什么都不做。也就是说,如果是生产环境,那么这个文件导出的两个变量,都是 undefined

我们看一下,如果不是生产环境,又做了些什么,首先定义一个变量 perf

const perf = inBrowser && window.performance

如果在浏览器环境,那么 perf 的值就是 window.performance,否则为 false,然后做了一些列判断,目的是确定 performance 的接口可用,如果都可用,那么将初始化 markmeasure 变量。

首先看 mark

mark = tag => perf.mark(tag)

实际上,mark 是一个函数,这个函数的作用就是使用给定的 tag,通过 performance.mark() 方法打一个标记。

measure 方法接收三个参数,这三个参数与 performance.measure() 方法所要求的参数相同,它的作用就是调用一下 performance.measure() 方法,然后调用三个清除标记的方法:

perf.clearMarks(startTag)
perf.clearMarks(endTag)
perf.clearMeasures(name)

可以发现,其实 markmeasure 这两个函数就是对 performance.mark()performance.measure() 的封装。对于 performance.mark()performance.measure() 这两个方法的详情,大家可以查看 Performance,这里我将用一个通俗的说法尽快让大家明白 markmeasure 的作用,首先 mark 可以理解为“打标记”,比如如下代码我们在 for 循环的前后各打一个标记:

mark('for-start')
for (let i = 0; i < 100; i++) {
    console.log(i)
}
mark('for-end')

但是仅仅打标记是没有什么用的,这个时候就需要 measure 方法,它能够根据两个标记来计算这两个标记间代码的性能数据,你只需要这样即可:

measure('for-measure', 'for-start', 'for-end')

props.js 文件代码说明

next-tick.js 文件代码说明