上一節咱們實現了固定尺寸的虛擬列表,這一節咱們增長難度,實現一個不固定尺寸的虛擬列表。算法
實現一個不固定大小尺寸的虛擬列表。
使用方式以下:緩存
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
根絕上一節講的,咱們須要實現如下幾個輔助函數:函數
// 根據索引獲取 位置偏移 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: (index) => getItemMetadata(props, index, instanceProps).offset // 根據索引獲取 元素尺寸大小 getItemSize: (index) => instanceProps.itemMetadataMap[index].size
// 使用已經緩存過得精確數據 + 未測量的預估數據 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: (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 ); };