简单写一个react组件吧----虚拟列表

参考:高性能渲染十万条数据(虚拟列表)

1. 应用场景

需要用列表的形式展示大量的数据,本文章只针对规则的、等高且固定高度的列表。如图:


虚拟列表

前端渲染大量数据时会造成页面卡顿,原因之一是渲染的DOM节点太多,而虚拟列表只渲染可见区域的DOM节点,极大的优化了渲染性能。

2. 思路

初次加载时,只渲染初始的一部分数据,页面滚动时,动态计算需要展示的数据和滚动的位置。为此,DOM的设计需要三个区域----容器、列表展示区域、支撑滚动条区域

  • 容器:包裹列表展示区和滚动条支撑区;
  • 列表展示区域:真实渲染的列表项区域,也就是可见的列表项部分;
  • 支撑滚动条区域:用于支撑容器的高度,使容器出现滚动条。
    样式的命名可以个性化一点......
<div class="jisl-container">
    <div class="phantom"></div>
    <div class="view">
      <!-- item-1 -->
      <!-- item-2 -->
      <!-- ...... -->
      <!-- item-n -->
    </div>
</div>

3. 代码实现

目录结构,新建一个文件夹,然后在文件夹中新建js和css文件

VirtualList\
    index.js
    style.css

在index.js中编辑代码

import React, {useState, useEffect, useRef} from 'react';
import './style.css';

const VirtualList = (props) => {
  const scrollRef = useRef(); // 滚动条ref

  const {
    data,       // 渲染的数据
    count,      // 列表的数量、长度
    size,       // 可视区渲染的列表项数量(真实DOM节点数量)
    viewSize,   // 可视区能看到的列表数量, 数值比size小, 即DOM比可见数量多, 具有缓冲作用
    rowHeight,  // 每一行列表项的高度
    renderNode, // 渲染的列表项DOM节点 
  } = props;

  const [startIndex, setStartIndex] = useState(0);       // 起始索引
  const [phantomHeight, setPhantomHeight] = useState(0); // 占位区的高度
  const [startOffset, setStartOffset] = useState(0);     // 渲染区域偏移量

  // 计算支撑滚动条区域的高度
  useEffect(() => {
    setPhantomHeight(rowHeight * count);
  }, [count, rowHeight])

  /**
   * 滚动时更新显示区域的数据和高度
   * @param {DOM.event} e 
   */
  const onScroll = e => {
    let scrollTop = e.target.scrollTop;               
    let offset = scrollTop - (scrollTop % rowHeight);
    let index = Math.floor(scrollTop / rowHeight); 
    setStartOffset(offset);
    setStartIndex(index);   
  }

  return (
      <div 
        className="jisl-container"
        style={
          (data && data.length > viewSize) || (data && data.length === 0)
          ? { height: rowHeight * viewSize } 
          : { height: rowHeight * data.length }
        }
        onScroll={onScroll}      
      >
        <div className="phantom" style={{height: phantomHeight}} />
        <div 
          className="view"
          style={{transform: `translateY(${startOffset}px)`}}                
        >      
          {
            data instanceof Array && data.length > 0
            ? data.slice(startIndex, startIndex + size).map((item, index) => {
                if(Object.prototype.toString.call(renderNode) !== '[object Function]') return; 
                return renderNode(data, item, index + startIndex);
              })
            : <div />
          }                
        </div>
      </div>
  )
}

export default VirtualList;

在style.css中编写样式

  // 外层容器 
  .jisl-container {
    position: relative;
    width: 100%;

    overflow-y: auto;
    overflow-x: hidden;
    background: #fff;
    box-shadow: 0 2px 5px -2px rgba(0,0,0,.05), 
                0 4px 10px 0 rgba(0,0,0,.08),
                0 6px 20px 4px rgba(0,0,0,.05);
  }

  // 支撑区域
  .phantom {
    width: 100%;
    background: #fff;
  }

  // 可视区列表项 
  .view {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 100%;
    background: #fff;
  } 
  • 外层容器设置overflow,只展示可见区域,并且position设置relative。每一个列表项高度rowHeight设置32px,显示数量viewSize设置5个,外层容器的高度为rowHeight * viewSize;
  • 支撑区域的高度固定,总的列表项数目是count,那么支撑区域高度为rowHeight * count;
  • 可视区域position设置absolute脱离文档流,然后计算偏移量,使用transform跟随滚动条移动位置;
  • 其中监听onscroll事件的逻辑最为关键
    单独截取出来
  /**
   * 滚动时更新显示区域的数据和高度
   * @param {DOM.event} e 
   */
  const onScroll = e => {
    let scrollTop = e.target.scrollTop;               
    let offset = scrollTop - (scrollTop % rowHeight);
    let index = Math.floor(scrollTop / rowHeight); 
    setStartOffset(offset);
    setStartIndex(index);   
  }

首先是获取当前滚动条的位置

  let scrollTop = e.target.scrollTop;    

滚动条位置变化时,计算渲染区域的偏移量

  let offset = scrollTop - (scrollTop % rowHeight);
  setStartOffset(offset);

计算展示的数据的索引

  let index = Math.floor(scrollTop / rowHeight); 
  setStartIndex(index);  

数组的slice方法不会改变原数组,所以渲染时直接用slice方法截取,size是渲染的DOM数量,size比可视区域的列表项viewSize大一点可以起到缓冲作用

data.slice(startIndex, startIndex + size)

封装好之后的使用方法如下

import React from 'react';

const test = () => {
  const data = ['这是一个数组'];  

  const renderNode = (data, item, index) => {
    return <div key={index}  onClick={() => console.log(data)}> { item } </div>
  }

  return (
    <VirtualList 
      data={ data }              // 总数据
      count={ data.length }      // 列表项数量
      size={ 8 }                 // 可视区渲染DOM的列表项数量
      viewSize={ 5 }             // 可视区能看到的列表数量
      rowHeight={ 32 }           // 每个列表项的行高度
      renderNode={ renderNode }  // 渲染的每个列表项
    />
  )
}

export default test;

结合下拉框使用的效果...


效果
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容