放棄antd table,基於React手寫一個虛擬滾動的表格

緣起css

標題有點誇張,並非徹底放棄antd-table,畢竟在react的生態圈裏,對國人來講,比較好用的PC端組件庫,也就antd了。即使經歷了2018年聖誕彩蛋事件,antd的使用者也不只不減,反而有所上升。html

客觀地說,antd是開源的,UI設計得比較美觀(甩出其餘組件庫一條街),並且是螞蟻金服的體驗技術部(一堆p7,p8,p9,基本都是大牛級的)在持續地開發維護,質量能夠信任。node

不過,antd雖好,但一些組件在某一些場景下,是很不適用的。例如,以表格形式無限滾動地展現大量數據(1w+)時,antd-table就特別蹩腳了,光是首次渲染就能卡個五秒白屏。若是這個表格還要求能編輯,甚至不一樣列之間發生聯動呢?對不起,antd-table無能爲力,會把頁面卡炸的。react

antd-table自己是基於rc-table的擴展,而rc-table所屬的react-component素來有本身的主張,在react社區其餘的組件庫都支持無限滾動時(例如react-data-grid, react-virtualized, react-tabulator..),很抱歉,它不支持。git

 爹爹不支持,做爲兒女的antd-table也很差反對,順其天然咯。github

因而,部分使用antd的開發者就腦闊疼了,想使用其餘支持無限滾動的表格組件吧,會發現諸多的問題:算法

  1.UI太醜,真的,特別是react-data-grid,不能再醜了。雖然它的功能很強大,但顏值是個硬傷。想給它整容,符合antd一慣的審美風格,還真的挺繁雜的,從上手到放棄系列。瀏覽器

  2.擴展起來,不接地氣。有的組件庫,功能很強,但封裝得太厲害,說的就是上面的react-data-grid,還有react-tabulator,要想用起來,可不容易。說是react組件,可怎麼用都以爲是反react,有點jq的傾向,惹不起。緩存

  3.文檔的可讀性差。react-data-grid,react-virtualized好歹還有基礎的API文檔,雖然寫的不咋地,但也比react-tabulator這個只能讓人去看源碼的強。antd

  4.版本不穩定。react-tabulator很任性,release直接從2,x升級到4.x...

  5.不支持樹形表格編輯。說的是react-virtualized,或許新版本支持了,但不得不對它說抱歉。

  6.圈子不活躍,人少。人少、不活躍就意味着這個庫可能不長久,好比react-tabulator。

一番比較下來,你會發現,仍是react-component舒服,文檔友好,擴展靈活,版本穩定,社區活躍,徹底能夠嵌套和插入本身寫的react組件(就是醜了點),想必這也是antd基於它來作擴展的一個重要考量。antd或許是意識到了無限滾動地重要性,好比移動端的瀑布流,PC端商品列表的無限下拉刷新,在3.x版本已經基於react-data-grid作了一層擴展,增長了List組件,用來支持無限滾動。

但,對於表格而言,仍是沒有人性化的解決方案。

沒辦法,需求來了,不上也得上,本身手寫一個吧。

目前爲止,無限滾動沒去作,只作了縱向虛擬滾動,滾動有些許延遲,但首次渲染和編輯的實時響應,仍是能夠接受的,並且支持固定左右列,橫向滾動,徹底支持自定義react組件的嵌套和插入,擴展起來太容易了。基本支持antd-table的用法。

實戰

在動手寫以前,要考慮一些問題:

  1.是採用原生table,仍是用div來模擬?

  2.對於樹形表格,採起怎樣的虛擬滾動方案?

  3.組件的職責邊界怎麼界定?

1、原生table Vs div模擬表格

table之因此叫table,用意很明顯了,在你想要以表格形式展現數據的時候,首先要想到的,就是用table。

table佈局有瀏覽器的特定算法實現加速繪製,且對靜態表格來講,頁面結構是很穩定的。

雖然div模擬表格繪製的速度也不慢,但要達到跟靜態表格同樣的結構穩定性,可就作許多額外的維護工做了,css輔助,js控制,瀏覽器背後對table作的髒活累活,你基本都得接手,從零開始。

但table也有硬傷,首先是樣式很差自定義,想改裝原生table,讓它變得好看,還真不是一件快活的事,具體參考antd-table。其次,若是要求表格左右列能固定,中間列可滾動,原生table就很絕望了,它不得很少叫來兩個table兄弟,讓他們來輔佐本身,一個在左,一個在右,跟本身裝載一樣多的數據,但卻只顯示固定列。三兄弟之間,還要時不時保持聯絡,確保你們每行高度都是同樣的。

若是這中間出了什麼誤差,就會致使滾動的表格看起來左邊或右邊的行像是掉了下來....用過antd-table的人,應該會有這樣的體會。

而div模擬表格就不同了,它是從零開始的,一張白紙,想怎麼畫就怎麼畫,要多美就能多美。

要實現左右固定列滾動也沒必要裝載三份如出一轍的數據,一份就夠了,它要作的,僅僅是把列固定,將固定列鄰居的位置計算好,就能達到一樣的效果。

這裏,想看示例,能夠看看阿里這位大爺寫的div模擬表格

基於這個角度的比較,我得給div模擬表格投一票。

 

2、虛擬滾動方案

首先,得先理解虛擬滾動的概念。

滾動,相信你們都瞭解,無非就是塊級盒子的內容長度或寬度超出了盒子的寬高,盒子若設置了溢出內容可滾動,那咱們就會看到滾動條,可滾動的距離,跟溢出內容所佔的長度或寬度是相等的。

 <div style="height:30px;overflow:scroll">
   <p  style="height: 10px">1</p>
   <p  style="height: 10px">2</p>
   <p  style="height: 10px">3</p>
   <p  style="height: 10px">4</p>
   <p  style="height: 10px">5</p>
   <p  style="height: 10px">6</p>
 </div>

 

如上述例子,四、五、6是溢出的。它們的高度是30px,便可滾動的距離。

能夠預見,若是還有七、八、9…9999等等近一萬條數據,那麼這個div同一時刻,最多隻能展現4條數據,剩下的9997條數據,都須要滾動才能看到。

建立一個dom節點,成本徹底能接受,十個百個千個也能夠接受,但上萬數十萬呢?就算能接受,也不應如此浪費。

既然只能在同一時刻看到4個節點,爲何不能只建立4個節點,剩下的節點都是經過滾動要展示的時候,纔去建立呢?

這天然是能夠的。

虛擬滾動,就是出於這個目的來設計的。

假設數據有6條,這裏只討論高度。

若是隻建立4個節點,立刻就會發現,滾動條能滾動的距離不對,只有10px。與預期的30px不符。這是由於,滾動距離是瀏覽器根據盒子和盒子裏的節點的高度計算出來的。咱們只能調整節點的高度,沒法直接修改滾動距離的值。

咱們能夠經過在後面建立一個輔助節點,將高度設爲20px來解決這個問題。

 <div style="height:30px;overflow:scroll">
   <p  style="height: 10px">1</p>
   <p  style="height: 10px">2</p>
   <p  style="height: 10px">3</p>
   <p  style="height: 10px">4</p>
   <p  style="height: 20px">佔位符</p>
 </div>

 

如今,經過監聽div的滾動事件,咱們能夠知道滾動條滾到了哪一個位置,經過計算,得知展現的第一條數據在全部數據中,處於哪一個位置,是第2條,仍是第1條等等信息...

而後,進一步得知,哪個未建立的節點,要當即被建立,而且,佔位符的高度要對應變化。

例如上述例子裏,展現2345的時候,佔位符高度就要設爲10px,而且最上面也要設置一個10px高的佔位符,如:

 <div style="height:30px;overflow:scroll">
   <p  style="height: 10px">佔位符</p>
   <p  style="height: 10px">2</p>
   <p  style="height: 10px">3</p>
   <p  style="height: 10px">4</p>
   <p  style="height: 10px">5</p>
   <p  style="height: 10px">佔位符</p>
  </div>

 

遵循的原則就是,確保2345節點(咱們稱之爲視圖區)的高度,與佔位符的高度加起來,等於總數據的實際總高度。

所以引伸出的一個問題就是,每一個節點的高度得固定(在表格裏,就是固定表格行高)。或者,至少是在完全展現完成以前,計算出實際高度。前面討論過的組件庫,除了react-data-grid,沒有哪一個不是固定行高的。

而且,視圖區的高度也要指定。

如此一來,有了這些不變高度的數值,就能經過監聽滾動來計算上下佔位符各自的高度。

虛擬滾動的效果,也就達成了。剩下都是優化的工做,例如緩存節點,diff計算每次滾動時要改變的節點等等。

到這裏,咱們已經得出了扁平數據列表的虛擬滾動方案。

那麼樹形表格呢?

樹形表格,準確的說,指的是數據在表格中以樹形的形式來展示。這樣的表格,能夠展開/收起父節點,而且能夠嵌套無限層級。參考antd-table的例子

讓樹形表格支持虛擬滾動,能夠利用剛纔討論的虛擬滾動方案。

這裏的關鍵點在於,樹形數據,是有父子層級關係的,並非扁平數據。

於是首先要作的,就是把樹形數據按順序遍歷平鋪展開,即扁平化。

// 樹形數據
const tree = [{
  node: 1,
  children: [{
    node: 11,
    children: []
  }, {
    node: 12,
    children: []
  }]
}, {
  node: 2,
  children: []
}, {
  node: 3,
  children: []
}]

// 樹形數據按順序平鋪展開
const flatten = [{
  node: 1
}, {
  node: 11
}, {
  node: 12
}], {
  node: 2
}], {
  node: 3
}]]

如此一來,咱們就能夠徹底複用討論過的虛擬滾動方案,達成樹形表格虛擬滾動的效果。

其次,樹形表格的展示,通常是要根據層級的深度來縮進的,這樣才美觀。咱們能夠展開樹形數據的時候,將層級深度記錄下來,在建立節點的時候,根據層級深度來決定縮進的寬度。

這裏,會遇到一些樣式上的問題,好比展開圖標、縮進的寬度,有可能會受到css規則的影響,使得實際效果與預期不符,這個就須要本身去排查解決了。

 

3、組件的職責邊界

上面已經提到如何實現一個虛擬滾動的樹形表格,但沒提到樹形表格怎麼展開、收起子元素,更沒提到表格的可編輯功能。

這涉及到組件職責邊界的肯定,也是如今要討論的。

一個組件,特別是react組件,它應該有什麼樣的功能,能提供什麼樣的API以供擴展,是要考慮清楚的。考慮不清楚的,就像react-tabulator,寫個自定義單元格編輯器都得尋找dom節點,跟JQ有什麼區別,並且還要按照它們定的規則來寫,不然就不起做用。

理想的組件,不該該附加額外的規則,而是利用現有的規則,加以合適的運行機制,來達到方便擴展的目的。

antd-table這點作的還算能夠,咱們只須要將本身的react組件跟提供的API對接,就能達成想要的效果。

因此,咱們來肯定一下虛擬滾動的樹形表格,應該有怎樣的職責邊界。

首先,列出這表格該有的基礎功能:

1.支持虛擬滾動

2.支持單元格自定義--任何dom節點或者react組件

3.支持左右列固定

沒錯,跟antd-table相比,只是多出了一個虛擬滾動。除此之外的其餘功能,都應該是由表格的使用者來實現,諸如可編輯單元格,樹形表格如何展開收起。

這些,可用一句話來總結——數據驅動視圖。

若是用過D3,相信很是能理解這個理念。數據變幻無窮,組件的功能也能變幻無窮,這是很理想的狀態。

這三個基礎功能裏,第1個能夠採用上述的虛擬滾動方案來實現。第3個能夠用css的sticky屬性配合js計算來實現(具體不贅述,參考阿里大爺的例子)。

第2個,其實卻是最簡單的了。

只須要用React編寫每一個單元格容器,就能作到支持單元格的自定義。由於react天生支持dom節點的嵌套,更是自己就支持react組件之間的互相組合。

到此,基於React手寫一個虛擬滾動的表格,已經Over。

行動力強的讀者,應該已經能夠寫出本身的demo了。

我寫的表格例子,內部大概長這樣:

      <Table onScroll={this.onScroll} style={{ maxHeight: this.tableHeight }}>
        <TableHead
          data={data}
          columns={dataColumns}
          rowWidth={this.rowWidth}
          rowKey={this.rowKey}
          onExpand={this.props.onExpand}
        />
        <Placeholder
          line={viewUpData.length}
          height={this.cellHeight * viewUpData.length + 'px'}
        />
        <ViewPort
          data={data}
          columns={dataColumns}
          rowWidth={this.rowWidth}
          rowKey={this.rowKey}
          onExpand={this.props.onExpand}
        />
        <Placeholder
          line={viewDownData.length}
          height={this.cellHeight * viewDownData.length + 'px'}
        />
      </Table>

外部使用虛擬滾動表格,大概是這樣:

          <VirtualTable
            bordered
            expandedRowKeys={expandedKeys}
            rowKey="id"
            onExpand={(expanded, record) => { this.onExpand(expanded, record) }}
            dataSource={dataSource}
            pagination={false}
            scroll={{ y: 250 }}
            columns={columns}
            viewLine={7}
            onBeforeScroll={this.onBeforeScroll}
          />

若是以前使用了antd-table來實現功能,那麼,只須要將antd-table換成虛擬滾動表格,再加個視圖區的限定於滾動監聽,就徹底OK了,不用改變任何原有的業務邏輯。

 

後續

數據驅動視圖理念的瓶頸,限於個人有限知識,認爲應是在於海量數據頻繁快速變化的時候,渲染視圖的速度如何能跟上來,怎樣作到讓人以爲畫面流暢,徹底不卡。

好比100萬條數據的下拉滾動。

 

學海無涯,苦做舟。這條路,一直是會有苦的...

相關文章
相關標籤/搜索