- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第 24 期,链接:juejin.cn/post/707976…。
源码内容及地址
源码解读
将工具函数大致分为以下几类:
- is**:用于判断是什么
- to**: 用于做转换
- has**: 用于判断是否有什么
- 辅助类方法
一、is** 部分
1.1 isUndef 判断是否为未定义,即是否是undefined
或null
function isUndef (v) {
return v === undefined || v === null
}
1.2 isDef 判断是否为已定义,即不等于undefined
同时也不等于null
function isDef (v) {
return v !== undefined && v !== null
}
1.3 isTrue 判断传入值是否为布尔值true
function isTrue (v) {
return v === true
}
1.4 isFalse 判断传入值是否为布尔值false
function isFalse (v) {
return v === false
}
1.5 isPrimitive 判断传入值是否为原始值
注意:此处原始值判断没有包含Null 类型
、Undefined 类型
、BigInt 类型
,应该是出于当前上下文(vue2)需要
function isPrimitive (value) {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
1.6 isObject 用于判断是否为非空的对象
注意:当前方法使用的前提是知道传入的值是对象类型,因keyof []
返回的也是'object'字符串
function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
1.7 isPlainObject 用于判断传入值是否为纯对象类型
// 获取对象的内置对象类型
var _toString = Object.prototype.toString;
/**
* Strict object type check. Only returns true
* for plain JavaScript objects.
*/
function isPlainObject (obj) {
return _toString.call(obj) === '[object Object]'
}
1.8 isRegExp 用于判断传入值是否为正则类型
function isRegExp (v) {
return _toString.call(v) === '[object RegExp]'
}
1.9 isValidArrayIndex 检查传入值是否为有效的数组索引
- 注意:
isFinite
方法用来判断被传入的参数值是否为一个有限数值Math.floor
向下取整parseFloat
函数解析一个参数(必要时先转换为字符串?这里有疑问,为啥要先转成字符串)并返回一个浮点数。
function isValidArrayIndex (val) {
var n = parseFloat(String(val));
return n >= 0 && Math.floor(n) === n && isFinite(val)
}
1.10 isPromise 判断传入值是否为 Promise 对象
function isPromise (val) {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
二、to** 部分
2.1 toRawType 获取内置类型字符串
// 获取对象的内置对象类型
var _toString = Object.prototype.toString;
function toRawType (value) {
return _toString.call(value).slice(8, -1)
}
2.2 toString 将值转化成实际展示的字符串
注意:val == null
这一步判断在val
为undefined
时也是返回true
,此处建议换成上文提到的isUndef
方法,语义上会更清晰些。
function toString (val) {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
2.3 toNumber 将输入值转化成数值,转换失败则返回原始值
注意:isNaN
用于判断传入是不是 NAN
(不是一个数值)
function toNumber (val) {
var n = parseFloat(val);
return isNaN(n) ? val : n
}
2.4 toArrary 将类数组对象转化成真正的数组
注意:类数组对象:可通过索引访问属性值并且具有length
属性的对象
function toArray (list, start) {
start = start || 0;
var i = list.length - start;
var ret = new Array(i);
while (i--) {
ret[i] = list[i + start];
}
return ret
}
2.5 toObject 将对象数组合并成一个对象
/**
* 将属性混合到目标对象中,类似于 Object.assign() 方法
*/
function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}
/**
* 将对象数组合并成一个对象
*/
function toObject (arr) {
var res = {};
for (var i = 0; i < arr.length; i++) {
if (arr[i]) {
extend(res, arr[i]);
}
}
return res
}
三、has** 部分
3.1 hasOwn 检查属性是否存在于对象中
注意:该方法其实就是Object.prototype.hasOwnProperty.call(obj, key)
方法的简写
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}
四、辅助类方法
4.1 通过Object.freeze
方法创建一个锁定的空对象
var emptyObject = Object.freeze({});
注意:
Object.freeze()
可以用来冻结对象类型的值(对象、数组)- 可通过
Object.isFrozen()
方法判断一个对象是否被冻结 - 在 ES5 中,如果这个方法的参数不是一个对象(一个原始值),那么它会导致
TypeError
。在 ES6 中,非对象参数将被视为要被冻结的普通对象,并被简单地返回。 Object.freeze()
为浅冻结,只冻结对象或数组的第一层,深冻结代码实现如下
// 深冻结函数。
function deepFreeze(obj) {
// 取回定义在 obj 上的属性名
var propNames = Object.getOwnPropertyNames(obj);
// 在冻结自身之前冻结属性
propNames.forEach(function(name) {
var prop = obj[name];
// 如果 prop 是个对象,冻结它
if (typeof prop == 'object' && prop !== null)
deepFreeze(prop);
});
// 冻结自身 (no-op if already frozen)
return Object.freeze(obj);
}
obj2 = {
internal: {}
};
deepFreeze(obj2);
obj2.internal.a = 'anotherValue';
obj2.internal.a; // undefined
- 用
Object.seal()
密封的对象可以改变它们现有的属性。使用Object.freeze()
冻结的对象中现有属性是不可变的。 - 解冻:原生中冻结是不可逆的,可通过深拷贝来实现解冻
4.2 makeMap 用于生成一个map
并返回一个方法来验证传入值是否存在于该map
中
注意:此方法利用了闭包的特性
function makeMap (
str,
expectsLowerCase
) {
var map = Object.create(null);
var list = str.split(',');
for (var i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase
? function (val) { return map[val.toLowerCase()]; }
: function (val) { return map[val]; }
}
4.3 isBuiltInTag 检查标签是否为 Vue
内置标签
var isBuiltInTag = makeMap('slot,component', true);
4.4 isReservedAttribute 检查属性是否为保留属性
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');
4.5 remove 从数组中移除指定项
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
上述方法优化建议: 当数组中不存在指定项时,因该返回原数组,做到输入输出一致
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
return arr
}
return arr
}
4.6 cached 创建一个能够缓存传入方法的计算结果的方法,避免重复计算
function cached (fn) {
var cache = Object.create(null);
return (function cachedFn (str) {
var hit = cache[str];
return hit || (cache[str] = fn(str))
})
}
4.7 camelize 将连字符转成小驼峰
var camelizeRE = /-(\w)/g;
var camelize = cached(function (str) {
return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});
4.8 capitalize 将首字母转成大写
var capitalize = cached(function (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
});
4.9 hyphenate 将驼峰法转成连字符
var hyphenateRE = /\B([A-Z])/g;
var hyphenate = cached(function (str) {
return str.replace(hyphenateRE, '-$1').toLowerCase()
});
4.10 genStaticKeys 获取 modules
中的 staticKeys
字符串
function genStaticKeys (modules) {
return modules.reduce(function (keys, m) {
return keys.concat(m.staticKeys || [])
}, []).join(',')
}
4.11 looseEqual 判断两个值是否近似相等
解读:
-
第一步,
a === b
,判断两个方法是否完全相等,是则返回true
,否则继续向下执行 -
第二步,通过
isObject
方法得到出入的值是否是引用类型的值,如果两个都不是引用类型,则直接转成字符串,再做全等判断,如果一个是一个不是则直接返回false
,如果两个都是引用类型,则继续向下判断- 判断两个值是不是数组,都是则继续递归子元素,一个是,一个不是则返回
false
- 判断两个值是不是
Date
类型,都是,则转成时间戳在进行判断,一个是,一个不是则返回false
- 判断两个值是不是
Object
类型,都是,则递归判断子元素是否相等,一个是,一个不是则返回false
- 判断两个值是不是数组,都是则继续递归子元素,一个是,一个不是则返回
function looseEqual (a, b) {
if (a === b) { return true }
var isObjectA = isObject(a);
var isObjectB = isObject(b);
if (isObjectA && isObjectB) {
try {
var isArrayA = Array.isArray(a);
var isArrayB = Array.isArray(b);
if (isArrayA && isArrayB) {
return a.length === b.length && a.every(function (e, i) {
return looseEqual(e, b[i])
})
} else if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime()
} else if (!isArrayA && !isArrayB) {
var keysA = Object.keys(a);
var keysB = Object.keys(b);
return keysA.length === keysB.length && keysA.every(function (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
}
}
4.12 looseIndexOf 返回数组中与 val
近似相等的值的索引
function looseIndexOf (arr, val) {
for (var i = 0; i < arr.length; i++) {
if (looseEqual(arr[i], val)) { return i }
}
return -1
}
4.13 once 限制传入方法只能执行一次
function once (fn) {
var called = false;
return function () {
if (!called) {
called = true;
fn.apply(this, arguments);
}
}
}
4.14 内部常用常量
var SSR_ATTR = 'data-server-rendered';
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
];
五、总结
整个工具方法阅读下来收获颇丰:
- 友好的方法命名,能让我们更好的区分其作用;
- 回顾了js 的数据类型:原始类型(
Null 类型
、Undefined 类型
、BigInt 类型
、String 类型
、Number 类型
、Symbol 类型
、Boolean 类型
)和引用类型; - 知道闭包的更多应用场景,计算结果缓存、调用次数限制等;
- 回顾了对象冻结相关的知识点; ...
最主要的还是迈出了源码阅读的重要一步,发现源码阅读并没有想象中的那么难。在此感谢若川大佬提供这样一个机会帮助我们去更好的阅读学习源码。