【源码共读】Vue2工具函数
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第24期
前言
github仓库地址 在线地址
点击在线地址查看,会发现该文件实际上有很多函数。实际上就是Vue2的工具函数库。下面就来简单学习一下。因为源码用的是ts,理解起来可能会加点成本,所以下面讲解会把类型部分去掉(其实是本人的ts水平不高,很难很好的解释)
工具函数
1. Object.freeze({})
1
| const emptyObject = Object.freeze({});
|
Object.freeze()
方法可以冻结一个对象。如果对象被冻结后,就不能再被修改、不能添加新的属性、不能删除已有属性、不能修改已有属性的配置(可枚举性、可写性等)。
1 2 3 4 5 6 7 8 9 10 11 12
| const freezeObj = Object.freeze({ name: 'clz' });
console.log(freezeObj);
freezeObj.age = 21; console.log(freezeObj);
delete freezeObj.name; console.log(freezeObj);
|
冻结对象后,只是第一层无法修改,第二层还是能够修改滴
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const freezeObj = Object.freeze({ job: { type: 'Coder', salary: -111 } });
console.log(freezeObj);
freezeObj.job.salary = 111; console.log(freezeObj);
delete freezeObj.job.salary; console.log(freezeObj);
freezeObj.name = 'clz'; console.log(freezeObj);
|
我们可以通过Object.isFrozen
来判断对象是不是冻结状态。
1 2 3 4 5 6
| const freezeObj = Object.freeze({ name: 'clz' });
console.log(Object.isFrozen(freezeObj)); console.log(Object.isFrozen({}));
|
那么这个工具库的这个不是函数的变量有什么作用呢?
我知道的场景就是通过赋值冻结的空对象,防止不小心添加属性等操作,比较程序员有可能手误。(这里想要感谢一下若川大佬,评论问问题,很耐心地解答)
2. 判断系列
2.1 isUndef
判断是不是没有定义。
1 2 3
| function isUndef(v) { return v === undefined || v === null }
|
注意:这个函数名是叫isUndef
,但是实际上,如果参数是null
的话,因为null
也没有什么实际意义,所以把null
和undefined
捆绑了(个人感觉此处命名有点点瑕疵)。
2.2 isDef
判断是不是已经定义了。
1 2 3
| function isDef(v) { return v !== undefined && v !== null }
|
这里其实就只是上面的取反就行了。因为定义和未定义本就是相反的。
2.3 isTrue
判断是不是true
。
1 2 3
| function isTrue(v) { return v === true }
|
2.4 isFalse
判断是不是false
。
1 2 3
| function isTrue(v) { return v === true }
|
2.5 isPrimitive
判断是不是原始值,即原始类型的值。
1 2 3 4 5 6 7 8 9
| function isPrimitive(value) { return ( typeof value === 'string' || typeof value === 'number' || typeof value === 'symbol' || typeof value === 'boolean' ) }
|
JS的基础类型有:
string
number
boolean
undefined
null
Symbol
BigInt
undefined
和null
已经在前面的未定义、已定义那里切出去了,而BigInt
是ES2020
新增的基本类型。这里的isPrimitive
更像是判断是不是有用的原始类型。
2.6 isObject
判断是不是对象。
1 2 3
| function isObject(obj) { return obj !== null && typeof obj === 'object' }
|
因为typeof null
的结果也是object
,所以还需要确定不是null
才行。
这里简单讲一下原因:JS存储数据用的是二进制存储,数据的前三位是存储的类型。对象的前三位是000
,而null
则是全为0
,即前三位也是000
。所以typeof null
也是object
。
2.7 isPlainObject
判断是不是纯对象。
1 2 3
| function isPlainObject(obj) { return _toString.call(obj) === '[object Object]' }
|
上面的_toString
实际上是Object.prototype.toString
,因为比较常用,所以处理成_toString
变量。
isObject
只是判断是不是对象,但是实际上用来判断数组也能得到true
的结果,因为数组也是对象。所以还得有一个判断是不是纯对象的方法。而判断的方法也比较简单,只需要调用Object.prototype.toString
方法即可(要使用bind
方法来调用)。如果是数组得到的结果会是'[object Array]'
,而纯对象得到的结果是'[object Object]'
。
2.8 isRegExp
判断是不是正则表达式
1 2 3
| function isRegExp(v) { return _toString.call(v) === '[object RegExp]' }
|
2.9 isValidArrayIndex
判断是不是有效的数组索引值。
1 2 3 4
| function isValidArrayIndex(val) { const n = parseFloat(String(val)) return n >= 0 && Math.floor(n) === n && isFinite(val) }
|
第一步是将参数变成字符串,第二步是转成浮点数。(第一行)
第二行是重点:
n >= 0
,因为数组索引值不能是负数
Math.floor(n) === n
,因为数组索引值不能是负数
isFinite(val
,参数只能是有限数值。在必要情况下,参数会转换称为数值。
下面稍微举几个例子方便理解isFinite
。其实也没啥好说的,就是限制参数只能是有限数值。顺带提一嘴,实际上第一行已经和前两个条件已经能过滤掉一些情况了,因为像是NaN
,true
这些参数,转成字符串再转成浮点数就已经是NaN
了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| console.log(isFinite(Infinity)); console.log(isFinite(NaN)); console.log(isFinite(-Infinity)); console.log(isFinite('12345a'));
console.log(isFinite('0')); console.log(isFinite('123')); console.log(isFinite(123)); console.log(isFinite(-123)); console.log(isFinite(true));
console.log(isFinite(0b101)); console.log(isFinite(0xFF));
console.log(isFinite(1.234))
|
2.10 isPromise
判断是不是Promise对象。这个方法非常有效,并且有意思。判断是不是Promise并不是直接判断,而是通过判断它的then
和catch
属性是不是函数来间接判断是不是Promise对象
1 2 3 4 5 6 7
| function isPromise() { return ( isDef(val) && typeof val.then === 'function' && typeof val.catch === 'function' ) }
|
3. 转换系列
3.1 toRawType
转换成原始类型(得到参数的类型)
1 2 3
| function toRawType(value) { return _toString.call(value).slice(8, -1) }
|
其实就是借助前面提到过的Object.prototype.call(value)
能得到形似[object Type]
的字符串。比较精确,如数组也是对象,通过这个方法能得到是数组,而不只是对象。然后通过slice(8, -1)
把参数的类型部分拿到。
3.2 toString
转换成字符串。
1 2 3 4 5 6 7
| function toString(val) { return val == null ? '' : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) ? JSON.stringify(val, null, 2) : String(val) }
|
首先,原始类型通过String()
方法就能直接转换成对应的字符串,但是undefined
和null
转换成字符串应该是空串才更合理,所以上面用了普通的==
来判断是不是这两个值,如果是,则返回空串。
但是,当值是对象、数组的时候,结果会有点差强人意。
1 2 3 4 5 6 7 8
| const obj = { name: 'clz' };
const arr = [1, 2, 3, 4];
console.log(String(obj)); console.log(String(arr));
|
这时候就需要使用JSON.stringify()
来将它们转换成对象了,可以看一下之前写的笔记JSON的使用之灵活版 | 赤蓝紫
1 2 3 4 5 6 7 8
| const obj = { name: 'clz' };
const arr = [1, 2, 3, 4];
console.log(JSON.stringify(obj)); console.log(JSON.stringify(arr));
|
至于源码中的第三个参数,其实就只是指定缩进的空格是2个,用于美化输出的。
3.3 toNumber
转换成数字型,如果没法转换成数字型就返回原字符串。该方法参数只能是字符串类型。
1 2 3 4
| function toNumber(val) { const n = parseFloat(val) return isNaN(n) ? val : n }
|
注意,由于parseFloat
只要参数字符串的第一个字符能被解析成数字,就会返回数字,即使后面不是数字也一样,如上面例子的123a
。
另外Infinity
也能被解析并返回Infinity
。
3.4 toArray
将伪数组转换成真数组。第二个参数可选,可以控制真数组的开始位置,默认是0。
1 2 3 4 5 6 7 8 9
| 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 }
|
伪数组具有length
属性,但是不具备数组的push
、forEach
等方法。
3.5 extend
扩展对象,把一个对象的属性值扩展到另一个对象上。放在这里主要是后面的toObject
会使用到。
1 2 3 4 5 6
| function extend(to, _from) { for (const key in _from) { to[key] = _from[key] } return to }
|
注意,如果to
上也有_from
的属性,那么_from
的该属性值会覆盖掉to
上的。
3.6 toObject
将一个对象数组合并到另一个对象中去。
1 2 3 4 5 6 7 8 9
| function toObject(arr) { const res = {} for (let i = 0; i < arr.length; i++) { if (arr[i]) { extend(res, arr[i]) } } return res }
|
注意:上面例子中,最后生成的对象不存在之前的456
,这是因为456
不能被for in
遍历,而Hello
能被遍历,只是会被拆解。不过,该方法用法应该只是将数组里的对象合并到另一个对象中去(从注释猜测的)
4. makeMap系列
主要介绍makeMap
方法以及使用makeMap
方法的。
4.1 makeMap
生成一个map
,注意:这里的map
只是键值对形式的对象。并且返回的并不是生成的map
,而是一个函数,用来判断key
在不在map
中的对象。具体可以实例可以查看下面的isBuiltInTag
的。expectsLowerCase
是可选参数,表示会将字符串参数变为小写,即不区分大小写。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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] }
|
这个方法并没有很复杂。简单讲一下步骤:
Object.create(null)
生成没有原型链的空对象
str.split(',')
把字符串以,
为分隔符,将字符串分割为字符串数组
遍历分割的数组,以子字符串为key
,以true
为值添加到map
中,表示该key
在生成的map
中。
返回一个函数,判断key
在不在map
中。这里会判断第二个参数是不是true
,如果是,则不区分大小写。
4.2 isBuiltInTag
判断是不是内置的tag
(这里的内置并不是html
的标签,而是Vue的slot
和component
)。用的是上面的makeMap
方法。
1
| const isBuiltInTag = makeMap('slot,component', true)
|
上面第二个参数为true
,表示不区分大小写,也可以不传,从而区分大小写。
4.3 isReservedAttribute
判断是不是保留的属性。还是通过makeMap
方法来实现,和isBuiltInTag
原理一样,就不再介绍了。
1
| const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')
|
5. remove
从数组中删除指定元素。如果有多个指定元素,只删除第一个。
1 2 3 4 5 6 7 8
| function remove(arr, item) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } }
|
原理就是通过indexOf
找到要删除的元素的位置,然后通过splice()
方法删除掉该元素。
6. hasOwn
判断是不是自己的属性,而不是继承过来的。
其实通过obj.hasOwnProperty(key)
好像就可以了,但是还是封装成了一个方法。本质应该和isTrue
一样,更适合大型项目的开发,比如代码易读性之类的。(猜的,没做过很大型的项目,泪目)
1 2 3 4
| const hasOwnProperty = Object.prototype.hasOwnProperty function hasOwn(obj, key) { return hasOwnProperty.call(obj, key) }
|
7. cached
利用闭包的特性,缓存数据。
1 2 3 4 5 6 7
| function cached(fn) { const cache = Object.create(null) return (function cachedFn(str) { const hit = cache[str] return hit || (cache[str] = fn(str)) }) }
|
接受一个函数,返回一个函数,返回的函数会判断有没有缓存数据,如果有,则直接返回缓存数据,如果没有,才会调用传入的函数,并且会缓存数据。
8. 字符转换系列
8.1 camelize
连字符转驼峰,如on-click
转成onClick
1 2 3 4
| const camelizeRE = /-(\w)/g const camelize = cached(str => { return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') })
|
上面用了正则表达式比较巧妙的实现:
/-(\w)/g
会匹配像是-click
之类的
replace()
替换掉匹配的部分,其中,第二个参数可以是函数,在上面的例子中,该函数的第二个参数代表第1个括号匹配的字符串,即click
,然后让它首字母大写。如果没有匹配到的,则不变化。
注意:上面用了缓存方法cached
来优化。
8.2 capitalize
首字母大写。
1 2 3
| const capitalize = cached(str => { return str.charAt(0).toUpperCase() + str.slice(1) })
|
8.3 hyphenate
驼峰转连字符,如onClick
转成 on-click
1 2 3 4
| const hyphenateRE = /\B([A-Z])/g const hyphenate = cached(str => { return str.replace(hyphenateRE, '-$1').toLowerCase() })
|
这个原理其实和上面的连字符一样,还简单了一点,因为都是小写字母。
原理就是通过正则表达式去匹配字符串,使用括号和$1
实现将匹配到的括号内的字符串变成添加上-
的形式。最后再将整个字符串小写化。
那么\B
有什么用呢?
\B
元字符匹配非单词边界。匹配位置的上一个和下一个字符的类型是相同的,即必须同时是单词,或同时是非单词字符。字符串的开头和结尾处被视为非单词字符。
所以当大写字母在字符串开头时不会被转化,即Onclick
不会变成-onclick
。
顺便看下没有\B
元字符的情况。
9. noop
1
| function noop(a, b, c) {}
|
第一反应:???
后面查了下资料:noop
的主要作用是为一些函数提供默认值,避免传入undefined
之类的数据导致代码出错。即如果参数原本是函数,但是最后传了undefined
的话,就会报xx is not a function
的错。
10. no
永假函数。
1
| const no = (a, b, c) => false
|
11. identity
返回自身。
1
| const identity = (_) => _
|
12. genStaticKeys
生成静态键的字符串。
1 2 3 4 5
| function genStaticKeys(modules) { return modules.reduce((keys, m) => { return keys.concat(m.staticKeys || []) }, []).join(',') }
|
接收一个对象数组,将staticKeys
的值(数组),拼接成静态键的数组,最后将该数组转化成字符串形式,用,
连接。
13. looseEqual
宽松相等:两个对象(包括数组)比较,如果它们形状相同,就返回true
。
{}
和另一个{}
是不相等的,因为对象是引用类型,但是用looseEqual
来判断是会认为相等的,因为形状(内容)完全相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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 { return false } } catch (e) { return false } } else if (!isObjectA && !isObjectB) { return String(a) === String(b) } else { return false } }
|
流程:
a
严格等于b
直接返回true
如果a
和b
都是对象(包括数组),依次执行以下操作:
如果都是数组,判断数组长度是否相等,并通过every
+looseEqual
判断数组元素是否都宽松相等
如果都是Date
对象,那就判断两者的绝对是件是否相同
如果两者都不是数组,那就分别获取a
和b
的键,判断数组长度是否相等,并通过every
+looseEqual
判断数组元素是否都宽松相等
上面都没有符合的话,就返回false
。
如果都不是对象,则比较它们转换为字符串后是否严格相等。
最后返回false
,此时是a
和b
一个是对象,一个不是对象,所以肯定不等。
14. looseIndexOf
返回数组中第一个与参数宽松相等的元素的位置。原生的indexOf
是严格相等。
1 2 3 4 5 6
| function looseIndexOf(arr, val) { for (let i = 0; i < arr.length; i++) { if (looseEqual(arr[i], val)) return i } return -1 }
|
15. once
确保函数只执行一次。
1 2 3 4 5 6 7 8 9
| function once(fn) { let called = false return function () { if (!called) { called = true fn.apply(this, arguments) } } }
|
主要还是利用闭包缓存数据的特性,定义一个初始值为false
的变量,返回一个函数,该函数会判断缓存的数据called
是不是false
,如果是,则将called
变为true
,并执行函数,通过apply
调用来绑定上下文。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function once(fn) { let called = false console.log(this);
return function () { if (!called) { called = true console.log(this); fn.apply(this, arguments) } } }
const obj = { age: 21 }; window.age = 100;
obj.fn = once(function () { console.log(this.age); });
obj.fn();
|
16. polyfillBind
兼容老版本浏览器不支持原生的bind
函数。并且会根据参数的多少来确定使用使用apply
还是call
,如果参数数量大于1,则使用apply
,如果参数数量小于1,则使用call
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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) }
export const bind = Function.prototype.bind ? nativeBind : polyfillBind
|
参考链接