欢迎关注前端早茶,与广东靓仔携手共同进阶
前端早茶专注前端,一起结伴同行,紧跟业界发展步伐~
导读
本文适用于以下三种读者
- 只想要了解一下虚拟列表
可阅读“实现一个简单的虚拟列表”之前的部分 - 想初步探究虚拟列表的具体实现
可重点阅读“实现一个简单的虚拟列表”中的方案一 - 想要深入研究和探讨如何在虚拟列表中解决列表项高度不固定的问题
可重点阅读“实现一个简单的虚拟列表”中的方案二与方案三
前言
工作中,我们经常会遇到列表项。如果列表项的数量比较多,很多情况下我们会采用分页加载的方式,来避免一次性加载大量的数据,造成页面的性能问题。
但是用户在分页加载浏览了大量数据之后,列表项也会逐渐增多,此时页面可能会存在卡顿的情况。亦或者是我们需要一次性加载大量的数据,将所有的数据一次性呈现到用户面前,而不是采用分页加载的方式,此时列表项的数量可能会非常庞大,造成页面的卡顿。
这次我们就来介绍一种虚拟列表的优化方法来解决数据量大的时候列表的性能问题。
什么是虚拟列表
虚拟列表是按需显示的一种技术,可以根据用户的滚动,不必渲染所有列表项,而只是渲染可视区域内的一部分列表元素的技术。

如图所示,当列表中有成千上万个列表项的时候,我们如果采用虚拟列表来优化。就需要只渲染可视区域( viewport )内的 item8 到 item15 这8个列表项。由于列表中一直都只是渲染8个列表元素,这也就保证了列表的性能。
虚拟列表组件

长列表的优化是一个一直以来都很棘手的非常复杂的问题,上图是 Antd Design 的List组件所建议的,推荐与 react-virtualized 组件结合使用来对长列表进行优化。
我们最好是使用一些现成的虚拟列表组件来对长列表进行优化,比较常见的有 react-virtualized 和 react-tiny-virtual-list 这两个组件,使用他们可以有效地对你的长列表进行优化。
react-tiny-virtual-list
react-tiny-virtual-list 是一个较为轻量的实现虚拟列表的组件,使用方便,其源码也只有700多行。下面是其官网给出的一个示例。
import React from 'react'; import {render} from 'react-dom'; import VirtualList from 'react-tiny-virtual-list'; const data = ['A', 'B', 'C', 'D', 'E', 'F', ...]; render( <VirtualList width='100%' height={600} itemCount={data.length} itemSize={50} // Also supports variable heights (array or function getter) renderItem={({index, style}) => <div key={index} style={style}> // The style property contains the item's absolute position Letter: {data[index]}, Row: #{index} </div> } />, document.getElementById('root') );
react-virtualized
在react生态中, react-virtualized作为长列表优化的存在已久, 社区一直在更新维护, 讨论不断, 同时也意味着这是一个长期存在的棘手问题。相对于轻量级的 react-tiny-virtual-list 来说, react-virtualized 则显得更为全面。
react-virtualized 提供了一些基础组件用于实现虚拟列表,虚拟网格,虚拟表格等等,它们都可以减小不必要的 dom 渲染。此外还提供了几个高阶组件,可以实现动态子元素高度,以及自动填充可视区等等。

在使用 Ant Design 的List组件的时候,官方也是推荐结合使用 react-virtualized 来对大数据列表进行优化。
实现一个简单的虚拟列表
我们已经清楚了虚拟列表的原理:只渲染可视区域内的一部分列表元素。那我们就使用虚拟列表的思想来实现一个简单的列表组件。此处,我们给出两种方案,均融合了分页下拉加载的方式。
方案一
第一种方案的dom结构如图
-
外层容器:设置height,overflow:scroll
-
滑动列表:绝对定位,然后用列表元素高度*列表元素数量计算出滑动列表高度
-
可视区域:动态计算可视区域在滑动列表中的偏移量,使用 translate3d 属性动态设置可视区域的偏移量,造成滑动的效果。
方案一原理图


这样做了以后,每次都只渲染了可视区域的几个 dom 元素,确实做到了对于大数据情况下的长列表的优化
但是,这里只是实现了列表元素固定高度的情况,对于高度不固定的列表,如何实现优化呢
import React from 'react'; // 应该接收的props: renderItem: Function<Promise>, getData:Function; height:string; itemHeight: string // 下滑刷新组件 class InfiniteTwo extends React.Component { constructor(props) { super(props); this.renderItem = props.renderItem this.getData = props.getData this.state = { loading: false, page: 1, showMsg: false, List: [], itemHeight: this.props.itemHeight || 0, start: 0, end: 0, visibleCount: 0 } } onScroll() { let { offsetHeight, scrollHeight, scrollTop } = this.refs.scrollWrapper; let showOffset = scrollTop - (scrollTop % this.state.itemHeight) const target = this.refs.scrollContent target.style.WebkitTransform = `translate3d(0, ${showOffset}px, 0)` this.setState({ start: Math.floor(scrollTop / this.state.itemHeight), end: Math.floor(scrollTop / this.state.itemHeight + this.state.visibleCount + 1) }) if(offsetHeight + scrollTop + 15 > scrollHeight){ if(!this.state.showMsg){ let page = this.state.page; page++; this.setState({ loading: true }) this.getData(page).then(data => { this.setState({ loading: false, page: page, List: data.concat(this.state.List), showMsg: data && data.length > 0 ? false : true }) }) } } } componentDidMount() { this.getData(this.state.page).then(data => { this.setState({ List: data }) // 初始化列表以后,也需要初始化一些参数 requestAnimationFrame(() =