背景

因为需要进行排序,antd 的 Table 组件,排序不能过页,所以直接不分页。
列表展示人数近千条,初次加载卡顿,觉得 dom 太多,渲染费时间,决定用虚拟列表

解决方式

  1. 自行实现 virtualList 组件,花挺多时间,选用第 2 种
  2. 使用 react-virtualized 组件库,由于该库没有拖拽功能,自行添加 sortable.js 实现拖拽功能,由于 sortable.js 自定义排序不太好实现,于是另找方法,react-sortable-hoc 有基于 react-virtualized 实现虚拟列表
  3. 最后采用 react-sortable-hoc 和 react-virtualized 结合的方式实现这一功能,解决了卡顿的问题

    虚拟列表实现原理

    虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。
  • 计算当前可视区域起始数据索引(startIndex)
  • 计算当前可视区域结束数据索引(endIndex)
  • 计算当前可视区域的数据,并渲染到页面中
  • 计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上

虚拟列表 - 图1
由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:

  1. <div class="infinite-list-container">
  2. <div class="infinite-list-phantom"></div>
  3. <div class="infinite-list">
  4. <!-- item-1 -->
  5. <!-- item-2 -->
  6. <!-- ...... -->
  7. <!-- item-n -->
  8. </div>
  9. </div>
  • infinite-list-container 为可视区域的容器
  • infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条
  • infinite-list 为列表项的渲染区域

接着,监听 infinite-list-container 的 scroll 事件,获取滚动位置 scrollTop

  • 假定可视区域高度固定,称之为 screenHeight
  • 假定列表每项高度固定,称之为 itemSize
  • 假定列表数据称之为 listData
  • 假定当前滚动位置称之为 scrollTop

则可推算出:

  • 列表总高度 listHeight = listData.length * itemSize
  • 可显示的列表项数 visibleCount = Math.ceil(screenHeight / itemSize)
  • 数据的起始索引 startIndex = Math.floor(scrollTop / itemSize)
  • 数据的结束索引 endIndex = startIndex + visibleCount
  • 列表显示数据为 visibleData = listData.slice(startIndex,endIndex)

当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

  • 偏移量startOffset = scrollTop - (scrollTop % itemSize);

使用效果

react-virtualized 效果 与 直接用 antd 效果相比
数据总数:200条

虚拟列表的 Tree

初次渲染 20 条数据,使用时间 15 ~ 25 ms

  1. import { Column, Table } from 'react-virtualized';
  2. import { useState, useLayoutEffect } from 'react'
  3. import 'react-virtualized/styles.css';
  4. const columns = [
  5. {
  6. dataIndex: "url",
  7. title: '相貌'
  8. },
  9. {
  10. dataIndex: "name",
  11. title: '名称'
  12. },
  13. {
  14. dataIndex: "class",
  15. title: '班级'
  16. },
  17. {
  18. dataIndex: "age",
  19. title: '年龄'
  20. },
  21. {
  22. dataIndex: "grade",
  23. title: '成绩'
  24. }
  25. ]
  26. let time = 0;
  27. function VirtualTable(props) {
  28. const [data, setData] = useState([]);
  29. useLayoutEffect(() => {
  30. setData(mockData); // mockData 为数组,有 200 条数据
  31. time = new Date().valueOf();
  32. }, [])
  33. useLayoutEffect(() => {
  34. if (data.length !== 0) {
  35. console.log(new Date().valueOf() - time);
  36. }
  37. }, [data])
  38. return (
  39. <Table
  40. width={300}
  41. height={300}
  42. headerHeight={20}
  43. rowHeight={30}
  44. rowCount={data.length}
  45. rowGetter={({ index }) => data[index]}
  46. >
  47. {
  48. columns.map(item => <Column key={item.dataIndex} label={item.title} dataKey={item.dataIndex} width={250} />)
  49. }
  50. </Table>
  51. )
  52. }

使用 antd 的 Tree

初次渲染 200 条数据,使用时间 141 ~ 145ms
初次渲染 1000 条数据,使用时间 465 ~ 545ms

  1. import { Table } from 'antd';
  2. import { useState, useLayoutEffect } from 'react'
  3. // antd 普通列表
  4. let time = 0;
  5. const columns = [
  6. {
  7. dataIndex: "url",
  8. title: '相貌'
  9. },
  10. {
  11. dataIndex: "name",
  12. title: '名称'
  13. },
  14. {
  15. dataIndex: "class",
  16. title: '班级'
  17. },
  18. {
  19. dataIndex: "age",
  20. title: '年龄'
  21. },
  22. {
  23. dataIndex: "grade",
  24. title: '成绩'
  25. }
  26. ]
  27. function NormalTable(props) {
  28. const [data, setData] = useState([]);
  29. useLayoutEffect(() => {
  30. setData(mockData);
  31. time = new Date().valueOf();
  32. }, [])
  33. useLayoutEffect(() => {
  34. if (data.length !== 0) {
  35. console.log(new Date().valueOf() - time);
  36. }
  37. }, [data])
  38. return (
  39. <Table
  40. columns={columns}
  41. dataSource={data}
  42. pagination={{
  43. defaultPageSize: 200
  44. }}
  45. />
  46. )
  47. }

React 自行实现虚拟列表

只实现了虚拟列表,未实现拖拽换行。

基于 onscroll 方法

外部传给虚拟列表组件:

  1. 列表每一项的高度 itemHeight
  2. 列表每一项的数据:listData
  3. 虚拟列表容器可视高度:screenHeight

实现虚拟列表的思路:
监听 infinite-list-container 的 onscoll ,获取已滚动高度 scrollTop

  1. 改变起始索引 start
  2. 结束索引 end 由 start 派生:end = Math.floor(screenHeight / itemHeight) + start
  3. infinite-list的偏移量 translateY:-listHeight + scrollTop
    1. 列表总长度:listHeight = listData.length * itemHeight
      1. .infinite-list-container {
      2. overflow: auto;
      3. }
      ```jsx import React, { useState, useMemo, useRef, useEffect } from ‘react’; import ‘./index.css’

const VirtualList = (props) => { const { listData, itemHeight, screenHeight, } = props

const containerRef = useRef() // 列表总高度 const listHeight = useMemo(() => { return listData.length * itemHeight }, [listData])

// 起始索引 const [start, setStart] = useState(0)

// 结束索引 const end = useMemo(() => { return Math.floor(screenHeight / itemHeight) + start }, [start])

const [transform, setTransform] = useState(‘’)

useEffect(() => { const { current: listContainer } = containerRef setTransform(translateY(${-listHeight + listContainer.scrollTop}px)) }, [start])

useEffect(() => { const { current: listContainer } = containerRef if (listContainer) { listContainer.onscroll = function () { // 修改起始索引 setStart(Math.floor(listContainer.scrollTop / itemHeight)) } } }, [])

return (

{ listData.slice(start, end) .map((item, index) =>
{item.node}
) }
) }

export default VirtualList;

  1. ```jsx
  2. const listData = []
  3. for (let i = 0; i < 300; i++) {
  4. listData.push({ node: i })
  5. }
  6. <VirtualList
  7. listData={listData}
  8. itemHeight={30}
  9. screenHeight={500}
  10. />

基于 IntersectionObserver 方法

参考资料

「前端进阶」高性能渲染十万条数据(虚拟列表)
react-virtualized的API文档
基于 react-virtualized 的 react-sortable-hoc 实现虚拟列表
sortable.js 的 github 仓库