本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
前言
源码路径:/src/shared/util.js 平时在搭建项目的基础架构时,必不可少的会添加 utils 工具函数, 那 Vue 源码是怎么写工具函数的呢? 带着疑问随笔者一起走进 Vue 源码的第一课工具函数
工具函数
emptyObject
export const emptyObject = Object.freeze({}) // 创建空对象
Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。
isUndef
// 这可以帮助在 JS 引擎中生成更好的 VM 代码,因为它们具有显式性和函数内联特性。
export function isUndef (v: any): boolean %checks { // 是否是未定义
return v === undefined || v === 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
}
isFalse
export function isFalse (v: any): boolean %checks { // 是否为假
return v === false
}
isPrimitive
export function isPrimitive (value: any): boolean %checks { // 判断入参是否是原始类型
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
js原始数据类型为:Number、String、Boolean、Null、Undefined、Symbol
isObject
// 快速检查对象 - 这主要用于告诉我们,当知道值为原始类型对象时,是要符合 JSON 的类型。
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}
toRawType
/**
* Get the raw type string of a value, e.g., [object Object].
*/
// 获取值的原始类型字符串
const _toString = Object.prototype.toString
// 获取当前值的原始类型字符串,从倒数第二位开始,截取8个长度字符
export function toRawType (value: any): string {
return _toString.call(value).slice(8, -1)
}
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, 是否是有限数
}
isFinite() 函数用于检查其参数是否是无穷大,也可以理解为是否为一个有限数值(finite number)。
提示: 如果参数是 NaN,正无穷大或者负无穷大,会返回 false,其他返回 true。
isPromise
export function isPromise (val: any): boolean { // 是否为promise
return (
isDef(val) && // 已定义
typeof val.then === 'function' && // 拥有then方法 是 function 类型
typeof val.catch === 'function' // 拥有catch方法 是 function 类型
)
}
toString
export function toString (val: any): string { // 序列化{a:1, b:2} 形式等
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) // 判断若为数组或对象,且prototype有toString方法
? JSON.stringify(val, null, 2) // 开始序列化,val原数据,null为替换,2为缩进值
: String(val) // 序列化
}
toNumber
export function toNumber (val: string): number | string { // 转换数字
const n = parseFloat(val)
return isNaN(n) ? val : n
}
parseFloat() 函数可解析一个字符串,并返回一个浮点数。
该函数指定字符串中的首个字符是否是数字。如果是,则对字符串进行解析,直到到达数字的末端为止,然后以数字返回该数字,而不是作为字符串。
isNaN() 函数用来确定一个值是否为NaN 。注:isNaN函数内包含一些非常有趣的规则;你也可以使用 ECMAScript 2015 中定义的 Number.isNaN() 来判断。
makeMap
export function makeMap (
str: string,
expectsLowerCase?: boolean
): (key: string) => true | void { // 创建个字典,返回函数检测key是否存在于map
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]
}
isBuiltInTag
// 检测标签tag是否是内置tag,比如 slot, component, 例如: isBuiltInTag('mytag') // undefined
export const isBuiltInTag = makeMap('slot,component', true)
isReservedAttribute
// 检查属性是否为vue保留属性,key,ref,slot,slot-scope,is, 例如isReservedAttribute('ref') // undefined
export const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')
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)
}
}
}
hasOwn
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 - 然后判断输入参数是不是在
cache的中。如果已经存在,直接返回cache的内容,如果没有存在,使用函数func对输入参数求值,然后把结果存储在cache中
使用场景:
- 需要大量重复计算
- 大量计算并且依赖之前的结果 参考知乎专栏--Javascript缓存函数&柯里化&偏函数
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()
})
\b 单词边界
\B 非单词边界
bind
function polyfillBind (fn: Function, ctx: Object): Function { // 实现bind
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 { // 原生bind
return fn.bind(ctx)
}
// 兼容bind
export const bind = Function.prototype.bind
? nativeBind
: polyfillBind
简单来说就是兼容了老版本浏览器不支持原生的 bind 函数。同时兼容写法,对参数的多少做出了判断,使用call和apply实现,据说参数多适合用 apply,少用 call 性能更好。?????
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
}
extend
export function extend (to: Object, _from: ?Object): Object { // 扩展集成(对象合并)
for (const key in _from) {
to[key] = _from[key]
}
return to
}
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
}
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 { // 合并每个数组的staticKeys项
return modules.reduce((keys, m) => {
return keys.concat(m.staticKeys || [])
}, []).join(',')
}
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
}
once
export function once (fn: Function): Function { // 函数执行一次
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}