美文网首页Web前端之路优美编程
稍微读一下Lodash中isEqual的实现

稍微读一下Lodash中isEqual的实现

作者: 小遁哥 | 来源:发表于2020-04-02 16:31 被阅读0次

为啥想要了解

前几天看了下React.PureComponentshouldComponentUpdate的默认实现,因为文档中只说是浅比较,就想知道有多浅,又提到不要用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))) { ,因为前面用过全等比较了,所以在这里valueother 有任意一个是 nullundefined都不可能相等,当valueother 都不是对象的时候也会进入,此时我只能想到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

image.png
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 image.png
image.png

undefinedTag = '[object Undefined]',nullTag = '[object Null]', ,调用Object.prototype.toString.call 得到的
symToStringTag 如果浏览器不支持Symbol类型,为undefined

image.png
Symbol.toStringTag 是一个内置的SymbolMap、和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]', 前面的逻辑主要是为了区分数组还是对象,以及他们是否为相同的标签,注意不数据类型,如果不是就退出比较。

image.png

需要注意的是,如果类型为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 长这样

image.png
_.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;
    }

到这里我已经快吐了!
这次是equalFuncstack有值,如果长度不相等就会退出,先排除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;
    }

似乎到了熟悉的地方,注意:
dataViewTagarrayBufferTag是一对。
boolTagdateTagnumberTag 是一起的
eq(+object, +other);做的验证
_.isEqual(new Number(1), 1) 他们会获得同样的标签,也会走到这里,因为Object.prototype.toString.call两者返回的标签一样

function eq(value, other) {
  return value === other || (value !== value && other !== other);
}

errorTag 比较了namemessage两个属性
mapTagsetTag,读到这里我裂开了呀,如果匹配到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是十分好用的。

相关文章

网友评论

    本文标题:稍微读一下Lodash中isEqual的实现

    本文链接:https://www.haomeiwen.com/subject/ksawuhtx.html