简单写一个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;

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


效果
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容