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


緣起

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

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

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

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

 爹爹不支持,作為兒女的antd-table也不好反對,順其自然咯。

於是,部分使用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這個只能讓人去看源碼的強。

  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.組件的職責邊界怎么界定?

一、原生table Vs div模擬表格

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

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

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

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

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

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

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

這里,想看示例,可以看看阿里這位大爺寫的div模擬表格

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

 

二、虛擬滾動方案

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

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

 <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>

 

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

可以預見,如果還有7、8、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規則的影響,使得實際效果與預期不符,這個就需要自己去排查解決了。

 

三、組件的職責邊界

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

這涉及到組件職責邊界的確定,也是現在要討論的。

一個組件,特別是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萬條數據的下拉滾動。

 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM