前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >mini react-window(一) 实现固定高度虚拟滚动

mini react-window(一) 实现固定高度虚拟滚动

原创
作者头像
测不准
发布于 2022-09-24 16:13:31
发布于 2022-09-24 16:13:31
2.1K00
代码可运行
举报
文章被收录于专栏:与前端沾边与前端沾边
运行总次数:0
代码可运行

我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据需要更新的场景(股票价格),会导致页面有很多计算和重绘,内存占用也会变多,这就需要我们对长列表处理进行优化。

长列表渲染

  • 海量数据渲染会有如下问题
    • 计算时间过长,用户等待时间长,体验差
    • CPU 处理时间过长,滑动过程可能卡顿
    • GPU 负载过高,渲染不过来会闪动
    • 内存占用过多,严重会引起浏览器卡死和崩溃
  • 优化
    • 下拉底部加载更多,实现赖加载,但是如果内容越来越多会引起大量重排和重绘
    • 虚拟列表,可视区域有限,看到的数据有限,在用户滚动时,指渲染可是区域内的内容即可,dom 少,渲染少

github 上也有很多针对 react 的虚拟滚动的库,我们这里对 react-window 的使用和实现,进行一下简单的学习分享,了解不同虚拟滚动场景下的使用方式和 react 的优秀封装,希望对你有帮助。

固定高度场景

这种场景中我们已知每一项的渲染高度,可以根据渲染个数计算出整体高度,我们只需要对可是区域内的渲染进行渲染计算即可。

image.png
image.png

由上图可知,我们定义可以区域的高度为 200px,每一项高度是 50px,那么我们只需要把所有的列表进行截取,只渲染中间的内容即可,上下超出的部分不参与绘制,可以提升性能。

使用事例

我们使用 create-react-app 创建项目,修改代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import FixedSizeList from './fixed-size-list'

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <FixedSizeList />
);
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/fixed-size-list
// 固定高度列表
import {FixedSizeList} from 'react-window'
import './fixed.css'

// 渲染的每一行的 item 项
function Row({index, style}) {
  return <div className={index % 2 ? 'odd': 'even'} style={style}>
    Row {index}
  </div>
}

function App() {
  // 可视区的宽高 200,每一项高度 50,列表总数 1000
  return <FixedSizeList className='list' height={200} width={200} itemSize={50} itemCount={1000}>
    {Row}
  </FixedSizeList>
}
export default App
代码语言:css
AI代码解释
复制
// src/fixed.css
.list {
  border: 1px solid gray;
}
.odd, .even {
  display: flex;
  align-items: center;
  justify-content: center;
}

.odd {
  background-color: pink;
}

.even {
  background-color: antiquewhite;
}

我们使用官方库效果如下:

image.png
image.png

我们可以看到可视区内展示 4 项,但是 dom 结构中展示了 6 项,这是因为列表在上下滑动的时候做了一个缓冲,避免滚动的时候有个白屏的效果,类似缓存。

那这里元素的定位为什么使用定位形式又使用 will-change 呢?这是使用了 will-change,让浏览器就可以提前知道哪些元素的属性将会改变,把元素提升到一个新层,提升性能,同时避免了重排重绘。

实现固定渲染虚拟滚动

  1. 创建自己实现组件的目录
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/react-window/index.js

export {default as FixedSizeList} from './FixedSizeList'
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/react-window/FixedSizeList.js

import createListComponet from './createListComponent'
// 传入组件的配置参数,返回一个组件
const FixedSizeList = createLstComponent({})

export default FixedSizeList

实现 FixedSizeList 组件时我们要注意我们没有直接写, react-window 提供了固定高度非固定高的等几种虚拟滚动场景,但是对于包裹元素来说基本都是一致的,只是具体的场景元素处理有不同,所以我们仿照官方库,先提供一个父组件,其他的具体场景的实现都是基于该父组件实现的,这种形式也就是我们说的高阶组件,就是这里的 createListComponent

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/react-window/createListComponent.js

import React from 'react'

function createListComponet({}) {
  return class extends React.Component {
    render() {
      // 这个类组件是返回的页面具体使用的那个组件,所以可以直接通过属性获取值
      const { width, height, itemCount, children: ComponentType } = this.props;
      // 我们根据上面的 dom 结构可以写出如下布局
      const containerStyle = {
        position: "relative",
        width,
        height,
        overflow: "auto",
        willChange: "transform",
      };
      const contentStyle = {
        width: '100%',
        height: ??? // 这里高度待定
      }
      const items = []
      // 如果有列表长度,进行每一项的处理,样式待定
      if (itemCount > 0) {
        // 这里我们现渲染所有的数据,稍后做截取处理
        for(let i = 0; i < itemCount; i++) {
          items.push(<ComponentType index={i} style={this.getItemStyle(i)} key={i} />)
        }
      }
      return <div style={containerStyle}>
        <div style={contentStyle}>{items}</div>
      </div>
    }
    
    // 每一项的样式
    getItemStyle =(i) => {
      const style = {
        position: "absolute",
        width: "100%",
        height: ???,
        top: ???,
      };

      return style;
    }
  }
}

上面的代码相信大家可以理解,我们对公共的样式结构进行了书写,同时对所有数据进行了渲染,这里有两处是空着的:

  • 内容高度和每一项元素样式

因为我们这里实现的固定高度场景,所以可知内容高度可以直接计算,但是其他的非固定高度场景不能够复用,所以这里我们使用传入的方式;同时每一项的样式的高度和 top 值也是需要具体场景单独计算。还记得 createLstComponent 方法可以接受参数,我们进行参数处理:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const FixedSizeList = createListComponent({
  getEstimatedTotalSize: ({ itemSize, itemCount }) => itemSize * itemCount, // 预计内容高度,固定高度直接相乘 就好
  getItemSize: ({ itemSize }) => itemSize, // 固定高度直接使用
  getItemOffset: ({ itemSize }, index) => itemSize * index // 因为元素是定位的,同时高度固定,所以 top 值可如此计算
});
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function createListComponent({
  getEstimatedTotalSize, // 估算内容高度
  getItemSize, // 每一项的高度
  getItemOffset, // 每一项的 top 值
}) {
....

contentStyle.height = getEstimatedTotalSize(this.props)

itemStyle.height = getItemSize(this.props)
itemStyle.top = getItemOffset(this.props, i)

实现效果如下,符合我们的预期:

image.png
image.png
  1. 实现可视区域内渲染 我们上面是直接对所有的列表进行了渲染,其实在可是区域外的数据,我们是不关心的,如果有数据更新也不应该进行渲染,因为我们看不到。所以我们要对渲染的截取索引进行处理。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
render() {
  ...
  if (itemCount > 0) {
    // 需要计算得出截取的索引
    const [startIndex, endIndex] = this.getRangeToRender()
    for (let i = startIndex; i <= endIndex; i++) {
      items.push(
        <ComponentType index={i} style={this.getItemStyle(i)} key={i} />
      );
    }
  }
  ...
}
state = {
  scrollOffset: 0, // 向上卷去的高度,就是我们说的滚动距离,scrollTop,默认 0
}
getRangeToRender = () => {
  const {scrollOffset} = this.state
  const {itemCount} = this.props
  // 索引的计算处理同样因为场景不同外部传入
  
  // 根据卷去高度计算开始索引
  const startIndex = getStartIndexForOffset(this.props, scrollOffset)
  // 根据开始索引计算 结束索引
  const endIndex = getEndIndexForOffset(this.props, startIndex)
  return [startIndex, endIndex]
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const FixedSizeList = createListComponent({
  ...
  // 开始索引我们需要向下取整,即使 item 滚动到一半,我们也要渲染
  getStartIndexForOffset: ({ itemSize }, offset) =>
    Math.floor(offset / itemSize),
  // 结束索引的计算为 开始索引 + 中间能展示的索引个数
  getEndIndexForOffset: ({ height, itemSize }, startIndex) =>
    startIndex + Math.ceil(height / itemSize) - 1 // 结束索引闭区间,所以 -1 (即算到了第八个,但是第八个其实是不展示的)
});

实现效果如下,可以看到我们只渲染了可是区域内能展示的数量

image.png
image.png

我们实现的滚动效果如下:

image.png
image.png

可以看到滚动不是很流畅,会有白屏,这就是为什么官方库会默认多两个元素的原因,预先渲染,避免白屏,我们继续优化;

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 定义需要预渲染的个数
static defaultProps = {
  overscanCount: 2, // 性能好可以多设置
}

getRangeToRender = () => {
  const {scrollOffset} = this.state
  const {overscanCount, itemCount} = this.props
  const startIndex = getStartIndexForOffset(this.props, scrollOffset)
  const endIndex = getEndIndexForOffset(this.props, startIndex)
  // 向下滚动要取最大值,向上滚动时要取最小值,需要跟索引临界值对比
  return [Math.max(0, startIndex - overscanCount), Math.min(itemCount - 1, endIndex + overscanCount)]
}

实现效果如下,可以看到滚动起来还是很流畅的,但是快速滚动还是有显示白屏的概率,可以增加 overscanCount 的值改善体验效果,但是现有的基本就够用了。

image.png
image.png

本小节我们实现了固定高度虚拟列表,代码不是很多,感兴趣的小伙伴可以自己动手实现自己的虚拟滚动库,下一小节我们继续实现其他场景下的滚动列表,如有问题欢迎留言讨论。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
JAVA以UTF-8导出CSV文件,用excel打开产生乱码的解决方法
1OutputStreamWriter osw = newOutputStreamWriter(resp.getOutputStream(), "UTF-8"); 
felixxue
2022/12/29
2K0
java怎么解决导出csv文件乱码
将查询的数据以xls文件导出时(UTF-8编码),数据正常;但以CSV文件导出时,文件中的中文乱码,同样是UTF-8编码,改成GBK编码导出时,中文显示正常。
Java架构师必看
2021/12/21
4.7K0
csv文件乱码
问题描述: 生成的csv文件,设置为UTF-8格式,在windows上用EXCEL打开的话会乱码,在linux上用vim或者cat打开查看正常;设置为GBK格式的话,在windows上用EXCEL打开正常,但在linux上乱码 解决方法: 在csv文件头部的最前面加bom BOM(Byte Order Mark),是 UTF编码方案里用于标识编码的标准标记,在 UTF-16里本来是 FF FE,变成 UTF-8就成了 EF BB BF。这个标记是可选的,因为 UTF8字节没有顺序,所以它可以被用来检测一个字
千往
2018/01/24
2.9K0
csv文件导出注意事项
默认情况下,在windows上用excel打开csv文件时,并不是按utf-8码解析的,就算代码里设置了写入字符串为utf-8字符集,也可能乱码。
菩提树下的杨过
2020/06/24
1.5K0
Java杂谈之BOM谜题
开发中做了一个导出CSV功能,本地通过wps测试都没有问题,但是测试人员测试的时候发现用excel打开中文表头会出现乱码现象,很奇怪的现象,用nodePad工具打开看也是正常的,但是用excel打开就是中文乱码,通过查找资料了解到是因为csv文件是utf-8编码的,但是没有增加bom头,这样就会导致在window环境下一些软件会用默认编码打开文件从而导致乱码问题,本文详细介绍从前端下载、后端读写如何解决该问题。
你呀不牛
2021/05/28
1.7K0
我也太牛了,解决了浏览器中,前台导出csv格式,UTF-8编码,且excek打开不乱码!
ExcellentExport.js的方法,利用base64下载文件。支持chrome ,opera,firefox. 于是决定拿来为我所用!
申君健
2018/09/21
5.1K0
WEB:字符集、编码、乱码 —— 看这篇就够了
ASCII(美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语,是现今最通用的单字节编码系统。
WEBJ2EE
2019/07/19
4.3K0
WEB:字符集、编码、乱码 —— 看这篇就够了
C#自动识别文件编码
在做导入微信商户后台退款数据时,无论怎么设置编码导出来都是乱码,后来在网上找了这个识别文件编码的代码,感觉不错。 最后识别出来是gb2312,看来我还是太渣了,只能吃土了,竟然忘记了这个编码。 下面,上代码。 1 /// <summary> 2 /// 用于取得一个文本文件的编码方式(Encoding)。 3 /// </summary> 4 public class TxtFileEncoder 5 { 6 pub
晓晨
2018/06/22
2.2K0
PHP file_get_contents 中文乱码解决方案记录
最近拉取了京东结算订单csv文件,结果发现在用file_get_contents获取内容的时候,中文出现了乱码,感觉京东这么大,这个技术问题他们帮忙解决才好吧,想想还是算了,自己动动手的问题。
猿哥
2019/08/06
3.6K0
PHP file_get_contents 中文乱码解决方案记录
win10 uwp 读取文本GBK错误
本文讲的是解决UWP文本GBK打开乱码错误,如何去读取GBK,包括网页GBK。最后本文给出一个方法追加文本。
林德熙
2018/09/18
1.4K0
把一个txt文件转化为带标题栏的Excel文档
public class Export { public static final String CSVNAME_COMPETITION="cpc.csv"; } public class CSV { public static final String ENDLINE = "\n"; public static final Map<String,String> HEADLINES=new HashMap<String,String>(); static{ //分别是Excel文档的第
用户5166556
2019/04/16
7550
[精选] PHP如何快速导出 百万级数据 到EXCEL
很多时候,因为数据统计,我们需要将数据库的数据导出到Excel等文件中,以供数据人员进行查看,如果数据集不大,其实很容易;但是如果对于大数集的导出,将要考虑各种性能的问题,这里以导出数据库一百万条数据为例,导出时间不过20秒,值得学习的一种大数据导出方式。
码农编程进阶笔记
2022/05/24
2K0
解释BOM头和去掉方法
类似WINDOWS自带的记事本等软件,在保存一个以UTF-8编码的文件时,会在文件开始的地方插入三个不可见的字符(0xEF 0xBB 0xBF,即BOM)。它是一串隐藏的字符,用于让记事本等编辑器识别这个文件是否以UTF-8编码。对于一般的文件,这样并不会产生什么麻烦。但对于 PHP来说,BOM是个大麻烦。
用户7657330
2020/08/14
1.1K0
java中IO写文件工具类
下面是一些根据常用java类进行组装的对文件进行操作的类,平时,我更喜欢使用Jodd.io中提供的一些对文件的操作类,里面的方法写的简单易懂。 其中jodd中提供的JavaUtil类中提供的方法足够我们使用,里面的方法写的非常简练,例如append,read等方法,封装更好,更符合面向对象, 这里面我写的一些方法可多都是模仿jodd,从里面进行抽取出来的。 /** * 获取路径文件夹下的所有文件 * @param path * @return */ public static Fi
用户5166556
2019/04/16
8750
Java进阶-IO(2)
话接上回,继续java IO部分的学习。上一次说完了字节流的读写数据,这次介绍一下字符流的读写数据。
reload
2024/03/03
2120
PHP Bom头导致乱码或者其他错误
转载至:https://www.cnblogs.com/wt645631686/p/6868826.html
黄啊码
2020/05/29
1.1K0
Java基础-21(01)总结字符流,IO流编码问题,实用案例必做一遍
1:字符流(掌握) // 字节流读取中文可能出现的小问题(所以用字符流输入输出中文) package cn.itcast_01; import java.io.FileInputStream; import java.io.IOException; /* * 字节流读取中文可能出现的小问题: */ public class FileInputStreamDemo { public static void main(String[] args) throws IOException { // 创建字
Java帮帮
2018/03/16
1.2K0
java基础io流——字符流的变革(深入浅出)
在io流里,先诞生了字节流,但是字节流读取数据会有乱码的问题(读中文会乱码)。比如:
100000860378
2018/09/13
5200
java基础io流——字符流的变革(深入浅出)
转换流实现了字节流和字符流之间的互相转换_java输出流输出文件
转换流也是继承自超类Writer,因此可以使用Writer中的共性方法:write(int c),write(char[] ch),…,flash(),close(),…
全栈程序员站长
2022/10/03
4280
转换流实现了字节流和字符流之间的互相转换_java输出流输出文件
&#65279导致页面顶部空白一行解决方法
模板文件生成html文件之后会在body开头处加入一个可见的控制符&#65279,导致页面头部会出现一个空白行。原因是页面的编码是UTF-8 + BOM。
OECOM
2020/07/01
1K0
推荐阅读
相关推荐
JAVA以UTF-8导出CSV文件,用excel打开产生乱码的解决方法
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档