歡迎關注前端早茶,與廣東靚仔攜手共同進階
前端早茶專注前端,一起結伴同行,緊跟業界發展步伐~
導讀
本文適用於以下三種讀者
- 只想要了解一下虛擬列表
可閱讀“實現一個簡單的虛擬列表”之前的部分 - 想初步探究虛擬列表的具體實現
可重點閱讀“實現一個簡單的虛擬列表”中的方案一 - 想要深入研究和探討如何在虛擬列表中解決列表項高度不固定的問題
可重點閱讀“實現一個簡單的虛擬列表”中的方案二與方案三
前言
工作中,我們經常會遇到列表項。如果列表項的數量比較多,很多情況下我們會采用分頁加載的方式,來避免一次性加載大量的數據,造成頁面的性能問題。
但是用戶在分頁加載瀏覽了大量數據之后,列表項也會逐漸增多,此時頁面可能會存在卡頓的情況。亦或者是我們需要一次性加載大量的數據,將所有的數據一次性呈現到用戶面前,而不是采用分頁加載的方式,此時列表項的數量可能會非常龐大,造成頁面的卡頓。
這次我們就來介紹一種虛擬列表的優化方法來解決數據量大的時候列表的性能問題。
什么是虛擬列表
虛擬列表是按需顯示的一種技術,可以根據用戶的滾動,不必渲染所有列表項,而只是渲染可視區域內的一部分列表元素的技術。

如圖所示,當列表中有成千上萬個列表項的時候,我們如果采用虛擬列表來優化。就需要只渲染可視區域( 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(() =