長列表優化之虛擬列表

背景

這幾個月接了一個日誌收集系統的活,由於這個系統是屬於傳承的項目,因此我也想在系統上作一些標誌性的改動,做爲接力棒傳遞下去。前端

這個日誌系統從前端到服務端,都作了不小的改動,好比虛擬列表,electron熱更新,sql優化,增長了用戶的概念,也就是須要登陸,把數據庫升級爲Elasticsearch,等等。我以後會把一些我感受比較好玩的東西分享出來。vue

我最早操刀修改的是日誌list的展現。這個list就是比較常規的堆節點,頁面會隨着時間的增長致使DOM節點愈來愈多,這是一個不得不改的問題。ajax

分析需求

首先,由於同事們對這個列表的長時間的使用已經習慣了,因此最好在體驗上不要進行大的修改。爲此,我須要把以前大概的功能列舉出來:sql

  • 每次列表有新的消息傳入的時候,都要能看到最新的那條數據
  • 當用戶點擊列表中的其中一條數據的時候,列表須要中止更新,也就是中止滾動
  • 當列表處於鎖定狀態,滾動條滾動到最底部的時候,列表恢復自動滾動
  • 當列表中有被選中狀態的數據時,能夠經過上下左右鍵來讓聚焦移動

總結完以前的功能以後,我須要再梳理一下個人需求:數據庫

  • 列表隨着時間會愈來愈長,須要控制展現的節點數量
  • 列表長度隨着WebSocket的通訊而增長,數據更新頻度過快,須要有緩衝池
  • 增長一個列表鎖定的提示,能夠手動解開列表的鎖定
  • 移動聚焦的時候會隨即展現日誌詳情,由於移動速度過快,因此須要增長防抖

梳理完成以後,通過考慮我決定使用虛擬列表來代替現有的長列表,這也是踩坑之路的開始。數組

開始開發

長列表轉虛擬列表

可能以前你們多多少少會據說過這個概念甚至開發過虛擬列表,我無論。在開始以前我仍是先和你們捋一下概念。(敲黑板!!!)瀏覽器

爲何須要虛擬列表

咱們知道,在瀏覽器渲染頁面的時候,當DOM節點的數量越多,每一次重繪的時候,對性能的影響也就越大。緩存

假如咱們須要展現一個信息量很大,大約有數十萬條數據。遇到這樣子的狀況,其實如今有許多的方案,咱們最多見的方案就相似PC上的下一頁、上一頁,可是這個方案在體驗上其實並不友好。大部分的用戶會比較喜歡不停的向下滾動就能夠看到新的內容,可是這個就會遇到一個問題,不停的加載數據,致使頁面堆積的節點愈來愈多,所消耗的內存不斷增大,最後連滾動都會卡頓。bash

這時候咱們從新分析一下,就會發現其實有不少數據咱們大多數狀況下是不須要看見的,若是隻考慮咱們能看到數據的話,其實須要渲染的數據量就會很是的少了,很好的提升了渲染的效率,減小由於大量的重繪照成沒必要要的影響。微信

這麼一梳理一下,答案簡直呼之欲出----虛擬列表。

什麼是虛擬列表

虛擬列表其實沒有什麼特別神奇的地方,說白了就是一種展現列表的思路,在頁面上建立一個容器做爲可視區,在這個可視區內展現長列表中的一部分,也就是在可視區渲染列表。

如圖中所示,是一個簡單的虛擬列表的模型,圖中有幾個概念須要你們稍微瞭解一下:

  • 可視區。
  • 真實列表。
  • startIndex。
  • endIndex。
可視區

可視區你們能夠這麼理解,咱們如今有一個<div class="show-box">,給這個元素加一些樣式。

.show-box{
    width: 375px;
    height: 500px;
    margin: 0 auto;
    position: relative;
}
複製代碼

經過這個樣式咱們能夠看出這個可視區容器的高度爲500px

真實列表

真實列表就是會被渲染出來的列表,這麼說可能不太理解,舉個栗子:如今須要被渲染出來的列表數量一共有1000條,可是實際上在頁面須要被渲染的列表數量(須要被看到的數據)只須要100條,這個100條就是所謂的真實列表。

<div class="list-body-box" @scroll="listScroll"> ----- 真實列表
  <div class="list-body"> ------ 載體
  </div>
</div>
-------------------------- style --------------------------------
.list-body {
  min-height: 10px;
  position: absolute;
  width: 100%;
}
複製代碼

在這裏,建議真實列表的長度須要比可視區的高度長一些,有一個滾動條的話,以後能夠經過scroll監聽作一些其餘的操做。

可能有一個點須要和你們解釋一下爲何個人<div class="list-body">絕對定位

當你的某一個元素會頻繁發生變化的時候,最好將這個模塊經過絕對定位的方式,脫離文檔流,能夠減小重繪帶來的影響。

咱們先看一下瀏覽器的渲染機制

  • 解析HTML,生成DOM樹,解析CSS,生成CSSOM樹
  • 將DOM樹和CSSOM樹結合,生成渲染樹(Render Tree)
  • Layout(迴流):根據生成的渲染樹,進行迴流(Layout),獲得節點的幾何信息(位置,大小)
  • Painting(重繪):根據渲染樹以及迴流獲得的幾何信息,獲得節點的絕對像素
  • Display:將像素髮送給GPU,展現在頁面上。

絕對定位或者浮動脫離了正常的文檔流,至關於只是在節點上存放了一個token,而後經過這個token去進行映射,因此若是你採用了絕對定位的方法,也只會對這一塊元素進行重繪。

startIndex

以前也說到了,真實列表實際上只是總列表其中很小的一部分,在這以外還有不少列表須要被渲染。所以,你們能夠把真實列表理解爲一個片斷。被渲染的第一個元素的index就是片斷中第一個元素在總列表中的位置,也就是數組中的index

舉個栗子:個人總列表(數組)的長度爲1000,而須要渲染的列表片斷爲100—200,那麼這個開始的位置,也就是數組的index則爲99

edIndex

解釋同上,最後一個元素的index199

虛擬列表的實現

這裏要提一下,個人框架用的是vue,因此虛擬列表的實現也是比較方便的。

<div class="list-body-box">
  <div class="list-body">
    <div 
      v-for="(item, idx) in list" 
      v-if="idx >= startIdx && idx <= endIdx" 
      :key="idx"
      class="list-row">
      <div class="col-item col-1">{{item.col_1}}</div>
      <div class="col-item col-2">{{item.col_2}}</div>
      <div class="col-item col-3">{{item.col_3}}</div>
      <div class="col-item col-4">{{item.col_4}}</div>
      <div class="col-item col-5">{{item.col_5}}</div>
      <div class="col-item col-6">{{item.col_6}}</div>
      <div class="col-item col-7">{{item.col_7}}</div>
    </div>
  </div>
</div>
複製代碼

模板上,沒有什麼太特別的地方,主要就是經過v-if去控制列表的展現,經過startIdxendIdx的增減,去展現不一樣位置的數據,讓這兩個值遞增就能夠實現列表滾動

下邊咱們會說一下自動滾動在代碼上的實現,主要是經過一個主動的事件去頻繁的觸發對startIdxendIdx遞增或者遞減。

let time = null;
...
autoScroll(){
  time = setTimeout(()=>{
    let listLen = this.list.length - 1;
    this.endIdx = listLen;
    this.startIdx = (listLen + 1) <= 100 ? 0 : listLen - 100;
    this.autoScroll();
  },300);
}
複製代碼

如上代碼所示,我只須要再讓一個方法去觸發autoScroll(),這個方法就會在setTimeout的做用下自調用,startIdxendIdx會不斷遞增列表就能夠自動滾動了,在這裏邊有一個表達式

this.startIdx = (listLen + 1) <= 100 ? 0 : listLen - 100;
複製代碼

這一塊的話主要是解決當頁面剛打開或者清空列表的時候,實際上列表的長度比較短,是不須要進行滾動的,換句話說,startIndex須要在列表總長度在到達一個值以前一直爲0

到這裏,簡單的虛擬列表就實現了。

WebSocket緩衝池

咱們使用的是WebSocket來傳遞數據,數據量很多。所以極可能會出現過於頻繁更新數據的狀況,數據一更新,頁面也會隨之改變,這樣會對性能照成必定的影響。因此咱們須要對這個頻度進行把控。目前的方案是加一個緩衝池。

這緩衝池的思路大概是這樣的,WebSocket傳遞數據的時候,咱們把這段時間的數據先存在一個數組中,而後每隔一段時間,好比500ms,再把數據push到完整的列表中,這個方案可能就會涉及到節流。

let socketPool = [];    //存儲一段時間的數據
let socketTimer;
socketFun( (data) => {
  //先製造一個緩存區間,用來作緩存socket的數據
  socketPool.push(data);
  //每次都把當前的數據進行push到list
  if(!socketTimer){
    socketTimer = setTimeout(()=>{
      this.appendRecord(socketPool);
      socketPool.length = 0;
      this.scrollToBottom();
      socketTimer = null;
    },500);
  }
});
複製代碼

在這裏邊appendRecord()是用來處理數據,而且把數據放入list中的方法,而scrollToBottom()就是爲了當數據push到list以後,列表能直接展現最新的數據,也就是讓頁面滾動到列表的最底部。

緩衝池其實也是提高性能的一個方案,這個方案最核心的地方就是減小頁面渲染的次數。你們能夠這麼理解:每秒鐘可能會有10條數據須要被渲染,假如我每次都老老實實的渲染,那麼10秒的時間我就要渲染10次,實際上是沒有必要的,所以咱們能夠考慮每2秒渲染一次,這樣10s的時間內個人渲染次數就會減小到5次。你能夠理解爲性能提高了一倍

列表鎖定

按照以前的需求,當用戶點擊列表中的其中一條數據的時候,列表是須要中止滾動的。因此我加了一個滾動鎖autoScrollLoack,這個鎖的做用就是當我點擊到列表中的某一條的時候,執行autoScrollLoack = true頁面就不會滾動了。這個鎖的判斷會放在this.scrollToBottom()中,代碼你們稍微看看就行。

scrollToBottom(){
  if(autoScrollLoack){
    return;
  }
  ...do something
},
複製代碼

這個autoScrollLoack在頁面中會與一個單選框進行雙向綁定,所以用戶就能夠經過改變單選框的選中狀態來控制鎖的狀態,其實在有了這個鎖以後,頁面若是由於需求中止滾動了,用戶也能有所感知,不至於忽然滾動就中止了,看起來像個bug。

聚焦移動

聚焦移動的功能以前需求也說過了,就是選中了一條信息,能夠經過上下鍵將聚焦指向上一個或者下一個,這個其實也比較好實現

<div 
  v-for="(item, idx) in list" 
  v-if="idx >= startIdx && idx <= endIdx" 
  :key="item.id"
  :class="{'active':curIdx==idx}"
  class="list-row"
  @click="showDetail(item.id)">
複製代碼

在這裏,你們能夠看到,active就是聚焦的時候列表的樣式。在邏輯上,把當前選中項的index賦給curIndex,前端模板上經過vue對class的綁定來控制樣式,判斷條件就是curIndex == index

聚焦功能已經實現了,那麼接下來要實現經過鍵盤中的上下鍵,實現移動聚焦的效果。這個功能很簡單,咱們徹底能夠經過vue提供的監聽事件來實現,具體的實現你們能夠在官網上搜一下keyup

<div class="list-body-box" @scroll="listScroll" @keyup="moveFocus">
...
...
moveFocus(e){
  let keyCode = Number(e.keyCode);
  switch(keyCode){
    case 38:
      this.curIdx -= 1;
      this.showDetail();
      break;
    case 40:
      this.curIdx += 1;
      this.showDetail();
      break;
  }
},
複製代碼

這段代碼實現了聚焦的上下挪動。根據需求咱們每一次聚焦的時候須要展現聚焦項對應的日誌詳情,詳情是須要發ajax請求來獲取的。問題來了,有一個場景:我想經過鍵盤把當前的聚焦向下挪動10次,在不停聚焦的過程當中我會觸發10次請求,這個其實不必,我在快速移動的過程當中,是不care詳情的,我只須要展現目標詳情就好了。綜上,咱們須要再加一個防抖。

let detailTimer;
showDetail(id){
  if(detailTimer){
    clearTimeout(detailTimer);
  }
  detailTimer = setTimeOut(() => {
    $.post('...',{
      id:id
    }).then((res) => {
      do something...  
    });
  },300);
}
複製代碼

從上邊的邏輯咱們能夠看出來,當用戶在快速挪動聚焦的時候是不會觸發請求的,實際上這個改動很大程度上提高了用戶的流暢度。

總結

其實虛擬列表的開發仍是比較簡單的,可是實際意義卻比較大,在這個過程當中會涉及到很多頁面的優化,感興趣的童鞋能夠嘗試一下,歡迎你們加我微信交流:zyf348519452,咱們下期見~

相關文章
相關標籤/搜索