在React項目中,如何優雅的優化長列表


  對於較長的列表,好比1000個數組的數據結構,若是想要同時渲染這1000個數據,生成相應的1000個原生dom,咱們知道原生的dom元素是很複雜的,若是長列表經過生成如此多的dom元素來實現,極可能使網頁失去響應。react

  貫穿React核心的就是"virtual dom",咱們一樣的能夠經過用虛擬列表的方式來優雅的優化長列表git

  • 原生dom渲染長列表的缺陷
  • 虛擬列表優化長列表的原理
  • 經過react-virtualized來優化長列表
  • 經過react-tiny-virtual-list來優化長列表

本文的原文地址發佈在個人博客中:github

github.com/fortheallli…ajax

歡迎star和fork~數組

本文的用例的代碼地址爲:瀏覽器

github.com/fortheallli…數據結構


1、原生dom渲染長列表的缺陷

  首先咱們嘗試在React項目中,未作任何優化一次性渲染1000個dom,每一個dom包含一個img標籤,原生dom自己是很複雜的對象,加上img標籤後。渲染的效果以下圖所示:dom

  能夠很明顯的看到白屏的時間很長,由於在React中,不作任何優化,直接渲染這麼包含1000個圖片的dom節點,即便React自己用了虛擬dom,可是在首次渲染的時候,是實實在在的生成了1000個真實的dom,咱們能夠查看網頁中的真實dom狀況,以下所示:

  從上圖咱們能夠看出,是確確實實的生成了1000個真實的dom,進入頁面後,須要渲染這1000個dom,所以白屏的時間很長。異步

  此外,在直接渲染1000個dom節點的頁面,觸發滾動事件,也會使得內存用量增長,具體能夠以下圖所示:函數

此外同時渲染不少dom節點,也會形成一下幾個問題:

  • 容易失幀,由於渲染很慢,因此沒法維持瀏覽器的幀率,主觀上會顯得頁面卡頓

  • 網頁失去響應,事件等沒法及時被觸發

  上述的效果都是在PC端展現的,對於特定的移動設備,直接無優化的渲染長列表所形成的問題會更加的放大。長列表的渲染在移動端的不少場景會遇到,好比微博,feeds流中等等。合理的優化長列表,能夠提高用戶體驗。

2、虛擬列表優化長列表的原理

優化長列表的原理很簡單,基本原理能夠一句話歸納:

用數組保存全部列表元素的位置,只渲染可視區內的列表元素,當可視區滾動時,根據滾動的offset大小以及全部列表元素的位置,計算在可視區應該渲染哪些元素。

具體實現步驟以下所示:

  1. 首先肯定長列表所在父元素的大小,父元素的大小決定了可視區的寬和高
  2. 肯定長列表每個列表元素的寬和高,同時初始的條件下計算好長列表每個元素相對於父元素的位置,並用一個數組來保存全部列表元素的位置信息
  3. 首次渲染時,只展現相對於父元素可視區內的子列表元素,在滾動時,根據父元素的滾動的offset從新計算應該在可視區內的子列表元素。這樣保證了不管如何滾動,真實渲染出的dom節點只有可視區內的列表元素。
  4. 假設可視區內能展現5個子列表元素,及時長列表總共有1000個元素,可是每時每刻,真實渲染出來的dom節點只有5個。 5.補充說明,這種狀況下,父元素通常使用position:relative,子元素的定位通常使用:position:absolute或sticky

經過虛擬列表優化後,一樣的顯示1000個包含圖片的dom,白屏時間會大大的減小。具體效果以下圖所示:

對於比無優化的狀況,優化後的虛擬列表渲染速度提高很明顯。

3、經過react-virtualized來優化長列表

社區實現虛擬列表的React組件不少,較爲經常使用的是react-virtualized和react-tiny-virtual-list,前一個較爲全面,第二個較爲輕量。接下來會分別來介紹這倆個React組件庫。

一、react-virtualized簡介

react-virtualized是一個實現虛擬列表較爲優秀的組件庫,react-virtualized提供了一些基礎組件用於實現虛擬列表,虛擬網格,虛擬表格等等,它們均可以減少沒必要要的dom渲染。此外還提供了幾個高階組件,能夠實現動態子元素高度,以及自動填充可視區等等。

react-virtualized的基礎組件包含:

  • Grid:用於優化構建任意網狀的結構,傳入一個二維的數組,渲染出相似棋盤的結構。
  • List:List是基於Grid來實現的,可是是一個維的列表,而不是網狀。
  • Table:Table也是基於Grid來實現,表格具備固定的頭部,而且能夠在垂直方向上滾動
  • Masonry:一樣能夠在水平方向,也能夠在垂直方向滾動,不一樣於Grid的是能夠自定義每一個元素的大小,或者子元素的大小也能夠是動態變化的
  • Collection:相似於瀑布流的形式,一樣能夠水平和垂直方向滾動。

值得注意的是這些基礎組件都是繼承於React中的PureComponent,所以當state變化的時候,只會作一個淺比較來肯定從新渲染與否 。

除了這幾個基礎組件外,react-virtualized還提供了幾個高階組件,好比ArrowKeyStepper 、AutoSizer、CellMeasurer、InfiniteLoader等,本文具體介紹經常使用的AutoSizer、CellMeasurer和InfiniteLoader。

  • AutoSizer:使用於一個子元素的狀況,經過AutoSizer包含的子元素會根據父元素Resize的變化,自動調節該子元素的可視區的寬度和高度,同時調節的還有該子元素可視區真實渲染的dom元素的數目。
  • CellMeasurer:這個高階組件能夠動態的改變子元素的高度,適用於提早不知道長列表中每個子元素高度的狀況。
  • InfiniteLoader:這個高階組件用於Table或者List的無限滾動,適用於滾動時異步請求等狀況

二、react-virtualized基礎組件的使用

下面咱們來介紹一下經常使用的基礎組件Grid、List。

(1)Grid

全部基礎組件基本上都是基於Grid構成的,一個簡單的Grid的例子以下:

import { Grid } from 'react-virtualized';
const list = [
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU'],
  ['Jony yu', 'Software Engineer', 'Shenzhen', 'CHINA', 'GUANGZHOU']
];

function cellRenderer ({ columnIndex, key, rowIndex, style }) {
  return (
    <div
      key={key}
      style={style}
    >
      {list[rowIndex][columnIndex]}
    </div>
  )
}
render(
  <Grid
   cellRenderer={cellRenderer}
   columnCount={list[0].length}
   columnWidth={100}
   height={300}
   rowCount={list.length}
   rowHeight={80}
   width={300}
  />,
  rootEl
);
複製代碼

顯示的效果以下圖所示:

渲染網格也是隻渲染可視區的dom節點,有個有趣的現象是滾動條的大小,這裏Grid作了一個細節優化,只有滾動的時候纔會顯示滾動條,中止滾動後會隱藏滾動條。

(2)List

接着來舉例說明一下List的使用:

import { List } from 'react-virtualized';
import  loremIpsum from "lorem-ipsum"
const rowCount = 1000;
const list = Array(rowCount).fill().map(()=>{
  return loremIpsum({
        count: 1,
        units: 'sentences',
        sentenceLowerBound: 3,
        sentenceUpperBound: 3
      }
})
function rowRenderer ({
  key,         
  index,      
  isScrolling, 
  isVisible,   
  style      
}) {
  return (
    <div
      key={key}
      style={style}
    >
      {list[index]}
    </div>
  )
}
export default class TestList extends Component{
  render(){
    return <div style={{height:"300px",width:"200px"}}>
            <List
              width={300}
              height={300}
              rowCount={list.length}
              rowHeight={20}
              rowRenderer={rowRenderer}
             />
          </div>
  }
}
複製代碼

List的使用方法也是極簡,指定列表總條數rowCount,每一條的高度rowHeight以及每次渲染的函數rowRenderer,就能夠構建一個渲染列表。具體的效果以下圖所示:

二、react-virtualized高階組件的使用

結合List來看看react-virtualized高階組件的使用。

(1)AutoSizer

首先來看使用不使用AutoSizer的缺點,以下圖所示,List只能指定固定的大小,若是其所在的父元素的大小resize了,那麼List是不會主動填滿父元素的可視區的:

從上圖能夠看出來,List是沒法自動填充父元素的。所以咱們這裏須要使用AutoSizer。AutoSizer的使用也很簡單,咱們只須要在List的基礎上:

class TestList extends Component{
  render(){
    return <div>
             <AutoSizer>
              {({ height, width }) => (
                <List
                  height={height}
                  rowCount={list.length}
                  rowHeight={20}
                  rowRenderer={rowRenderer}
                  width={width}
                />
              )}
            </AutoSizer>
          </div>
  }
}
複製代碼

效果以下圖所示:

上述能夠看出來增長了AutoSizer能夠動態的適應父元素寬度和高度的變化。

可是也存在一個問題:

子元素太長,換行後改變了子元素的高度後沒法子適應,也就是說僅僅經過基礎的組件List是不支持子元素的高度動態改變的場景

(2)CellMeasurer

爲了解決上述的子元素能夠動態變化的問題,咱們能夠利用高階組件CellMeasurer:

import { List,AutoSizer,CellMeasurer, CellMeasurerCache} from 'react-virtualized';
const cache = new CellMeasurerCache({ defaultHeight: 30,fixedWidth: true});
function cellRenderer ({ index, key, parent, style }) {
  console.log(index)

  return (
    <CellMeasurer
      cache={cache}
      columnIndex={0}
      key={key}
      parent={parent}
      rowIndex={index}
    >
      <div
        style={style}
      >
        {list[index]}
      </div>
    </CellMeasurer>
  );
}
複製代碼

對於須要渲染的List,以下所示:

class TestList extends Component{
  render(){
    return <div>
             <AutoSizer>
              {({ height, width }) => (
                <List
                  height={height}
                  rowCount={list.length}
                  rowHeight={cache.rowHeight}
                  deferredMeasurementCache={cache}
                  rowRenderer={cellRenderer}
                  width={width}
                />
              )}
            </AutoSizer>
          </div>
  }
}
複製代碼

最後的結果以下所示:

上圖咱們看出來,子列表元素的高度能夠動態變化,經過CellMeasurer能夠實現子元素的動態高度。

(3)InfiniteLoader

最後咱們來考慮這種無限滾動的場景,不少狀況下咱們可能須要分頁加載,就是常見的在可視區內無限滾動的場景。react-virtualized提供了一個高階組件InfiniteLoader用於實現無限滾動。

InfiniteLoader的使用很簡單,只要按着文檔來便可,就是分頁的去在家下一頁,滾動分頁所調用的函數爲:

function loadMoreRows ({ startIndex, stopIndex }) {
  return new Promise(function(resolve,reject){
    resolve()
  }).then(function(){
    //模擬ajax請求
    let temList = Array(10).fill(1).map(()=>{
      return loremIpsum({
            count: 1,
            units: 'sentences',
            sentenceLowerBound:3,
            sentenceUpperBound:3
        })
    })
    list = list.concat(temList)
  })
}
複製代碼

最後的效果以下:

看起來跟基礎組件List同樣,其實惟一的區別就是會在滾動的時候自動執行loadMoreRows函數去更新list

(4)總結

經過基礎組件Grid、List以及高階組件AutoSizer、CellMeasurer和InfiniteLoader,已經能夠構建出比較複雜的場景,可是有一個缺陷,就是CellMeasurer雖然說必定程度上支持動態子元素的高度的變化,實際上是一種估算,存在不少邊界狀況,沒法適應於動態元素的場景,特別是文本節點較多致使的高度變化。可是對於圖片節點的動態高度支持沒有很大的問題。

舉例一種邊界狀況,CellMeasurer沒法支持文本動態高度的狀況:

從上圖能夠看到,慢慢縮小的過程當中,若是縮的過小,並無動態的撐大子元素的高度,出現了文字的重疊。

4、經過react-tiny-virtual-list來優化長列表

react-tiny-virtual-list是一個較爲輕量的實現虛擬列表的組件,不一樣於react-virtualized支持網格以及表格等渲染優化。 react-tiny-virtual-list只支持列表,使用方便,其源碼也只有700多行。

使用極其簡單:

import VirtualList from 'react-tiny-virtual-list';
const data = ['A', 'B', 'C', 'D', 'E', 'F','A', 'B', 'C',
'D', 'E', 'F','A', 'B', 'C', 'D', 'E', 'F',
'A', 'B', 'C', 'D', 'E', 'F'];
export default class TinyVirtual extends Component {
  render(){
    return <VirtualList
            width='100%'
            height={200}
            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>
            }
            />
  }
}
複製代碼

最後的渲染結果也是類似的,也能夠支持無限滾動等等。

可是react-tiny-virtual-list有一個致命的缺點:

徹底不支持子元素的動態高度或者寬度

5、總結

本文介紹了虛擬列表的優化的原理,以及經常使用的React能夠優化虛擬列表的組件庫。在接下來的文章中,會具體的介紹react-tiny-virtual-list和react-virtualized的源碼,敬請期待。

相關文章
相關標籤/搜索