debug.js
正所谓磨刀不误砍柴工,在真正阅读源码之前先了解清楚一些变量是如何判断、纯函数的作用是十分有必要的。知道锤子、斧头、扳手、剪刀是干嘛用的,这样才不至于在初次见面的时候显得措手不及。
该文件主要暴露了四个方法warn、tip、generateComponentTrace、formatComponentName,主要代码如下
//noop是从shared/util中引入的空方法
export function noop (a?: any, b?: any, c?: any) {}
//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') {
//暂时忽略此部分
}
主要在调试中使用,生产环境下他们就仅仅是个空Function,不做任何处理,只有在开发环境下这四个变量才会被重新定义。
注:源码中以后会经常遇到
process.env.NODE_ENV !== 'production'这样的写法,这是从性能角度考虑,一些检测、警告等非必要的提示只有在非生产环境才会做处理,以此达到最优性能。
//检测当前环境是否支持 console
const hasConsole = typeof console !== 'undefined'
//正则表达式
const classifyRE = /(?:^|[-_])(\w)/g
//转换字符串格式
const classify = str => str
.replace(classifyRE, c => c.toUpperCase())
.replace(/[-_]/g, '')
前面两个相对好理解,classify的目的是使用正则classifyRE将字符串中横岗写法转换成驼峰式。例如:
classify('fishing-wang');
//输出 FisingWang
//重写打印或警告内容到客户端
warn = (msg, vm) => {
const trace = vm ? generateComponentTrace(vm) : ''
if (config.warnHandler) {
config.warnHandler.call(null, msg, vm, trace)
} else if (hasConsole && (!config.silent)) {
console.error(`[Vue warn]: ${msg}${trace}`)
}
}
tip = (msg, vm) => {
if (hasConsole && (!config.silent)) {
console.warn(`[Vue tip]: ${msg}` + (
vm ? generateComponentTrace(vm) : ''
))
}
}
env.js
正所谓磨刀不误砍柴工,在真正阅读源码之前先了解清楚一些变量是如何判断、纯函数的作用是十分有必要的。知道锤子、斧头、扳手、剪刀是干嘛用的,这样才不至于在初次见面的时候显得措手不及。
检测当前宿主环境对变量的支持情况。
//检测当前浏览器是否支持 '__proto__'
export const hasProto = '__proto__' in {}
//检测是否浏览器环境
export const inBrowser = typeof window !== 'undefined'
//检测当前是否是weex环境
export const inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform
//是否weex平台
export const weexPlatform = inWeex && WXEnvironment.platform.toLowerCase()
//先确保当前环境是浏览器环境,然后将浏览器的use Agent信息保存在 UA 变量中 (非浏览器环境则直接返回false)
export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
//是否IE浏览器
export const isIE = UA && /msie|trident/.test(UA)
//是否IE9浏览器
export const isIE9 = UA && UA.indexOf('msie 9.0') > 0
//是否Edge浏览器
export const isEdge = UA && UA.indexOf('edge/') > 0
//是否安卓环境
export const isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android')
//是否IOS环境
export const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios')
//是否Chrome环境
export const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
//当前 window.navigator.userAgent 是否含有 phantomjs 关键字
export const isPhantomJS = UA && /phantomjs/.test(UA)
//是否火狐浏览器环境
export const isFF = UA && UA.match(/firefox\/(\d+)/)
export const nativeWatch = ({}).watch
在火狐浏览器中原生提供了Object.prototype.watch方法。所以只有当前宿主环境为火狐的情况下,nativeWatch才会返回正确的函数,否则直接返回undefined。也可以用于区分VUE实例中的watch,防止冲突产生意外情况。
//通过监听调用test-passive检测当前浏览器是否支持passive。
//其中代码用try catch 包裹起来,说明支持性较差,容易出错
export let supportsPassive = false
if (inBrowser) {
try {
const opts = {}
Object.defineProperty(opts, 'passive', ({
get () {
supportsPassive = true
}
}: Object))
window.addEventListener('test-passive', null, opts)
} catch (e) {}
}
//检测是否服务端环境
let _isServer
export const isServerRendering = () => {
if (_isServer === undefined) {
if (!inBrowser && !inWeex && typeof global !== 'undefined') {
_isServer = global['process'] && global['process'].env.VUE_ENV === 'server'
} else {
_isServer = false
}
}
return _isServer
}
//检测是否开发工具环境
export const devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__
//检测当前方法是否是原生js提供的
export function isNative (Ctor: any): boolean {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
//检测当前宿主环境是否支持原生 symbol Reflect.ownKeys, 返回布尔值
export const hasSymbol =
typeof Symbol !== 'undefined' && isNative(Symbol) &&
typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys)
lang.js
正所谓磨刀不误砍柴工,在真正阅读源码之前先了解清楚一些变量是如何判断、纯函数的作用是十分有必要的。知道锤子、斧头、扳手、剪刀是干嘛用的,这样才不至于在初次见面的时候显得措手不及。
本文件只暴露一个变量,三个方法
export const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
unicode正则表达式,用于解析template模板
export function isReserved (str: string): boolean {
const c = (str + '').charCodeAt(0)
return c === 0x24 || c === 0x5F
}
检测字符串是否是以_或者美元符号$开头,主要用来检测VUE组件实例中变量的命名是否符合规范(VUE不建议以_或$为开头的字符串命名变量,这是框架保留命的名方式)。
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
应该是为了方便调用,作者将Object.defineProperty做了一个简单的封装。def函数支持四个参数,分别是源对象本身、key值、value值、是否可枚举,最后一个参数enumerable非必填,默认不可枚举。
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
对路径进行一个简单的解析,单看这个方法其实比较简单:若path符合正则(例如含有~、/、*)则返回(其实是边界处理,确保不传入异常值而已),否则用.切割path成一个数组,保存在变量segments中,随后返回一个函数,函数内遍历访问segments得到一个对象。此方法主要用于初始化watcher的时候触发值的读取。
perf.js
正所谓磨刀不误砍柴工,在真正阅读源码之前先了解清楚一些变量是如何判断、纯函数的作用是十分有必要的。知道锤子、斧头、扳手、剪刀是干嘛用的,这样才不至于在初次见面的时候显得措手不及。
性能监控专用
定义非生产环境用于监控性能的两个变量mark、measure,生产环境无需监控,直接返回undefined。源代码如下:
import { inBrowser } from './env'
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)
}
}
}
当且仅当宿主是浏览器且在非生产环境下,perf就是window.performance。if语句中做了多个判断,就是为了确保当前环境支持window.performance。之后重新定义mark、measure,以上代码在非生产环境下简化后的结果就是:
const mark = tag => window.performance.mark(tag);
const measure = (name,startTag,endTag) => {
window.performance.measure(name,startTag,endTag)
window.performance.clearMarks(startTag)
window.performance.clearMarks(endTag)
}
window.performance是什么?
Web Performance API允许网页访问某些函数来测量网页和Web应用程序的性能。详细介绍请移步window.performance
shared/util.js
正所谓磨刀不误砍柴工,在真正阅读源码之前先了解清楚一些变量是如何判断、纯函数的作用是十分有必要的。知道锤子、斧头、扳手、剪刀是干嘛用的,这样才不至于在初次见面的时候显得措手不及。
emptyObject
export const emptyObject = Object.freeze({})
创建一个冻结的空对象,这就意味着emptyObject不可扩展,也就是不能添加新的属性
isUndef
export function isUndef (v: any): boolean %checks {
return v === undefined || v === null
}
判断变量是否未定义,也就是判断是否是定义了未赋值,或者赋值null的情况
isDef
export function isDef (v: any): boolean %checks {
return v !== undefined && v !== null
}
判断变量是否定义
isTrue
export function isTrue (v: any): boolean %checks {
return v === true
}
判断变量是否为true
isFalse
export function isFalse (v: any): boolean %checks {
return v === false
}
判断变量是否为false
isPrimitive
export function isPrimitive (value: any): boolean %checks {
return (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
判断变量是否是原始值,也就是判断变量是否未非复合型数据
isObject
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}
判断变量是否是 “对象” 或者 “数组”
toRawType
const _toString = Object.prototype.toString
export function toRawType (value: any): string {
return _toString.call(value).slice(8, -1)
}
判断类型后返回原始字符串类型。例如判断一个数组:Object.prototype.toString.call([])得到的结果是[object Array],再使用slice(8,-1)后得到的结果就是Array字符串
isPlainObject
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
判断变量是否是纯对象
isRegExp
export function isRegExp (v: any): boolean {
return _toString.call(v) === '[object RegExp]'
}
判断变量是否是正则对象
isValidArrayIndex
export function isValidArrayIndex (val: any): boolean {
const n = parseFloat(String(val))
return n >= 0 && Math.floor(n) === n && isFinite(val)
}
判断变量是否是有效的数组索引。
- 正常的数组索引格式应该是大于等于0的整数
- 先将变量解析并返回一个浮点数,保存在变量n中
- 当且仅当变量大于等于0,且是整数,并且是一个有限数值的情况下才会返回true
isPromise
export function isPromise (val: any): boolean {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
判断一个变量是否是Promise
toString
export function toString (val: any): string {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
将变量转换成字符串格式
- 若是null,则返回空字符串
- 若是数组或纯对象,则使用JSON.stringify处理
- 其他情况直接String函数处理
toNumber
export function toNumber (val: string): number | string {
const n = parseFloat(val)
return isNaN(n) ? val : n
}
将变量转换成数字类型,若转换失败(比如转换字符串"aaa"得到的结果是NaN)则返回初始值
makeMap
export function makeMap (
str: string,
expectsLowerCase?: boolean
): (key: string) => true | void {
const map = Object.create(null)
const list: Array<string> = str.split(',')
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase
? val => map[val.toLowerCase()]
: val => map[val]
}
返回一个函数,判断给定的字符串中是否包含指定内容(映射)。
str 数据格式: 以
,间隔的字符串,例如:a,b,c,d,e
expectsLowerCase:是否将map的值key小写
使用示例(检测是否是小写的元音字母):
let isLower = makeMap('a,b,c,d,e');
isLower('b'); // true
isLower('f'); // undefined
remove
export function remove (arr: Array<any>, item: any): Array<any> | void {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
从数组中删除给定元素,并返回被删除项组成的数组:[item]
在数组不为空的情况下,获取当前元素序号,当元素存在的前提下(大于-1),则使用
splice方法删除,并返回被删除的元素组成的新数组,否则返回undefined
使用示例:
let arr = ['a','b','c'];
remove(arr,'a'); // ['a']
//arr变成 ['b','c']
hasOwn
// 封装`Object.prototype.hasOwnProperty`,用于检测给定对象中是否含有给定`key`值
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
return hasOwnProperty.call(obj, key)
}
cached
export function cached<F: Function> (fn: F): F {
const cache = Object.create(null)
return (function cachedFn (str: string) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}: any)
}
为一个纯函数创建创建一个缓存版本的函数。输入的参数必须是一个纯函数,得到的返回也是一个纯函数。
- 首先我们要确认为何一定要传一个纯函数,因为纯函数的输出结果只与输入有关,所运行的环境不能改变输出值。
- 创建一个原型链为空的闭包对象
cache用以缓存结果。 - 随后返回一个函数
cachedFn,优先读取缓存,如果有则直接返回返回的值,否则使用原函数fn计算一次并缓存结果。
camelize
const camelizeRE = /-(\w)/g
export const camelize = cached((str: string): string => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})
将中横线连字符转换成驼峰式命名
capitalize
export const capitalize = cached((str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1)
})
将字符串首字母改成大写
hyphenate
const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cached((str: string): string => {
return str.replace(hyphenateRE, '-$1').toLowerCase()
})
与camelize方法相反,将驼峰式命名改成连字符方式。例如:hyphenateRE('aaBb') => aa-bb
polyfillBind
nativeBind
bind
function polyfillBind (fn: Function, ctx: Object): Function {
function boundFn (a) {
const l = arguments.length
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
boundFn._length = fn.length
return boundFn
}
function nativeBind (fn: Function, ctx: Object): Function {
return fn.bind(ctx)
}
export const bind = Function.prototype.bind
? nativeBind
: polyfillBind
以上其实就是一个bind函数,绑定this指向后返回的一个新函数。
polyfillBind为手动写的一个绑定函数nativeBindFunction原型链上的原生绑定函数bind优先判断是否支持原生绑定函数,若支持则优先使用原生,否则使用手动实现的函数
toArray
export function toArray (list: any, start?: number): Array<any> {
start = start || 0
let i = list.length - start
const ret: Array<any> = new Array(i)
while (i--) {
ret[i] = list[i + start]
}
return ret
}
从指定位置(默认从0开始)开始将类数组转换成数组的方法。
list类数组start可选的开始索引位置
extend
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
将一个对象(_from)的数据拷贝到另一个对象(to)上。若key值有重复,则_from中的会替换to中的数据。注意:若value值为复合型数据,则是浅拷贝。
toObject
export function toObject (arr: Array<any>): Object {
const res = {}
for (let i = 0; i < arr.length; i++) {
if (arr[i]) {
extend(res, arr[i])
}
}
return res
}
遍历数组每个对象元素,将内容拷贝到一个新对象上,并返回新对象。for循环遍历数组,与extend函数一起使用,拷贝到新对象res中,逻辑很简单。
noop
export function noop (a?: any, b?: any, c?: any) {}
一个空函数,什么都不做。
no
export const no = (a?: any, b?: any, c?: any) => false
始终返回false的函数。
identity
export const identity = (_: any) => _
将输入值直接返回的纯函数
genStaticKeys
export function genStaticKeys (modules: Array<ModuleOptions>): string {
return modules.reduce((keys, m) => {
return keys.concat(m.staticKeys || [])
}, []).join(',')
}
从编译器模块生成包含静态键的字符串。
modules是一个数组,是编译器的一个选项,其中每个元素都是一个可能包含staticKeys属性的对象。此方法的目的就是遍历数组元素,收集staticKeys以,隔开组成的拼接字符串。
looseEqual
export function looseEqual (a: any, b: any): boolean {
if (a === b) return true
const isObjectA = isObject(a)
const isObjectB = isObject(b)
if (isObjectA && isObjectB) {
try {
const isArrayA = Array.isArray(a)
const isArrayB = Array.isArray(b)
if (isArrayA && isArrayB) {
return a.length === b.length && a.every((e, i) => {
return looseEqual(e, b[i])
})
} else if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime()
} else if (!isArrayA && !isArrayB) {
const keysA = Object.keys(a)
const keysB = Object.keys(b)
return keysA.length === keysB.length && keysA.every(key => {
return looseEqual(a[key], b[key])
})
} else {
/* istanbul ignore next */
return false
}
} catch (e) {
/* istanbul ignore next */
return false
}
} else if (!isObjectA && !isObjectB) {
return String(a) === String(b)
} else {
return false
}
}
判断两个值是否全等。
looseIndexOf
export function looseIndexOf (arr: Array<mixed>, val: mixed): number {
for (let i = 0; i < arr.length; i++) {
if (looseEqual(arr[i], val)) return i
}
return -1
}
查找给定元素是否在指定数组中,若存在则返回当前元素所在数组中的索引,否则返回-1。
once
export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
利用闭包的特性实现一个只调用一次的函数。