2

上一节我们实现了固定尺寸的虚拟列表,这一节我们增加难度,实现一个不固定尺寸的虚拟列表。

需求

实现一个不固定大小尺寸的虚拟列表。
使用方式如下:

const rowHeights = new Array(1000)
  .fill(true)
  .map(() => 25 + Math.round(Math.random() * 50));
 
const getItemSize = index => rowHeights[index];
 
const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);
 
const Example = () => (
  <List
    height={150}
    itemCount={1000}
    itemSize={getItemSize}
    width={300}
  >
    {Row}
  </List>
);

分析

想想尺寸大小不固定 和 上一节的固定尺寸有那些异同?
考虑一下,我们发现整个流程逻辑都是一样的,除了计算 每个元素定位的时候,因为尺寸不一样,导致的计算方式不一样。尺寸不一致要求我们去遍历累积计算每一个元素真实的大小和位置。
简单说就是在 固定尺寸的基础实现上,更新一下 辅助计算函数。

实现原理

  1. 因为是不固定尺寸,所以需要从索引0开始计算 每一条数据对应的offset和size,这样顺序往后,就分成了,已经计算过的 和 未计算过的。
  2. 已经计算过的用一个对象缓存下来,后续使用的使用,直接从缓存里取用。
  3. 在onScroll滚动的事件里,需要根据offset查找对应的startIndex的offset,这里有两种情况,已经缓存过,那从缓存区间里查找即可,可以利用二分查找法提高搜索效率。如果没有缓存过,那么可以使用 指数查找法缩小查找范围区间,然后再用二分查找法搜索

实现

根绝上一节讲的,我们需要实现以下几个辅助函数:

// 根据索引获取 位置偏移
getItemOffset(index) {}

// 根据索引获取 元素尺寸大小
getItemSize(index) {}

// 获取预估总尺寸
getEstimatedTotalSize() {}

// 根据 滚动位置offset 获取 数据区间开始 索引startIndex
getStartIndexForOffset(offset) {}

// 根据数据开始索引startIndex 获取 数据区间 结束索引endIndex
getStopIndexForStartIndex() {}

先往实例上挂载一些属性,用来缓存测量过的数据:

instance.instanceProps = {
      itemMetadataMap: {}, // 缓存对象
      estimatedItemSize: estimatedItemSize, // 每一项给出的默认size
      lastMeasuredIndex: -1, // 已领测量到的元素索引
};

然后我们添加一个辅助方法,用来获取每一个item对应的信息,有缓存取缓存,没有就计算保存,如下:

getItemMetadata(props, index, instanceProps) {
  const { itemSize } = props;
  const { itemMetadataMap, lastMeasuredIndex } = instanceProps;
  // itemMetadataMap缓存 每一项的size 以及偏移
  if (index > lastMeasuredIndex) {
    let offset = 0; // 默认,第一个元素偏移0

    // 初始化获取offset,下面for循环的基准值
    if (lastMeasuredIndex >= 0) {
      const itemMetadata = itemMetadataMap[lastMeasuredIndex];
      offset = itemMetadata.offset + itemMetadata.size;
    }
    for (let i = lastMeasuredIndex + 1; i <= index; i++) {
      let size = itemSize(i);
      itemMetadataMap[i] = {
        offset,
        size,
      };
      offset += size;
    }
    instanceProps.lastMeasuredIndex = index;
  }
  return itemMetadataMap[index];
}

然后逐个实现上述辅助函数

getItemOffset && getItemSize

// 根据索引获取 位置偏移
getItemOffset: (index) => getItemMetadata(props, index, instanceProps).offset

// 根据索引获取 元素尺寸大小
getItemSize: (index) =>
    instanceProps.itemMetadataMap[index].size

getEstimatedTotalSize

// 使用已经缓存过得精确数据 + 未测量的预估数据
const getEstimatedTotalSize = (
  { itemCount },
  { itemMetadataMap, estimatedItemSize, lastMeasuredIndex }
) => {
  let totalSizeOfMeasuredItems = 0;

  if (lastMeasuredIndex >= 0) {
    const itemMetadata = itemMetadataMap[lastMeasuredIndex];
    totalSizeOfMeasuredItems = itemMetadata.offset + itemMetadata.size;
  }

  const numUnmeasuredItems = itemCount - lastMeasuredIndex - 1;
  const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedItemSize;

  return totalSizeOfMeasuredItems + totalSizeOfUnmeasuredItems;
};

getStartIndexForOffset

getStartIndexForOffset: (props, offset, instanceProps) =>
    findNearestItem(props, instanceProps, offset)

这里需要着重说明一下,搜索算法:

const findNearestItem = (props, instanceProps, offset) => {
  const { itemMetadataMap, lastMeasuredIndex } = instanceProps;
  // 获取已经测量过的最后一个元素的offset偏移
  const lastMeasuredItemOffset =
    lastMeasuredIndex > 0 ? itemMetadataMap[lastMeasuredIndex].offset : 0;

  if (lastMeasuredItemOffset >= offset) {
    // 查询目标在 已经测量过的范围内,直接使用二分查找算法
    return findNearestItemBinarySearch(
      props,
      instanceProps,
      lastMeasuredIndex,
      0,
      offset
    );
  } else {
    // 查询目标在未测量区,使用指数查找内嵌二分查找
    // 指数查找主要是避免搜索计算整个数据区间
    return findNearestItemExponentialSearch(
      props,
      instanceProps,
      Math.max(0, lastMeasuredIndex),
      offset
    );
  }
};

// 二分查找算法的实现,没什么好讲的。
const findNearestItemBinarySearch = (
  props,
  instanceProps,
  high,
  low,
  offset
) => {
  while (low <= high) {
    const middle = low + Math.floor((high - low) / 2);
    const currentOffset = getItemMetadata(props, middle, instanceProps).offset;

    if (currentOffset === offset) {
      return middle;
    } else if (currentOffset < offset) {
      low = middle + 1;
    } else if (currentOffset > offset) {
      high = middle - 1;
    }
  }

  if (low > 0) {
    return low - 1;
  } else {
    return 0;
  }
};

// 指数查找 算法,没什么好说的。
const findNearestItemExponentialSearch = (
  props,
  instanceProps,
  index,
  offset
) => {
  const { itemCount } = props;
  let interval = 1;

  while (
    index < itemCount &&
    getItemMetadata(props, index, instanceProps).offset < offset
  ) {
    index += interval;
    interval *= 2;
  }

  return findNearestItemBinarySearch(
    props,
    instanceProps,
    Math.min(index, itemCount - 1),
    Math.floor(index / 2),
    offset
  );
};

donglegend
910 声望82 粉丝

长安的风何时才能吹到边梁?


引用和评论

0 条评论