我正在参与掘金会员专属活动-源码共读第一期,点击参与
今天带来的是vue2
源码中的shared
模块,主要是针对util.ts
文件下的几十个实用工具函数,这些函数在vue2
源码中被广泛使用,我们可以通过这些函数来了解vue2
源码的一些实现细节。
废话不多说,直接上仓库地址:
- Vue2源码
- 可以直接点这个到
shared
目录./src/shared
看到源码发现都是.ts
结尾的,看不明白的小伙伴可以使用tsc xxx.ts
命令编译成.js
文件,然后再看。
我下面会将这些函数都装换为js
的代码,方便大家理解和阅读。
如果遇到ts
的代码看不懂,我这里教大家一个方法,简单直接粗暴,直接删除:
后面的东西就好了,比如:
删掉就是下面的样子:
export function isFunction(value) {
return typeof value === 'function'
}
复制代码
这种方式能应对大多数情况,还有一些情况,比如泛型,可以直接删除<xxx>
,比如:
var a: <T>(a: T) => T = function<E>(a: E): E {
return a
}
复制代码
删除<T>
和:
后面的东西后:
var a = function(a) {
return a
}
复制代码
还有很多关键字,如果这些你都了解我觉得你已经可以看懂ts
,就不需要我这里的方法了。
所有的工具函数
我先将所有的工具函数都罗列出来,让大家先有个整体的印象,然后再逐个分析。
emptyObject
: 被冻结的空对象isArray
: es6的Array.isArray
方法isUndef
: 判断是否是undefined
或者null
isDef
: 判断是否不是undefined
并且不是null
isTrue
: 判断是否是true
isFalse
: 判断是否是false
isPrimitive
: 判断是否是原始类型isFunction
: 判断是否是函数isObject
: 判断是否是对象toRawType
: 获取对象的原始类型isPlainObject
: 判断是否是纯对象isRegExp
: 判断是否是正则isValidArrayIndex
: 判断是否是有效的数组索引isPromise
: 判断是否是Promise
对象toString
: 将值转换为实际呈现的字符串toNumber
: 将值转换为数字makeMap
: 生成一个包含指定字符串的对象isBuiltInTag
: 判断是否是内置标签isReservedAttribute
: 判断是否是保留属性remove
: 从数组中移除指定项hasOwn
: 判断对象是否有指定属性cached
: 缓存函数camelize
: 将字符串转换为驼峰格式capitalize
: 将字符串首字母大写hyphenate
: 将字符串转换为连字符格式bind
: 将函数绑定到指定的上下文toArray
: 将类数组转换为数组extend
: 将源对象的属性拷贝到目标对象toObject
: 将数组转换为对象noop
: 空函数no
: 返回false
identity
: 返回传入的值genStaticKeys
: 生成静态键looseEqual
: 比较两个值是否相等looseIndexOf
: 获取指定值在数组中的索引once
: 保证函数只执行一次hasChanged
: 判断两个值是否不相等
一共是36
个函数,下面开始逐个分析。
emptyObject
export const emptyObject = Object.freeze({})
复制代码
使用Object.freeze
冻结一个空对象,这样就可以保证这个对象不会被修改。
简单的提一下Object.freeze
的用法,它可以冻结一个对象,冻结后的对象不可扩展,也就是说不能再添加新的属性,也不能删除已有属性,已有属性的值也不能被修改,同时也不能修改已有属性的getter
和setter
方法。
我这里就不详讲了,这个API
的特性都可以写一篇文章了,大家可以自行查阅。
扩展阅读:Object.freeze()
isArray
export const isArray = Array.isArray
复制代码
这个函数就是es6
的Array.isArray
方法,用来判断一个值是否是数组。
扩展阅读:Array.isArray()
isUndef
export function isUndef(v) {
return v === undefined || v === null
}
复制代码
判断一个值是否是undefined
或者null
,这里使用了严格相等,在源码上面有注释,这样显示定义,有助于js
引擎进行优化。
isDef
export function isDef(v) {
return v !== undefined && v !== null
}
复制代码
判断一个值是否不是undefined
或者null
,正好和isUndef
相反。
isTrue
export function isTrue(v) {
return v === true
}
复制代码
判断一个值是否是true
。
isFalse
export function isFalse(v) {
return v === false
}
复制代码
判断一个值是否是false
。
isPrimitive
export function isPrimitive(value) {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
复制代码
判断一个值是否是原始类型,原始类型包括string
、number
、symbol
、boolean
;
扩展阅读:JavaScript 原始类型
isFunction
export function isFunction(value) {
return typeof value === 'function'
}
复制代码
判断一个值是否是函数。
isObject
export function isObject(obj) {
return obj !== null && typeof obj === 'object'
}
复制代码
判断一个值是否是对象,这里先判断是否是null
,因为null
的类型是object
。
扩展阅读:typeof
toRawType
const _toString = Object.prototype.toString
export function toRawType(value) {
return _toString.call(value).slice(8, -1)
}
复制代码
获取一个指的原生类型,这里使用了Object.prototype.toString
,这个方法可以返回一个对象的原生类型;
比如[object Object]
,然后使用slice
截取[object
和Object]
之间的字符串,就是原生类型。
扩展阅读:Object.prototype.toString()
isPlainObject
export function isPlainObject(obj) {
return _toString.call(obj) === '[object Object]'
}
复制代码
严格检查一个值是否是纯JavaScript
对象,这里还是使用上面获取的_toString
方法。
isRegExp
export function isRegExp(v) {
return _toString.call(v) === '[object RegExp]'
}
复制代码
判断一个值是否是正则表达式,逻辑和上面相同。
isValidArrayIndex
export function isValidArrayIndex(val) {
const n = parseFloat(String(val))
return n >= 0 && Math.floor(n) === n && isFinite(val)
}
复制代码
检查传入的值是否是一个有效的数组索引:
- 先将值做
String
转换 - 然后使用
parseFloat
转换为数字,这里使用parseFloat
而不是parseInt
,因为parseInt
会将3.14
转换为3
,而parseFloat
会保留小数点后面的值; - 判断值是否大于等于
0
,因为数组索引不能为负数; - 判断值是否是一个整数,这里使用
Math.floor
向下取整,然后和原值做严格相等,因为3.14
和3
是不相等的; - 判断值是否是有限的
扩展阅读:
isPromise
export function isPromise(val) {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
复制代码
判断一个值是否是Promise
对象,首先使用上面定义的isDef
方法判断是否是undefined
或者null
;
然后判断then
和catch
是否是函数,因为Promise
对象必须有then
和catch
方法。
尝试了一下使用一个对象里面有then
和catch
方法的对象,返回结果是true
,所以还是约定大于规范。
扩展阅读:Promise
toString
function toString(val) {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
复制代码
将一个值转化为一个可阅读的字符串,拆分一下:
- 如果值是
null
或者undefined
,返回空字符串; - 如果值是数组或者是一个纯对象,且没有重写
toString
方法,使用JSON.stringify
转换为字符串,这里使用JSON.stringify
的第二个参数为null
,第三个参数为2
,表示缩进为2
个空格; - 如果都不是,使用
String
转换为字符串。
扩展阅读:JSON.stringify()
toNumber
function toNumber(val) {
const n = parseFloat(val)
return isNaN(n) ? val : n
}
复制代码
将一个值转换为数字,如果转换失败,返回原值。
扩展阅读:isNaN()
makeMap
export function makeMap(str, expectsLowerCase) {
const map = Object.create(null)
const list = str.split(',')
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase ? val => map[val.toLowerCase()] : val => map[val]
}
复制代码
将一个字符串转换为一个对象,这个对象的属性值都是true
;
最后返回一个函数,这个函数接收一个值,然后判断这个值是否是对象的属性。
这里第二个值影响返回的函数的行为,如果为true
,则会将传入的值转换为小写,然后返回map
对象的属性值。
扩展阅读:toLowerCase()
isBuiltInTag
export const isBuiltInTag = makeMap('slot,component', true)
复制代码
判断一个标签是否是内置标签,这里使用makeMap
方法将slot,component
转换为一个对象,然后返回一个函数;
这个函数将接收一个值,然后然后就可以通过返回值判断这个值是否是内置标签。
可以看到源码中一共有4处用到了这个方法:
isReservedAttribute
export const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')
复制代码
判断一个属性是否是保留属性,同上面的isBuiltInTag
方法相同。
这个只有两处使用:
remove
export function remove(arr, item) {
const len = arr.length
if (len) {
// fast path for the only / last item
if (item === arr[len - 1]) {
arr.length = len - 1
return
}
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
复制代码
从数组中移除一个元素,这里先将数组的长度赋值给len
,如果频繁使用obj.field
的形式访问对象的属性,会比较耗性能;
然后使用隐式类型装换的方式做判断,0 == false
为true
;
然后对比数组的最后一个元素和传入的元素是否相等,如果相等,直接将数组的长度减一,源码中有注释,这是一个快速路径;
最后使用indexOf
方法找到元素的索引,然后使用splice
方法移除这个元素。
扩展阅读:splice()
hasOwn
export const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn(obj, key) {
return hasOwnProperty.call(obj, key)
}
复制代码
判断一个对象是否有某个属性,这里使用hasOwnProperty
方法,这个方法是Object
的原型方法;
然后使用call
方法改变this
指向,将obj
作为this
传入,判断obj
是否有key
属性。
扩展阅读:hasOwnProperty()
cached
export function cached(fn) {
const cache = Object.create(null)
return function cachedFn(str: string) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}
}
复制代码
缓存函数,这里使用Object.create(null)
创建一个空对象,然后返回一个函数;
这个函数接收一个字符串,然后判断这个字符串是否在缓存对象中;
这里使用管道符||
,如果hit
存在,直接返回hit
,否则将fn(str)
的结果赋值给cache[str]
,然后返回cache[str]
。
小知识variable = value
是有返回值的,返回值就是value
。
camelize
const camelizeRE = /-(\w)/g
export const camelize = cached((str) => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})
复制代码
将字符串转换为驼峰命名,这里使用了正则表达式,-(\w)
匹配-
后面的一个字符,然后使用replace
方法替换;
同时这里还使用到了上面的cached
方法,用于性能优化。
扩展阅读:replace()
capitalize
export const capitalize = cached((str) => {
return str.charAt(0).toUpperCase() + str.slice(1)
})
复制代码
将字符串的首字母大写,这里使用charAt
方法获取字符串的第一个字符,然后使用toUpperCase
方法将其转换为大写,然后使用slice
方法截取字符串的第二个字符到最后一个字符,然后拼接起来。
这里也使用了cached
方法,用于性能优化。
扩展阅读:charAt()
hyphenate
const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cached((str) => {
return str.replace(hyphenateRE, '-$1').toLowerCase()
})
复制代码
将驼峰命名转换为连字符命名,这里使用了正则表达式,\B
匹配非单词边界,([A-Z])
匹配大写字母,然后使用replace
方法替换;
这里也使用到了上面的cached
方法;
bind
/**
* Simple bind polyfill for environments that do not support it,
* e.g., PhantomJS 1.x. Technically, we don't need this anymore
* since native bind is now performant enough in most browsers.
* But removing it would mean breaking code that was able to run in
* PhantomJS 1.x, so this must be kept for backward compatibility.
*/
/* istanbul ignore next */
function polyfillBind(fn, ctx) {
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, ctx) {
return fn.bind(ctx)
}
// @ts-expect-error bind cannot be `undefined`
export const bind = Function.prototype.bind ? nativeBind : polyfillBind
复制代码
这里是先定义了一个polyfillBind
函数,然后定义了一个nativeBind
函数,看名字就知道,polyfillBind
是用来兼容不支持bind
方法的浏览器,nativeBind
是直接使用bind
方法;
这里的重点是polyfillBind
函数,里面定义了一个boundFn
函数,这个函数会依据参数的个数来调用fn
的apply
或者call
方法;
扩展阅读:
toArray
export function toArray(list, start) {
start = start || 0
let i = list.length - start
const ret = new Array(i)
while (i--) {
ret[i] = list[i + start]
}
return ret
}
复制代码
将类数组转换为数组,这里使用了while
循环,将list
中的元素依次添加到ret
中;
es6中有Array.from
方法,可以将类数组转换为数组,扩展阅读:Array.from()
extend
export function extend(to, _from) {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
复制代码
将属性合并到目标对象中,也就是将_from
中的属性合并到to
中;
这里使用了for...in
循环,扩展阅读:for...in
toObject
export function toObject(arr) {
const res = {}
for (let i = 0; i < arr.length; i++) {
if (arr[i]) {
extend(res, arr[i])
}
}
return res
}
复制代码
将对象数组合并为一个对象,这里使用了for
循环,然后调用了上面的extend
方法;
es6中有Object.assign
方法,可以将对象合并为一个对象,扩展阅读:Object.assign()
noop
export function noop(a, b, c) {}
复制代码
空函数,什么都不做,通常用于兜底函数;
no
export const no = (a, b, c) => false
复制代码
返回false
的函数,通常用于兜底函数;
identity
export const identity = _ => _
复制代码
返回传入的参数的函数,通常用于兜底函数;
genStaticKeys
/**
* @param modules Array<{ staticKeys?: string[] }
* @return {*}
*/
export function genStaticKeys(modules) {
return modules.reduce((keys, m) => {
return keys.concat(m.staticKeys || [])
}, []).join(',')
}
复制代码
这里写上函数签名方便理解,modules
是一个对象数组,每个对象都有一个staticKeys
属性,这个属性是一个字符串数组;
这个函数的作用是将modules
中的每个对象的staticKeys
属性的值合并为一个字符串,然后用逗号分隔;
looseEqual
export function looseEqual(a, b) {
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
}
}
复制代码
好长,不要慌,来看看这个函数的作用,这个函数的作用是判断两个值是否相等;
看到太长就拆分:
function looseEqual(a, b) {
if (a === b) return true
return equal(a, b)
}
function equal(obj1, obj2) {
const isObjectA = isObject(a)
const isObjectB = isObject(b)
if (isObjectA && isObjectB) {
try {
return arrayEqual(obj1, obj2) || dateEqual(obj1, obj2) || objectEqual(obj1, obj2)
} catch (e) {
/* istanbul ignore next */
return false
}
} else if (!isObjectA && !isObjectB) {
return String(a) === String(b)
} else {
return false
}
}
function arrayEqual(arr1, arr2) {
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])
})
)
}
return false
}
function dateEqual(date1, date2) {
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime()
}
return false
}
function objectEqual(obj1, obj2) {
const keysA = Object.keys(obj1)
const keysB = Object.keys(obj2)
return (
keysA.length === keysB.length &&
keysA.every(key => {
return looseEqual(a[key], b[key])
})
)
}
复制代码
拆分开来可以看到,主要分为四种情况:
- 如果两个值是相等的,直接返回
true
,这里的相等是指===
,也就是说,如果两个值是引用类型,那么只有当两个值的引用地址相同时才会返回true
; - 如果两个值都是
Array
,那么判断两个数组的长度是否相等,如果相等,那么再判断两个数组中的每个元素是否相等,如果相等,递归到looseEqual
函数中; - 如果两个值都是
Date
,那么判断两个Date
对象的时间戳是否相等; - 如果两个值都是
Object
,那么判断两个对象的键值对个数是否相等,如果相等,递归到looseEqual
函数中;
流程如下:
graph TB
A[looseEqual] --> B[a==b]
B -->|是| C[return true]
B -->|否| D[a b 都是数组]
D -->|是| E[判断两个数组的长度是否相等]
E -->|否| G[a b 都是日期]
E -->|是| F[判断两个数组中的每个元素是否相等]
F --> A
G -->|否| H[a b 都是对象]
G -->|是| I[判断两个日期的时间戳是否相等]
I -->|否| J[return false]
I -->|是| K[return true]
H -->|否| L[a b 都不是对象]
L -->|否| M[return false]
L -->|是| N[判断两个对象的键值对个数是否相等]
N -->|否| O[return false]
N -->|是| P[判断两个对象中的每个键值对是否相等]
P --> A
流程图不能用
===
,所以用==
代替
流程图看着也挺复杂的,但是代码还是很好理解的。
looseIndexOf
function looseIndexOf(arr, val) {
for (let i = 0; i < arr.length; i++) {
if (looseEqual(arr[i], val)) return i
}
return -1
}
复制代码
这个函数就是在数组中查找某个值的索引,如果找到了,就返回索引,否则返回-1
。
这里就用到了looseEqual
函数,如果两个值相等,那么就返回索引,否则继续循环。
once
function once(fn) {
let called = false
return function() {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
复制代码
这个函数的作用是将一个函数转换为只能执行一次的函数。
这里用到了一个闭包,called
变量是在函数外部定义的,所以它的作用域是全局的,当once
函数执行时,called
变量会被赋值为false
,然后返回一个函数,这个函数就是once
函数的返回值,这个函数会判断called
变量是否为false
,如果是,那么就将called
变量赋值为true
,然后执行fn
函数,如果called
变量不是false
,那么就不执行fn
函数。
hasChanged
export function hasChanged(x, y) {
if (x === y) {
return x === 0 && 1 / x !== 1 / y
} else {
return x === x || y === y
}
}
复制代码
这个函数是Object.is
的polyfill
,Object.is
是用来判断两个值是否相等的;
扩展阅读:Object.is()
结束
到这里,我们就完成了Vue
的utils
模块的源码分析;
这个模块的代码量不大,但是里面的函数还是很有用的,这里最多的就是类型判断,还有一些常用的函数,对于我们日常开发可能大多数都用不到,但是里面一些思想都值得我们学习;
例如类型判断,我们可以用typeof
来判断,但是typeof
有一些缺陷,比如typeof null
的结果是object
,所以我们可以用Object.prototype.toString.call
来判断,这样就可以准确的判断出类型;
例如缓存函数,我们可以用一个对象来缓存函数的执行结果,这样就可以减少函数的执行次数,提高性能;
例如once
函数,我们可以用一个变量来记录函数是否执行过,如果执行过了,那么就不再执行,这样就可以将一个函数转换为只能执行一次的函数;
这些都是很有用的思想,我们可以在平时的开发中多多使用,提高我们的开发效率。