为啥想要了解
前几天看了下React.PureComponent
中shouldComponentUpdate
的默认实现,因为文档中只说是浅比较,就想知道有多浅,又提到不要用JSON.stringify()
这种方法,耗费性能,所以我好奇lodash的isEqual的实现
正文
版本4.17.15
第一步
function isEqual(value, other) {
return baseIsEqual(value, other);
}
第二步
function baseIsEqual(value, other, bitmask, customizer, stack) {
if (value === other) {
return true;
}
if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {
return value !== value && other !== other;
}
return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);
}
baseIsEqual(value, other);
没有传bitmask
, customizer
, stack
,暂时也不用理会!
isObjectLike
是这样的
function isObjectLike(value) {
return value != null && typeof value == 'object';
}
if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {
,因为前面用过全等比较了,所以在这里value
或 other
有任意一个是 null
或undefined
都不可能相等,当value
和 other
都不是对象的时候也会进入,此时我只能想到NaN
会使得value !== value && other !== other;
有意义。
我脚得这样写更清晰些,有些嚣张!
if (value == null || other == null){
return false;
}
if (!isObjectLike(value) && !isObjectLike(other)) {
let isNaN = value !== value && other !== other;
return isNaN;
}
_.isEqual(undefined, null)为false
,尽管null == undefined
为true
_.isEqual(Number.NaN, Number.NaN)为true
,尽管NaN === NaN
为false
那么问题来了,如何实现一个isNaN
函数,记得之前用的是Number.NaN+""
第三步
注意 baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);
参数中baseIsEqual
就是当前函数,所以下面equalFunc
是有值得
baseIsEqualDeep
的实现
function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) {
var objIsArr = isArray(object),
othIsArr = isArray(other),
objTag = objIsArr ? arrayTag : getTag(object),
othTag = othIsArr ? arrayTag : getTag(other);
objTag = objTag == argsTag ? objectTag : objTag;
othTag = othTag == argsTag ? objectTag : othTag;
var objIsObj = objTag == objectTag,
othIsObj = othTag == objectTag,
isSameTag = objTag == othTag;
if (isSameTag && isBuffer(object)) {
if (!isBuffer(other)) {
return false;
}
objIsArr = true;
objIsObj = false;
}
if (isSameTag && !objIsObj) {
stack || (stack = new Stack);
return (objIsArr || isTypedArray(object))
? equalArrays(object, other, bitmask, customizer, equalFunc, stack)
: equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack);
}
if (!(bitmask & COMPARE_PARTIAL_FLAG)) {
var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'),
othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__');
if (objIsWrapped || othIsWrapped) {
var objUnwrapped = objIsWrapped ? object.value() : object,
othUnwrapped = othIsWrapped ? other.value() : other;
stack || (stack = new Stack);
return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack);
}
}
if (!isSameTag) {
return false;
}
stack || (stack = new Stack);
return equalObjects(object, other, bitmask, customizer, equalFunc, stack);
}
蓬松的头发,淡黄的裙子,突然变态起来。
arrayTag = '[object Array]',
isArray
就是 Array.isArray
arrayTag = '[object Array]',
这个好找一些getTag
就是baseGetTag
image.png
function baseGetTag(value) {
if (value == null) {
return value === undefined ? undefinedTag : nullTag;
}
return (symToStringTag && symToStringTag in Object(value))
? getRawTag(value)
: objectToString(value);
}
objectToString
本质上就是Object.prototype.toString.call
image.png
undefinedTag = '[object Undefined]',
、nullTag = '[object Null]',
,调用Object.prototype.toString.call
得到的
symToStringTag
如果浏览器不支持Symbol
类型,为undefined
,
Symbol.toStringTag
是一个内置的Symbol
, Map
、和Promise
能够被Object.prototype.toString()
识别是因为引擎为他们设置好了Symbol.toStringTag
这个值,同样,你可以为自己得对象添加,详情可点击此处Object(value)
可以把基本类型转换为对象image.png
可知 _.isEqual(new Map([[1, 2]]), { name: 2 })
能够进入 getRawTag
function getRawTag(value) {
var isOwn = hasOwnProperty.call(value, symToStringTag),
tag = value[symToStringTag];
try {
value[symToStringTag] = undefined;
var unmasked = true;
} catch (e) {}
var result = nativeObjectToString.call(value);
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag;
} else {
delete value[symToStringTag];
}
}
return result;
}
result = nativeObjectToString.call(value);
小朋友,你会不会有很多得问号?最后返回得也是result
,针对value[symToStringTag]
得一系列操作暂时看不出端倪
value
类型为Map
时,
tag = value[symToStringTag];
为 Map
unmasked = true;
执行了,value[symToStringTag] = undefined;
和delete value[symToStringTag];
都执行,但是没有效果
回到baseIsEqualDeep
中
argsTag = '[object Arguments]',
前面的逻辑主要是为了区分数组还是对象,以及他们是否为相同的标签,注意不数据类型,如果不是就退出比较。
需要注意的是,如果类型为argsTag
,则会判定为objectTag
这个isBuffer
是用在nodejs上的
此时我们来到了
if (isSameTag && !objIsObj) {
stack || (stack = new Stack);
return (objIsArr || isTypedArray(object))
? equalArrays(object, other, bitmask, customizer, equalFunc, stack)
: equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack);
}
注意stack
有值了,
isTypedArray
在浏览器下的实现为
function baseIsTypedArray(value) {
return isObjectLike(value) &&
isLength(value.length) && !!typedArrayTags[baseGetTag(value)];
}
isLength
的实现
function isLength(value) {
return typeof value == 'number' &&
value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}
typedArrayTags
长这样
_.isEqual(new Int8Array(32), new Int8Array(32))
、_.isEqual([1, 2], [1, 2])
能进入equalArrays
function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {
var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
arrLength = array.length,
othLength = other.length;
if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
return false;
}
// Assume cyclic values are equal.
var stacked = stack.get(array);
if (stacked && stack.get(other)) {
return stacked == other;
}
var index = -1,
result = true,
seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined;
stack.set(array, other);
stack.set(other, array);
// Ignore non-index properties.
while (++index < arrLength) {
var arrValue = array[index],
othValue = other[index];
if (customizer) {
var compared = isPartial
? customizer(othValue, arrValue, index, other, array, stack)
: customizer(arrValue, othValue, index, array, other, stack);
}
if (compared !== undefined) {
if (compared) {
continue;
}
result = false;
break;
}
// Recursively compare arrays (susceptible to call stack limits).
if (seen) {
if (!arraySome(other, function(othValue, othIndex) {
if (!cacheHas(seen, othIndex) &&
(arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) {
return seen.push(othIndex);
}
})) {
result = false;
break;
}
} else if (!(
arrValue === othValue ||
equalFunc(arrValue, othValue, bitmask, customizer, stack)
)) {
result = false;
break;
}
}
stack['delete'](array);
stack['delete'](other);
return result;
}
到这里我已经快吐了!
这次是equalFunc
和stack
有值,如果长度不相等就会退出,先排除stack
的一些逻辑,后续就是按照下标去比较。
arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack)
,如果值不全等,会调用equalFunc
,就是baseIsEqual
做深度比较
回到baseIsEqualDeep
_.isEqual(new Map([[1, 2]]), new Map([[1, 2]]))
会触发equalByTag
function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) {
switch (tag) {
case dataViewTag:
if ((object.byteLength != other.byteLength) ||
(object.byteOffset != other.byteOffset)) {
return false;
}
object = object.buffer;
other = other.buffer;
case arrayBufferTag:
if ((object.byteLength != other.byteLength) ||
!equalFunc(new Uint8Array(object), new Uint8Array(other))) {
return false;
}
return true;
case boolTag:
case dateTag:
case numberTag:
// Coerce booleans to `1` or `0` and dates to milliseconds.
// Invalid dates are coerced to `NaN`.
return eq(+object, +other);
case errorTag:
return object.name == other.name && object.message == other.message;
case regexpTag:
case stringTag:
// Coerce regexes to strings and treat strings, primitives and objects,
// as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring
// for more details.
return object == (other + '');
case mapTag:
var convert = mapToArray;
case setTag:
var isPartial = bitmask & COMPARE_PARTIAL_FLAG;
convert || (convert = setToArray);
if (object.size != other.size && !isPartial) {
return false;
}
// Assume cyclic values are equal.
var stacked = stack.get(object);
if (stacked) {
return stacked == other;
}
bitmask |= COMPARE_UNORDERED_FLAG;
// Recursively compare objects (susceptible to call stack limits).
stack.set(object, other);
var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack);
stack['delete'](object);
return result;
case symbolTag:
if (symbolValueOf) {
return symbolValueOf.call(object) == symbolValueOf.call(other);
}
}
return false;
}
似乎到了熟悉的地方,注意:
dataViewTag
、arrayBufferTag
是一对。
boolTag
、dateTag
、numberTag
是一起的
用eq(+object, +other);
做的验证
_.isEqual(new Number(1), 1)
他们会获得同样的标签,也会走到这里,因为Object.prototype.toString.call
两者返回的标签一样
function eq(value, other) {
return value === other || (value !== value && other !== other);
}
errorTag
比较了name
和message
两个属性
mapTag
和 setTag
,读到这里我裂开了呀,如果匹配到mapTag
也会执行 setTag
下面的代码,
我只记得
switch ("0") {
case "0":
case "1":
case "2":
console.log("120");
}
最后转换为数组再调用equalArrays
function mapToArray(map) {
var index = -1,
result = Array(map.size);
map.forEach(function(value, key) {
result[++index] = [key, value];
});
return result;
}
function setToArray(set) {
var index = -1,
result = Array(set.size);
set.forEach(function(value) {
result[++index] = value;
});
return result;
}
感叹扩展运算符的强大!
再次回到baseIsEqualDeep
,看一下equalObjects
_.isEqual({ name: 1 }, { name: 2 })
会进入
function equalObjects(object, other, bitmask, customizer, equalFunc, stack) {
var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
objProps = getAllKeys(object),
objLength = objProps.length,
othProps = getAllKeys(other),
othLength = othProps.length;
if (objLength != othLength && !isPartial) {
return false;
}
var index = objLength;
while (index--) {
var key = objProps[index];
if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) {
return false;
}
}
// Assume cyclic values are equal.
var stacked = stack.get(object);
if (stacked && stack.get(other)) {
return stacked == other;
}
var result = true;
stack.set(object, other);
stack.set(other, object);
var skipCtor = isPartial;
while (++index < objLength) {
key = objProps[index];
var objValue = object[key],
othValue = other[key];
if (customizer) {
var compared = isPartial
? customizer(othValue, objValue, key, other, object, stack)
: customizer(objValue, othValue, key, object, other, stack);
}
// Recursively compare objects (susceptible to call stack limits).
if (!(compared === undefined
? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack))
: compared
)) {
result = false;
break;
}
skipCtor || (skipCtor = key == 'constructor');
}
if (result && !skipCtor) {
var objCtor = object.constructor,
othCtor = other.constructor;
// Non `Object` object instances with different constructors are not equal.
if (objCtor != othCtor &&
('constructor' in object && 'constructor' in other) &&
!(typeof objCtor == 'function' && objCtor instanceof objCtor &&
typeof othCtor == 'function' && othCtor instanceof othCtor)) {
result = false;
}
}
stack['delete'](object);
stack['delete'](other);
return result;
}
如果是个普通对象,则调用Object.keys
,本身不会返回原型链上的属性
先判断了长度
然后循环遍历通过hasOwnProperty
判断在前者有的后者自身属性上也有,而非继承而来
然后循环比较值,通过(objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack))
比较深层次的值
总结
最大的收获是Symbol.Symbol.toStringTag
,由于技术迭代的原因,写法很陈旧,有一些本就是病垢,不值得借鉴,对于业务开发来说,实现显得笨重了些。
很多写法减少了if、else,一定程度上复用了代码,使得逻辑紧凑,却也使得清晰性和可读性下降了。可能这就是一个库的研发人员,和一个库的使用人员视角的不同。
作为一个使用者,lodash是十分好用的。
网友评论