大數據列表渲染系列(三)可變尺寸

上一節咱們實現了固定尺寸的虛擬列表,這一節咱們增長難度,實現一個不固定尺寸的虛擬列表。算法

需求

實現一個不固定大小尺寸的虛擬列表。
使用方式以下:緩存

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>
);

分析

想一想尺寸大小不固定 和 上一節的固定尺寸有那些異同?
考慮一下,咱們發現整個流程邏輯都是同樣的,除了計算 每一個元素定位的時候,由於尺寸不同,致使的計算方式不同。尺寸不一致要求咱們去遍歷累積計算每個元素真實的大小和位置。
簡單說就是在 固定尺寸的基礎實現上,更新一下 輔助計算函數。dom

實現原理

  1. 由於是不固定尺寸,因此須要從索引0開始計算 每一條數據對應的offset和size,這樣順序日後,就分紅了,已經計算過的 和 未計算過的。
  2. 已經計算過的用一個對象緩存下來,後續使用的使用,直接從緩存裏取用。
  3. 在onScroll滾動的事件裏,須要根據offset查找對應的startIndex的offset,這裏有兩種狀況,已經緩存過,那從緩存區間裏查找便可,能夠利用二分查找法提升搜索效率。若是沒有緩存過,那麼可使用 指數查找法縮小查找範圍區間,而後再用二分查找法搜索

實現

根絕上一節講的,咱們須要實現如下幾個輔助函數:函數

// 根據索引獲取 位置偏移
getItemOffset(index) {}

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

// 獲取預估總尺寸
getEstimatedTotalSize() {}

// 根據 滾動位置offset 獲取 數據區間開始 索引startIndex
getStartIndexForOffset(offset) {}

// 根據數據開始索引startIndex 獲取 數據區間 結束索引endIndex
getStopIndexForStartIndex() {}

先往實例上掛載一些屬性,用來緩存測量過的數據:rest

instance.instanceProps = {
      itemMetadataMap: {}, // 緩存對象
      estimatedItemSize: estimatedItemSize, // 每一項給出的默認size
      lastMeasuredIndex: -1, // 已領測量到的元素索引
};

而後咱們添加一個輔助方法,用來獲取每個item對應的信息,有緩存取緩存,沒有就計算保存,以下:code

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
  );
};
相關文章
相關標籤/搜索