虚拟列表

按需显示的一种实现,只对可见区域(视口 View port)进行渲染,对非可见区域中的数据不渲染或部分渲染(作为 Buffer)的技术。目的是达到极高的渲染性能,提升用户体验。

原理

模型

虚拟列表分为

  1. VirtualList 虚拟列表区域
  2. VisibleRange 视口区域
  3. RenderRange(PreScanRange + VisibleRange + PostScanRange) 渲染区域
  • RenderRange = PreScanRange + VisibleRange + PostScanRange = stop - start
  1. LoadedRange 已加载区域
  • LoadedRange = lastMeasureIndex
  1. UnloadedRange 未加载区域
  • UnloadedRange = VisibleRange.size - lastMeasureIndex

实际滚动过程中需要不断更新 start、stop、lastMeasureIndex,从而得出渲染区域和视口区域
image.png

位置缓存

LoadedRange 中的元素已经得到计算的尺寸、偏移缓存哈希中,用以减少重复计算,以提高性能。
image.png

查找指定元素

基于有序元素偏移列表

  • LoadedRange 中使用二分查找算法,时间复杂度可降低到 O(lgN);
  • UnloadedRange 中使用指数查找算法,时间复杂度同样为 O(lgN), 但可以将 N 值缩小,进一步提高搜索性能

image.png
先用指数搜索定位范围 low - heigh,内嵌二分查找 target

滚动偏移计算

VirtualList 总长度
TotalSize = lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size + ( itemCount - lastMeasuredIndex - 1 ) * estimatedItemSize
使用已经计算的偏移尺寸,加上未加载的预估尺寸,即基本等价于整体尺寸

不定尺寸子元素

  • 静态计算;提前预知所有元素的尺寸,并通过数组类型的 prop 传入,每个列表项的高度通过索引来获得
  • 动态延迟计算:传入获取列表项高度的方法,给这个方法传入 item 和 index,返回对应列表项的高度

将上面的方式得出尺寸作为计算依据,若无法获取对应尺寸,则使用预估尺寸。

React-virtual

Auto-sizing, buttery smooth, headless virtualization… with just one hook.

https://react-virtual.tanstack.com/docs/api

  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3. import './index.css'
  4. import { useVirtual } from 'react-virtual'
  5. function App() {
  6. const rows = new Array(10000)
  7. .fill(true)
  8. .map(() => 25 + Math.round(Math.random() * 100))
  9. const columns = new Array(10000)
  10. .fill(true)
  11. .map(() => 75 + Math.round(Math.random() * 100))
  12. return (
  13. <div>
  14. <p>
  15. These components are using <strong>dynamic</strong> sizes. This means
  16. that each element's exact dimensions are unknown when rendered. An
  17. estimated dimension is used to get an a initial measurement, then this
  18. measurement is readjusted on the fly as each element is rendered.
  19. </p>
  20. <br />
  21. <br />
  22. <h3>Rows</h3>
  23. <RowVirtualizerDynamic rows={rows} columns={columns} />
  24. <br />
  25. <br />
  26. <h3>Columns</h3>
  27. <ColumnVirtualizerDynamic rows={rows} columns={columns} />
  28. <br />
  29. <br />
  30. <h3>Grid</h3>
  31. <GridVirtualizerDynamic rows={rows} columns={columns} />
  32. </div>
  33. )
  34. }
  35. function RowVirtualizerDynamic({ rows }) {
  36. const parentRef = React.useRef()
  37. const rowVirtualizer = useVirtual({
  38. size: rows.length,
  39. parentRef,
  40. })
  41. return (
  42. <>
  43. <div
  44. ref={parentRef}
  45. className="List"
  46. style={{
  47. height: `200px`,
  48. width: `400px`,
  49. overflow: 'auto',
  50. }}
  51. >
  52. <div
  53. style={{
  54. height: rowVirtualizer.totalSize,
  55. width: '100%',
  56. position: 'relative',
  57. }}
  58. >
  59. {rowVirtualizer.virtualItems.map(virtualRow => (
  60. <div
  61. key={virtualRow.index}
  62. ref={virtualRow.measureRef}
  63. className={virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'}
  64. style={{
  65. position: 'absolute',
  66. top: 0,
  67. left: 0,
  68. width: '100%',
  69. transform: `translateY(${virtualRow.start}px)`,
  70. }}
  71. >
  72. <div style={{ height: rows[virtualRow.index] }}>
  73. Row {virtualRow.index}
  74. </div>
  75. </div>
  76. ))}
  77. </div>
  78. </div>
  79. </>
  80. )
  81. }
  82. function ColumnVirtualizerDynamic({ columns }) {
  83. const parentRef = React.useRef()
  84. const columnVirtualizer = useVirtual({
  85. horizontal: true,
  86. size: columns.length,
  87. parentRef,
  88. })
  89. return (
  90. <>
  91. <div
  92. ref={parentRef}
  93. className="List"
  94. style={{
  95. width: `400px`,
  96. height: `100px`,
  97. overflow: 'auto',
  98. }}
  99. >
  100. <div
  101. style={{
  102. width: columnVirtualizer.totalSize,
  103. height: '100%',
  104. position: 'relative',
  105. }}
  106. >
  107. {columnVirtualizer.virtualItems.map(virtualColumn => (
  108. <div
  109. key={virtualColumn.key}
  110. ref={virtualColumn.measureRef}
  111. className={
  112. virtualColumn.index % 2 ? 'ListItemOdd' : 'ListItemEven'
  113. }
  114. style={{
  115. position: 'absolute',
  116. top: 0,
  117. left: 0,
  118. height: '100%',
  119. transform: `translateX(${virtualColumn.start}px)`,
  120. }}
  121. >
  122. <div style={{ width: columns[virtualColumn.index] }}>
  123. Column {virtualColumn.index}
  124. </div>
  125. </div>
  126. ))}
  127. </div>
  128. </div>
  129. </>
  130. )
  131. }
  132. function GridVirtualizerDynamic({ rows, columns }) {
  133. const parentRef = React.useRef()
  134. const rowVirtualizer = useVirtual({
  135. size: rows.length,
  136. parentRef,
  137. })
  138. const columnVirtualizer = useVirtual({
  139. horizontal: true,
  140. size: columns.length,
  141. parentRef,
  142. })
  143. const [show, setShow] = React.useState(true)
  144. const halfWay = Math.floor(rows.length / 2)
  145. return (
  146. <>
  147. <button onClick={() => setShow(old => !old)}>Toggle</button>
  148. <button onClick={() => rowVirtualizer.scrollToIndex(halfWay)}>
  149. Scroll to index {halfWay}
  150. </button>
  151. <button onClick={() => rowVirtualizer.scrollToIndex(rows.length - 1)}>
  152. Scroll to index {rows.length - 1}
  153. </button>
  154. {show ? (
  155. <div
  156. ref={parentRef}
  157. className="List"
  158. style={{
  159. height: `400px`,
  160. width: `500px`,
  161. overflow: 'auto',
  162. }}
  163. >
  164. <div
  165. style={{
  166. height: rowVirtualizer.totalSize,
  167. width: columnVirtualizer.totalSize,
  168. position: 'relative',
  169. }}
  170. >
  171. {rowVirtualizer.virtualItems.map(virtualRow => (
  172. <React.Fragment key={virtualRow.key}>
  173. {columnVirtualizer.virtualItems.map(virtualColumn => (
  174. <div
  175. key={virtualColumn.key}
  176. ref={el => {
  177. virtualRow.measureRef(el)
  178. virtualColumn.measureRef(el)
  179. }}
  180. className={
  181. virtualColumn.index % 2
  182. ? virtualRow.index % 2 === 0
  183. ? 'ListItemOdd'
  184. : 'ListItemEven'
  185. : virtualRow.index % 2
  186. ? 'ListItemOdd'
  187. : 'ListItemEven'
  188. }
  189. style={{
  190. position: 'absolute',
  191. top: 0,
  192. left: 0,
  193. transform: `translateX(${virtualColumn.start}px) translateY(${virtualRow.start}px)`,
  194. }}
  195. >
  196. <div
  197. style={{
  198. height: rows[virtualRow.index],
  199. width: columns[virtualColumn.index],
  200. }}
  201. >
  202. Cell {virtualRow.index}, {virtualColumn.index}
  203. </div>
  204. </div>
  205. ))}
  206. </React.Fragment>
  207. ))}
  208. </div>
  209. </div>
  210. ) : null}
  211. <br />
  212. <br />
  213. {process.env.NODE_ENV === 'development' ? (
  214. <p>
  215. <strong>Notice:</strong> You are currently running React in
  216. development mode. Rendering performance will be slightly degraded
  217. until this application is build for production.
  218. </p>
  219. ) : null}
  220. </>
  221. )
  222. }
  223. ReactDOM.render(
  224. <React.StrictMode>
  225. <App />
  226. </React.StrictMode>,
  227. document.getElementById('root')
  228. )