今天带来的是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]
之间的字符串,就是原生类型。
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
转换为字符串。
toNumber
function toNumber(val) { const n = parseFloat(val) return isNaN(n) ? val : n }
将一个值转换为数字,如果转换失败,返回原值。
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
对象的属性值。
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
方法移除这个元素。
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
属性。
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
方法,用于性能优化。
capitalize
export const capitalize = cached((str) => { return str.charAt(0).toUpperCase() + str.slice(1) })
将字符串的首字母大写,这里使用charAt
方法获取字符串的第一个字符,然后使用toUpperCase
方法将其转换为大写,然后使用slice
方法截取字符串的第二个字符到最后一个字符,然后拼接起来。
这里也使用了cached
方法,用于性能优化。
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
函数中;
流程如下:
流程图不能用
===
,所以用==
代替
流程图看着也挺复杂的,但是代码还是很好理解的。
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
函数,我们可以用一个变量来记录函数是否执行过,如果执行过了,那么就不再执行,这样就可以将一个函数转换为只能执行一次的函数;
这些都是很有用的思想,我们可以在平时的开发中多多使用,提高我们的开发效率。