# core/util 目录下的工具方法全解 ## debug.js 文件代码说明 该文件主要导出四个函数,如下: ```js export let warn = noop export let tip = noop export let generateComponentTrace = (noop: any) // work around flow check export let formatComponentName = (noop: any) ``` 这四个变量都被初始化为空函数· 接下来是这样一段代码: ```js if (process.env.NODE_ENV !== 'production') { // ... } ``` 这些代码被包含在一个环境判断的语句块内,这说明,这些代码只有在非生产环境才会生效,而在这些代码中我们能看到如下语句: ```js if (process.env.NODE_ENV !== 'production') { // 其他代码... warn = (msg, vm) => { // ... } tip = (msg, vm) => { // ... } formatComponentName = (vm, includeFile) => { // ... } generateComponentTrace = vm => { // ... } } ``` 上面的代码是简化过的,可以发现,在非生产环境下分别对 `warn`、`tip`、`formatComponentName` 以及 `generateComponentTrace` 进行了赋值,且值都为函数,接下来我们分别看一下这四个函数的作用,不过在这之前,我们需要介绍三个变量,也就是 `if` 语句块最开始的三个变量: ```js 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` 的使用如下: ```js console.log(classify('aaa-bbb-ccc')) // AaaBbbCcc ``` ### warn ### tip ### formatComponentName ## env.js 文件代码说明 ### inBrowser 源码如下: ```js export const inBrowser = typeof window !== 'undefined' ``` * 描述:检测当前宿主环境是否是浏览器 * 源码解析:通过判断 `window` 对象是否存在即可 ### UA 源码如下: ```js export const UA = inBrowser && window.navigator.userAgent.toLowerCase() ``` * 描述:获取当浏览器的 `user Agent`,简称 `UA`。 * 源码解析:首先使用 `inBrowser` 检测当前宿主环境是否是浏览器,如果是则通过 `window.navigator.userAgent.toLowerCase()` 获取当前浏览器的 `UA` 字符串,并将该字符串小写化之后赋值给 `UA` 常量。 ### isIE 源码如下: ```js export const isIE = UA && /msie|trident/.test(UA) ``` * 描述:判断当前浏览器是否是 `Internet Explorer` 浏览器。 * 源码解析: 大家可以访问这里 [Internet Explorer User Agent Strings](http://useragentstring.com/pages/useragentstring.php?name=Internet+Explorer) 查看 `IE2` 到 `IE11` 所有版本的用户代理字符串,我们能够发现在这些 `UA` 字符串中必然包含 `'trident'` 或者 `'msie'` 这两个字符串。所以只需要使用正则去匹配 `UA` 中是否包含这两个字符串即可判断是否为 `IE` 浏览器。 ### hasProto 源码如下: ```js // can we use __proto__? export const hasProto = '__proto__' in {} ``` * 描述:`hasProto` 用来检查当前环境是否可以使用对象的 `__proto__` 属性。我们知道,一个对象的 `__proto__` 属性指向了它构造函数的原型,但这是一个在 `ES2015` 中才被标准化的属性,`IE11` 及更高版本才能够使用。 * 源码解析: 判断当前环境是否可以使用 `__proto__` 属性很简单,正如源码所示那样,使用 `in` 运算符从一个空的对象字面量开始沿着原型链逐级检查,看其是否存在即可。 ### nativeWatch 源码如下: ```js // Firefox has a "watch" function on Object.prototype... export const nativeWatch = ({}).watch ``` * 描述:在 `Firefox` 中原生提供了 `Object.prototype.watch` 函数,所以当运行在 `Firefox` 中时 `nativeWatch` 为原生提供的函数,在其他浏览器中 `nativeWatch` 为 `undefined`。这个变量主要用于 `Vue` 处理 `watch` 选项时与其冲突。 ### isServerRendering 源码如下: ```js // 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` 语句: ```js if (!inBrowser && !inWeex && typeof global !== 'undefined') {...} ``` 可知如果不在浏览器中(`!inBrowser`)也不是weex(`!inWeex`),同时 `global` 有定义,则可能是服务端渲染,那么继续判断: ```js global['process'].env.VUE_ENV === 'server' ``` 是否成立,其中 `global['process'.env.VUE_ENV]` 是 `vue-server-renderer` 注入的。如果成立那么说明是服务端渲染。如果上面的条件有一项不成立,那么都不认为是服务端渲染。 注意,在 `isServerRendering` 中使用全局变量 `_isServer` 保存了最终的值,如果发现 `_isServer` 有定义,那么就不会重新计算,从而提升性能。毕竟环境是不会改变的,只需要求值一次即可。 ### hasSymbol 源码如下: ```js 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`,用来捕获组件生命周期函数等的内部错误,使用方法如下: ```js Vue.config.errorHandler = function (err, vm, info) { // ... } ``` 我们通过设置 `Vue.config.errorHandler` 为一个函数,实现对特定错误的捕获。具体使用可以查看官方文档。而接下来要讲的 `handleError` 函数就是用来实现 `Vue.config.errorHandler` 这一配置功能的,我们看看是怎么做的。 ### handleError 源码如下: ```js 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 源码中是这样使用的: ```js try { ... } catch (e) { handleError(e, vm, `${hook} hook`) } ``` * `{any} vm` 这里应该传递 `Vue` 实例 * `{String} info` `Vue` 特定的错误提示信息 * 源码分析 首先迎合一下使用场景,在 `Vue` 的源码中 `handleError` 函数的使用一般如下: ```js try { handlers[i].call(vm) } catch (e) { handleError(e, vm, `${hook} hook`) } ``` 上面是生命周期钩子回调执行时的代码,由于生命周期钩子是开发者自定义的函数,这个函数的执行是很可能存在运行时错误的,所以这里需要 `try catch` 包裹,且在发生错误的时候,在 `catch` 语句块中捕获错误,然后使用 `handleError` 进行错误处理。知道了这些,我们再看看 `handleError` 到底怎么处理的,源码上面已经贴出来了,首先是一个 `if` 判断: ```js 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` 函数,且将三个参数透传了过去: ```js globalHandleError(err, vm, info) ``` `globalHandleError` 函数就定义在 `handleError` 函数的下面,源码如下: ```js 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` 函数的下面,源码如下: ```js 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` 函数开头的一段代码: ```js 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` 的实现。实际上我们可以这样写代码: ```js var vm = new Vue({ errorCaptured: function (err, vm, info) { console.log(err) console.log(vm) console.log(info) } }) ``` `errorCaptured` 选项可以用来捕获子代组件的错误,当子组件有错误被 `handleError` 函数处理时,父组件可以通过该选项捕获错误。这个选项与生命周期钩子并列。 举一个例子,如下代码: ```js var ChildComponent = { template: '