lodash源码解析:uniq和union家族

2,578 阅读8分钟

本篇继续分析下 uniqunion 家族的方法,uniq 方法主要是将数组去重,union 方法主要是将多个数组聚合成一个数组并去重。包括uniquniqByuniqWithunionunionByunionWith、以及核心方法baseUniqbashFlatten。并说明下在 lodashbywith这两个后缀的区别。

对应源码分析已推到 github 仓库: github.com/MageeLin/lo…

uniqunion 家族方法的依赖路径图如下所示,其实只用看左半部分,右半部分的代码已经在之前的文章中分析过。

uniq-and-union

lodash 中的后缀

lodash 的方法中,同一家族的方法都是用后缀来区别开的,比如本篇中的 uniqByuniqWith。仔细翻了下所有的方法,最常用的后缀就是下表这几个:

后缀意义
byiteratee(value),迭代方法
withcomparator(arrVal, othVal),比较方法
whilepredicate(value, index, array),断言方法
lastfromRight,指示是否从右向左
rightfromRight,指示是否从右向左
deepdepthCLONE_DEEP_FLAG,指示是否深度运算

这里面大部分的后缀还是很容易看明白它的作用的,但是迭代方法 by 和比较方法 with 就很难分明白。多分析了几个方法,把自己的理解分享下。

就拿 uniq 方法举例,同样都是去重,uniqByuniqWith 的官方示例如下:

// uniqBy的官方示例
uniqBy([2.1, 1.2, 2.3], Math.floor);
// => [2.1, 1.2]

// uniqWith的官方示例
const objects = [
  { x: 1, y: 2 },
  { x: 2, y: 1 },
  { x: 1, y: 2 },
];
uniqWith(objects, isEqual);
// => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]

很显然,uniqBy 的示例中,是把数组中所有的元素都向下取整后进行了对应的去重,也就是说挨个比较了 Math.floor(2.1)Math.floor(1.2)Math.floor(2.3)相同的就对应去重.

而在 uniqWith 中,是调用了 isEqual 进行的两两比较,也就是说挨个比较 isEqual(objects[0], objects[1])isEqual(objects[0], objects[2])isEqual(objects[1], objects[2])返回true的就对应去重。

所以在我看来,with 是可以完全实现所有的 by 的,比如 uniqBy 的官方示例可以使用 uniqWith 来实现。

// uniqBy的官方示例
uniqBy([2.1, 1.2, 2.3], Math.floor);
// => [2.1, 1.2]

// 用uniqWith实现
uniqWith(
  [2.1, 1.2, 2.3],
  (arrVal, othVal) => Math.floor(arrVal) === Math.floor(othVal)
);
// => [2.1, 1.2]

uniq 和 union 家族

uniqunion 家族放在一起分析,是因为 union 的实现是利用的先将多个数组展平,然后再去重来实现的,所以 union 家族的三个方法都依赖到了 baseUniq

依赖的内部方法

setToArray

set 转化为 array

/**
 * 将 `set` 的值转化为 `array`
 *
 * @private
 * @param {Object} set 要转化的 `set`
 * @returns {Array} 返回 `values` 组成的数组
 */
function setToArray(set) {
  let index = -1;
  // 根据set的长度创建一个定长数组
  const result = new Array(set.size);

  // 不断的把set的值push给result
  set.forEach((value) => {
    result[++index] = value;
  });
  // 返回结果
  return result;
}

export default setToArray;

createSet

创建一个 set,主要是在创建 set 做了兼容性的判断,很巧妙。

import setToArray from './setToArray.js';

/** 作为各种`Number`常量的引用 */
const INFINITY = 1 / 0;

/**
 * 创建一个`values`组成的set对象
 *
 * @private
 * @param {Array} values 要添加到set中的values值
 * @returns {Object} 返回新的set
 */
// [,-0] => [empty, -0]
// new Set([,-0]) => Set{ 0: undefined, 1: 0 }
// setToArray(new Set([,-0])) => [undefined, 0]
// setToArray(new Set([,-0]))[1] => 0
// 1 / setToArray(new Set([,-0]))[1] => Infinity
// 所以可以看出,这里第一个目的是来试一试 undefined 会不会被 Set 跳过,第二个目的是试一试-0会不会被转为0
// 用来判断浏览器对Set的支持程度
const createSet =
  Set && 1 / setToArray(new Set([, -0]))[1] == INFINITY
    ? // 可以用set时,就返回创建set的函数
      (values) => new Set(values)
    : // 不能用set,就返回个空函数
      () => {};

export default createSet;

baseUniq

baseUniq 实现时也进行了长数组的优化,如果是手动实现可以不用这么复杂,根本原理就是双层的迭代。

import SetCache from './SetCache.js';
import arrayIncludes from './arrayIncludes.js';
import arrayIncludesWith from './arrayIncludesWith.js';
import cacheHas from './cacheHas.js';
import createSet from './createSet.js';
import setToArray from './setToArray.js';

/** 超过该常量后,就进行大数组优化 */
const LARGE_ARRAY_SIZE = 200;

/**
 * `uniqBy`的基础实现
 *
 * @private
 * @param {Array} array 要检查的数组
 * @param {Function} [iteratee] iteratee迭代器调用每个元素
 * @param {Function} [comparator] comparator比较器比较每个元素
 * @returns {Array} 返回一个新的数组副本
 */
function baseUniq(array, iteratee, comparator) {
  // 初始化变量
  let index = -1;
  let includes = arrayIncludes;
  let isCommon = true;

  const { length } = array;
  const result = [];
  // 把seen的指向result的地址
  let seen = result;

  // 当第三个参数comparator存在时,就非普通模式
  // 并把includes设为arrayIncludesWith
  if (comparator) {
    isCommon = false;
    includes = arrayIncludesWith;
  }
  // 另外如果length过长,也是非普通模式
  // 就用cache缓存的模式
  else if (length >= LARGE_ARRAY_SIZE) {
    const set = iteratee ? null : createSet(array);
    if (set) {
      return setToArray(set);
    }
    isCommon = false;
    includes = cacheHas;
    seen = new SetCache();
  }
  // 普通模式下,就指向result
  else {
    seen = iteratee ? [] : result;
  }

  // 跳出的位置
  // 开始迭代
  outer: while (++index < length) {
    let value = array[index];
    // 如果有迭代器,就用迭代器来变换下本次迭代的元素
    const computed = iteratee ? iteratee(value) : value;

    // 当comparator不存在且value为0时,将value设为0
    value = comparator || value !== 0 ? value : 0;

    // 普通模式情况下
    if (isCommon && computed === computed) {
      let seenIndex = seen.length;
      // 在这里实现了去重,如果seen中查到了与computed相同的元素,就跳出到最外层
      while (seenIndex--) {
        if (seen[seenIndex] === computed) {
          continue outer;
        }
      }
      // 有iteratee时,就给seen push一个转化后的
      if (iteratee) {
        seen.push(computed);
      }
      // 给结果数组push该值
      result.push(value);
    }
    // 非普通模式下,就调用includes方法
    else if (!includes(seen, computed, comparator)) {
      // 当为缓存的情况下,就给seen push这个computed
      if (seen !== result) {
        seen.push(computed);
      }
      // 给结果数组push该值
      result.push(value);
    }
  }
  return result;
}

export default baseUniq;

baseFlatten

baseFlatten 之前解析过,不赘述它的依赖了。

import isFlattenable from './isFlattenable.js';

/**
 * `flatten`方法的基本方法,支持带约束条件的扁平化
 *
 * @private
 * @param {Array} array 要扁平化的数组
 * @param {number} depth 最大的递归深度
 * @param {boolean} [predicate=isFlattenable] 每次迭代调用的断言函数
 * @param {boolean} [isStrict] 仅限通过`predicate`断言函数检查的值。
 * @param {Array} [result=[]] 初始的结果数组
 * @returns {Array} 返回一个新的扁平化后的数组。
 */
function baseFlatten(array, depth, predicate, isStrict, result) {
  // 给predicate和result置初始值
  // 默认是 isFlattenable 和 []
  predicate || (predicate = isFlattenable);
  result || (result = []);

  // array为null或undefined时,返回result
  if (array == null) {
    return result;
  }

  // 开始迭代
  for (const value of array) {
    // 深度大于0且可扁平化的value才执行下一步
    if (depth > 0 && predicate(value)) {
      if (depth > 1) {
        // 递归展平数组(受到调用堆栈数限制)。
        baseFlatten(value, depth - 1, predicate, isStrict, result);
      } else {
        // 达到深度或完全展平后就push到result中
        result.push(...value);
      }
      // 不可扁平化的值就按原样赋值给结果数组(isStrict为假的情况下)
    } else if (!isStrict) {
      // 这里其实处理的是不能通过predicate检验的元素,当isStrict为true的时候,就直接舍弃
      // 相当于push,不使用push是因为性能原因
      // 参见https://segmentfault.com/q/1010000021808718
      result[result.length] = value;
    }
  }
  // 把最后的result数组返回
  return result;
}

export default baseFlatten;

uniq 家族

uniq 家族的实现都是利用的 baseUniq,分别是调用如下方法:

  1. baseUniq(array)
  2. baseUniq(array, iteratee)
  3. baseUniq(array, undefined, comparator)

uniq

import baseUniq from './.internal/baseUniq.js';

/**
 * 创建一个去重后的array数组副本。
 * 使用了 [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
 * 做等值比较。只有第一次出现的元素才会被保留。值在array中的顺序决定result的顺序。
 *
 * @since 0.1.0
 * @category Array
 * @param {Array} array 要检查的数组。
 * @returns {Array} 返回一个去重后的新的array副本。
 * @see uniqBy, uniqWith
 * @example
 *
 * uniq([2, 1, 2])
 * // => [2, 1]
 */
function uniq(array) {
  // array有值且为数组时,返回baseUniq()的结果
  return array != null && array.length ? baseUniq(array) : [];
}

export default uniq;

uniqBy

import baseUniq from './.internal/baseUniq.js';

/**
 * 这个方法类似 `uniq` ,但是它接受一个 `iteratee` (迭代函数),
 * 调用数组(array)的每个元素以产生唯一性计算的标准。
 * 值在array中的顺序决定result的顺序。
 * iteratee 调用时会传入一个参数:(value)。
 *
 * @since 4.0.0
 * @category Array
 * @param {Array} array 要检查的array。
 * @param {Function} iteratee iteratee调用每个元素。
 * @returns {Array} 返回一个去重后的新的array副本。
 * @see uniq, uniqWith
 * @example
 *
 * uniqBy([2.1, 1.2, 2.3], Math.floor)
 * // => [2.1, 1.2]
 */
function uniqBy(array, iteratee) {
  // array有值且为数组时,返回baseUniq()的结果
  return array != null && array.length ? baseUniq(array, iteratee) : [];
}

export default uniqBy;

uniqWith

import baseUniq from './.internal/baseUniq.js';

/**
 * 这个方法类似 `uniq`, 但是它接受一个 `comparator`比较器,
 * 调用比较`array`数组的每一个元素。
 * 值在array中的顺序决定result的顺序。
 * comparator 调用时会传入2个参数: (arrVal, othVal)。
 *
 * @since 4.0.0
 * @category Array
 * @param {Array} array 要检查的数组。
 * @param {Function} [comparator] comparator 调用每个元素。
 * @returns {Array} 返回一个去重后的新的array副本。
 * @see uniq, uniqBy
 * @example
 *
 * const objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }]
 *
 * uniqWith(objects, isEqual)
 * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]
 */
function uniqWith(array, comparator) {
  // 检查comparator是不是一个函数
  comparator = typeof comparator === 'function' ? comparator : undefined;
  return array != null && array.length
    ? // 调用baseUniq()
      baseUniq(array, undefined, comparator)
    : [];
}

export default uniqWith;

union 家族

union 家族的实现都是利用的 baseUniqbaseFlatten,分别是调用如下方法:

  1. baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true))
  2. baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), iteratee)
  3. baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), undefined, comparator)

union

import baseFlatten from './.internal/baseFlatten.js';
import baseUniq from './.internal/baseUniq.js';
import isArrayLikeObject from './isArrayLikeObject.js';

/**
 * 创建一个按顺序排列的唯一值的数组。
 * 所有给定数组的元素值使用
 * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
 * 做等值比较。
 * (注: arrays(数组)的并集,按顺序返回,返回数组的元素是唯一的)
 *
 * @since 0.1.0
 * @category Array
 * @param {...Array} [arrays] 要检查的arrays
 * @returns {Array} 返回arrays的并集
 * @see difference, unionBy, unionWith, without, xor, xorBy
 * @example
 *
 * union([2, 3], [1, 2])
 * // => [2, 3, 1]
 */
function union(...arrays) {
  // 调用baseUniq(baseFlatten())
  return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true));
}

export default union;

unionBy

import baseFlatten from './.internal/baseFlatten.js';
import baseUniq from './.internal/baseUniq.js';
import isArrayLikeObject from './isArrayLikeObject.js';
import last from './last.js';

/**
 * 这个方法类似 `union` ,但是它接受一个 `iteratee` (迭代函数),
 * 调用`arrays`数组中的每个`array`的每个元素以产生唯一性计算的标准。
 * 结果值从该值出现的第一个 `array` 中选择。
 * iteratee 会传入一个参数:(value)。
 *
 * @since 4.0.0
 * @category Array
 * @param {...Array} [arrays] 要检查的arrays。
 * @param {Function} iteratee 每个元素调用的iteratee。
 * @returns {Array} 返回组合values后的新数组。
 * @see difference, union, unionWith, without, xor, xorBy
 * @example
 *
 * unionBy([2.1], [1.2, 2.3], Math.floor)
 * // => [2.1, 1.2]
 */
function unionBy(...arrays) {
  // 拿到iteratee参数并确保是个函数
  let iteratee = last(arrays);
  if (isArrayLikeObject(iteratee)) {
    iteratee = undefined;
  }
  // 调用baseUniq(baseFlatten(), iteratee)
  return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), iteratee);
}

export default unionBy;

unionWith

import baseFlatten from './.internal/baseFlatten.js';
import baseUniq from './.internal/baseUniq.js';
import isArrayLikeObject from './isArrayLikeObject.js';
import last from './last.js';

/**
 * 这个方法类似 `union`但是它接受一个 `comparator`比较器 ,
 * 调用比较 `arrays` 数组的每个 `array` 的每一个元素。
 * 结果值从该值出现的第一个 `array` 中选择。
 * comparator 调用时会传入2个参数: (arrVal, othVal)。
 *
 * @since 4.0.0
 * @category Array
 * @param {...Array} [arrays] The arrays to inspect.要检查的arrays。
 * @param {Function} [comparator] `comparator`比较器调用每个元素。
 * @returns {Array} 返回组合values后的新数组。
 * @see difference, union, unionBy, without, xor, xorBy
 * @example
 *
 * const objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]
 * const others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]
 *
 * unionWith(objects, others, isEqual)
 * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]
 */
function unionWith(...arrays) {
  // comparator是最后一个参数,并确保是一个函数
  let comparator = last(arrays);
  comparator = typeof comparator === 'function' ? comparator : undefined;
  // 调用baseUniq(baseFlatten(), undefined, comparator)
  return baseUniq(
    baseFlatten(arrays, 1, isArrayLikeObject, true),
    undefined,
    comparator
  );
}

export default unionWith;