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 文件代码说明

inBrowser

源码如下:

export const inBrowser = typeof window !== 'undefined'
  • 描述:检测当前宿主环境是否是浏览器

  • 源码解析:通过判断 window 对象是否存在即可

UA

源码如下:

export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
  • 描述:获取当浏览器的 user Agent,简称 UA

  • 源码解析:首先使用 inBrowser 检测当前宿主环境是否是浏览器,如果是则通过 window.navigator.userAgent.toLowerCase() 获取当前浏览器的 UA 字符串,并将该字符串小写化之后赋值给 UA 常量。

isIE

源码如下:

export const isIE = UA && /msie|trident/.test(UA)
  • 描述:判断当前浏览器是否是 Internet Explorer 浏览器。

  • 源码解析:

大家可以访问这里 Internet Explorer User Agent Strings 查看 IE2IE11 所有版本的用户代理字符串,我们能够发现在这些 UA 字符串中必然包含 'trident' 或者 'msie' 这两个字符串。所以只需要使用正则去匹配 UA 中是否包含这两个字符串即可判断是否为 IE 浏览器。

hasProto

源码如下:

// can we use __proto__?
export const hasProto = '__proto__' in {}
  • 描述:hasProto 用来检查当前环境是否可以使用对象的 __proto__ 属性。我们知道,一个对象的 __proto__ 属性指向了它构造函数的原型,但这是一个在 ES2015 中才被标准化的属性,IE11 及更高版本才能够使用。

  • 源码解析:

判断当前环境是否可以使用 __proto__ 属性很简单,正如源码所示那样,使用 in 运算符从一个空的对象字面量开始沿着原型链逐级检查,看其是否存在即可。

nativeWatch

源码如下:

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

isServerRendering

源码如下:

// this needs to be lazy-evaled because vue may be required before
// vue-server-renderer can set VUE_ENV
let _isServer
export const isServerRendering = () => {
  if (_isServer === undefined) {
    /* istanbul ignore if */
    if (!inBrowser && !inWeex && typeof global !== 'undefined') {
      // detect presence of vue-server-renderer and avoid
      // Webpack shimming the process
      _isServer = global['process'].env.VUE_ENV === 'server'
    } else {
      _isServer = false
    }
  }
  return _isServer
}
  • 描述:isServerRendering 函数的执行结果是一个布尔值,用来判断是否是服务端渲染。

  • 源码解析:

根据 if 语句:

if (!inBrowser && !inWeex && typeof global !== 'undefined') {...}

可知如果不在浏览器中(!inBrowser)也不是weex(!inWeex),同时 global 有定义,则可能是服务端渲染,那么继续判断:

global['process'].env.VUE_ENV === 'server'

是否成立,其中 global['process'.env.VUE_ENV]vue-server-renderer 注入的。如果成立那么说明是服务端渲染。如果上面的条件有一项不成立,那么都不认为是服务端渲染。

注意,在 isServerRendering 中使用全局变量 _isServer 保存了最终的值,如果发现 _isServer 有定义,那么就不会重新计算,从而提升性能。毕竟环境是不会改变的,只需要求值一次即可。

hasSymbol

源码如下:

export const hasSymbol =
  typeof Symbol !== 'undefined' && isNative(Symbol) &&
  typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys)
  • 描述:hasSymbol 常量是一个布尔值,用来判断当前宿主环境是否支持原生 SymbolReflect.ownKeys 的可用性。

  • 源码解析:

首先判断 SymbolReflect 是否存在,并使用 isNative 函数保证 SymbolReflect.ownKeys 全部是原生定义。

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 || inWeex) && typeof console !== 'undefined') {
    console.error(err)
  } else {
    throw err
  }
}

可以看到,在非生产环境下,先使用 warn 函数报一个警告,然后判断是否在浏览器或者Weex环境且 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 的实现。实际上我们可以这样写代码:

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 也会被调用。最重要的是:如果 errorCaptured 钩子函数返回假将阻止错误继续向“上级”传递。

lang.js 文件代码说明

isReserved

  • 源码如下:
/**
 * Check if a string starts with $ or _
 */
export function isReserved (str: string): boolean {
  const c = (str + '').charCodeAt(0)
  return c === 0x24 || c === 0x5F
}
  • 描述:isReserved 函数用来检测一个字符串是否以 $ 或者 _ 开头,主要用来判断一个字段的键名是否是保留的,比如在 Vue 中不允许使用以 $_ 开头的字符串作为 data 数据的字段名,如:
new Vue({
  data: {
    $a: 1,  // 不允许
    _b: 2   // 不允许
  }
})
  • 源码分析:

判断一个字符串是否以 $_ 开头还是比较容易的,只不过 isReserved 函数的实现方式是通过字符串的 charCodeAt 方法获得该字符串第一个字符的 unicode,然后与 0x240x5F 作比较。其中 $ 对应的 unicode 码为 36,对应的十六进制值为 0x24_ 对应的 unicode 码为 95,对应的十六进制值为 0x5F。有的同学可能会有疑问为什么不直接用字符 $_ 作比较,而是用这两个字符对应的 unicode 码作比较,其实无论哪种比较方法差别不大,看作者更倾向于哪一种。

def

  • 源码如下:
/**
 * Define a property.
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
  • 描述:def 函数是对 Object.defineProperty 函数的简单包装,为了调用方便

  • 源码分析:

def 函数接收四个参数,分别是 源对象,要在对象上定义的键名,对应的值,以及是否可枚举,如果不传递 enumerable 参数则代表定义的属性是不可枚举的。

parsePath

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 文件代码说明