浅谈一波虚拟列表

95 阅读3分钟

问题描述

  • 在我们一个后台向外提供接口的系统中,需要为接口提供配置参数,这些参数的格式基本就是对象格式,之前采用的方案是通过可编辑的表格来实现,每一行支持编辑和修改,支持添加子节点,但时候发现有些参数的嵌套层级特别深,要有6~7层,就会导致表单的元素的数量特别多,在这种情况下编辑表格,每输入一个字符,页面就会卡顿2~3秒。

问题分析

  • 经过分析发现,当在输入框中输入时,页面会重新渲染整个Form表单,所以在数据量特别大的时候就会js就会画更长的时间处理渲染列表,主线程就会长时间被占用,阻塞了其他任务的执行,比如用户输入、动画、滚动,引起UI渲染和响应延迟,导致页面卡顿。
  • 底层antDesign的Forn表单问题实现问题,antdesign3的Rc-Form和antDesign4的RC-From实现方式不同,antDesign3的form表单onchange函数一出发,就会刷新整个页面,导致页面卡顿。所以问题转化成了如何解决大数据树状的性能问题

解决方案

  • Form分割:一般对长列表,可以采用form分割的形式,如果Table中每一行都是独立的Form,问题就解决了,但是Table表格中的渲染元素时在定义Table的Column时中使用Form.item定义的,并不能按照列进行分割。
  • 虚拟列表:根据react-virtualized三方依赖库,自己开发一个虚拟列表。但是开发成本较高,使用三方包React Rsuite ,重构整个表格逻辑代码,将普通表格优化为虚拟列表。
  • 虚拟列表的原理:
    • 首先,根据可视区域的高度,以及每一行的高度计算出要渲染的节点数量。为了避免延迟加载页面会出现白屏闪烁的现象,可以在上下各流出一屏的缓存。
    • 其次,根据单行元素的高度和数据量计算出元素容器的高度,得到每一行的占位符,此时元素容器会出现滚动条,检测元素同期的滚动条事件。
    • 其次,根据scrollTop计算出滚动条中的滚轮距离顶部的距离,计算出当前可是区域应该显示数据开始位置以及结束为止,得到目前需要渲染的数据量。
    • 最后,计算出每一个元素具体容器顶部的位置,得到需要渲染的元素在容器中的位置。

实现简单的虚拟列表

test.gif

/** @format */  
  
![test.gif](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/88a68020ee834d6f9d99c0994b4a118b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY2F0c2F5ZXI=:q75.awebp?rk3s=f64ab15b&x-expires=1770621157&x-signature=zffOfbM1NfgZ5P2vf1UvvcAFaj8%3D)
import React, { useState, useRef, useEffect } from 'react';  
  
const ITEM_HEIGHT = 20// 每项的固定高度  
const VISIBLE_ITEM_COUNT = 20// 可见项目数  
  
const VirtualList = ({ items }) => {  
  const containerRef = useRef(null);  
  const initItems = items;  
  const [visibleItems, setVisibleItems] = useState([]);  
  const [scrollTop, setScrollTop] = useState(0);  
  
  useEffect(() => {  
    const handleScroll = () => {  
      console.log('handleScroll');  
      const container = containerRef.current;  
      if (container) {  
        console.log('Container height:', container.clientHeight); // 容器可见区域的高度  
        console.log('Scroll height:', container.scrollHeight);  
        const scrollTop = container.scrollTop;  
        setScrollTop(scrollTop);  
        console.log('scrollTop', scrollTop);  
        // 计算视口内的起始和结束索引  
        const startIdx = Math.floor(scrollTop / ITEM_HEIGHT);  
  
        const endIdx = Math.min(  
          items.length - 1,  
          startIdx + VISIBLE_ITEM_COUNT,  
        );  
        // console.log(visibleItems);  
        // console.log('startIdx', startIdx);  
        // console.log('endIdx', endIdx);  
        // console.log('setVisibleItems', items.slice(startIdx, endIdx + 1));  
        setVisibleItems(items.slice(startIdx, endIdx + 1));  
      }  
    };  
  
    const container = containerRef.current;  
    if (container) {  
      container.addEventListener('scroll', handleScroll);  
      handleScroll(); // 初始化时计算可见项  
    }  
  
    return () => {  
      if (container) {  
        container.removeEventListener('scroll', handleScroll);  
      }  
    };  
  }, [items]);  
  
  const containerHeight = items.length * ITEM_HEIGHT// 列表总高度  
  
  return (  
    

  
      

{'scrollTop:' + scrollTop + ''}

  
      

        ref={containerRef}  
        style={{  
          height'400px',  
          overflowY'auto',  
          position'relative',  
          border'1px solid #ddd',  
        }}  
      >  
        

          style={{  
            height: containerHeight,  
            position'relative',  
            backgroundColor'yellow',  
          }}  
        >  
          {visibleItems.map((item, index) => (  
            

              key={index}  
              style={{  
                position'absolute',  
                top: items.indexOf(item) * ITEM_HEIGHT + 'px',  
                heightITEM_HEIGHT + 'px',  
                width'100%',  
                boxSizing'border-box',  
                borderBottom'1px solid #ddd',  
              }}  
            >  
              {`${item} top:${  
                initItems.indexOf(item) * ITEM_HEIGHT   
              }px  ${initItems.indexOf(item)}    ${ITEM_HEIGHT}`}  
            

  
          ))}  
          
        
      
  );  
};  
  
export default VirtualList;