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 => {
// ...
}
}
上面的代码是简化过的,可以发现,在非生产环境下分别对 warn
、tip
、formatComponentName
以及 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 查看 IE2
到 IE11
所有版本的用户代理字符串,我们能够发现在这些 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
为原生提供的函数,在其他浏览器中nativeWatch
为undefined
。这个变量主要用于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
常量是一个布尔值,用来判断当前宿主环境是否支持原生Symbol
和Reflect.ownKeys
的可用性。源码解析:
首先判断 Symbol
和 Reflect
是否存在,并使用 isNative
函数保证 Symbol
与 Reflect.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
,并且在 ChildComponent
的 beforeCreate
钩子中写了如下代码:
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
,然后与 0x24
和 0x5F
作比较。其中 $
对应的 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
parsePath
函数的源码在 初始 Watcher 一节中讲解
options.js 文件代码说明
perf.js 文件代码说明
这个文件导出两个变量,分别是 mark
和 measure
:
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
的接口可用,如果都可用,那么将初始化 mark
和 measure
变量。
首先看 mark
:
mark = tag => perf.mark(tag)
实际上,mark
是一个函数,这个函数的作用就是使用给定的 tag
,通过 performance.mark()
方法打一个标记。
measure
方法接收三个参数,这三个参数与 performance.measure()
方法所要求的参数相同,它的作用就是调用一下 performance.measure()
方法,然后调用三个清除标记的方法:
perf.clearMarks(startTag)
perf.clearMarks(endTag)
perf.clearMeasures(name)
可以发现,其实 mark
和 measure
这两个函数就是对 performance.mark()
和 performance.measure()
的封装。对于 performance.mark()
和 performance.measure()
这两个方法的详情,大家可以查看 Performance,这里我将用一个通俗的说法尽快让大家明白 mark
和 measure
的作用,首先 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')