性能優化小冊 - 渲染十萬條數據:基於 IntersectionObserver 的虛擬列表

技術不侷限於框架,相同的原理只是實現方式略有不一樣。

前置

1. 什麼是虛擬列表?

首先,虛擬列表只是一個概念,本人對虛擬列表這個表述不置能否。

虛擬列表是對於列表形態數據展現的一種按需渲染,是對長列表渲染的一種優化。html

虛擬列表不會一次性完整地渲染長列表,而是按需顯示的一種方案,以提升無限滾動的性能。webpack

2. 虛擬列表的實現原理?

根據容器元素的高度 clientHeight 以及列表項元素的高度 offsetHeight 來顯示長列表數據中的某一個部分,而不是去完整地渲染整個長列表。web

實現一個虛擬列表須要:編程

  • 得知容器元素的高度 clientHeight
  • 得知列表項元素的高度 offsetHeight
  • 計算可視區域應該渲染的列表項的個數 count = clientHeight / offsetHeight
  • 計算可視區域數據渲染的起始位置 start
  • 計算可視區域數據渲染的結束位置 end
  • 對完整長列表數據進行截斷 sliceList = dataList.slice(start, end)
  • 渲染截斷後的列表數據,進而實現無限加載

3. 虛擬列表與懶加載有何不一樣?

懶加載與虛擬列表其實都是延時加載的一種實現,原理相同但場景略有不一樣。segmentfault

  • 懶加載的應用場景偏向於網絡資源請求,解決網絡資源請求過多時,形成的網站響應時間過長的問題。
  • 虛擬列表是對長列表渲染的一種優化,解決大量數據渲染時,形成的渲染性能瓶頸的問題。

4. IntersectionObserver 介紹

IntersectionObserver 提供了一種異步觀察目標元素與視口的交叉狀態,簡單地說就是能監聽到某個元素是否會被咱們看到,當咱們看到這個元素時,能夠執行一些回調函數來處理某些事務。瀏覽器

let io = new IntersectionObserver(callback, option);

callback 會觸發兩次。一次是目標元素剛剛進入視口(開始可見),另外一次是徹底離開視口(開始不可見)。緩存

更多介紹 Intersection Observer性能優化

實現

1. 生成十萬條數據:網絡

function getDataList() {
  let data = []
  for(let i = 0; i < 100000; i++) {
    data.push({id: "item" + i, value: Math.random() * i})
  }
  return data;
}

2. Dom 建立及列表渲染:app

不依賴框架的狀況下,須要命令性的去建立 DOM 以及操做 DOM

<ul class="container">
  <span class="sentinels">....</span>
</ul>
function $(selector) {
  return document.querySelector(selector)
}

function loadData(start, end) {
  // 截取數據
  let sliceData = getDataList().slice(start, end)
  // 現代瀏覽器下,createDocumentFragment 和 createElement 的區別其實沒有那麼大
  let fragment = document.createDocumentFragment(); 
  for(let i = 0; i < sliceData.length; i++) {
    let li = document.createElement('li');
    li.innerText = JSON.stringify(sliceData[i])
    fragment.appendChild(li);
  }
  $('.container').insertBefore(fragment, $('.sentinels'));
}

若是是基於 Virtual DOM 的框架,直接操做數據便可(僞代碼):

// 父組件
<virtual-list :listData="listData"></virtual-list>

// 子組件
<ul class='container'>
  <li
    v-for="item in sliceData" 
    :key="item.id"
  >{{ item }}</li>
</ul>
...

// js
this.sliceData = this.data.slice(start, index)

3. 使用 IntersectionObserver API 建立監聽器:

let count = Math.ceil(document.body.clientHeight / 120);
let startIndex = 0;
let endIndex = 0;
...
let io = new IntersectionObserver(function(entries) {
    loadData(startIndex, count)
    // 標誌位元素進入視口
    if(entries[0].isIntersecting) {
      // 更新列表數據起始和結束位置
      startIndex = startIndex += count;
      endIndex = startIndex + count;
      if(endIndex >= getDataList().length) {
        // 數據加載完取消觀察
        io.unobserve(entries[0].target)
      }
      // requestAnimationFrame 由系統決定回調函數的執行時機
      requestAnimationFrame(() => {
        loadData(startIndex, endIndex)
        let num = Number(getDataList().length - startIndex)
        let info = ['還有', num , '條數據']
        $('.top').innerText = info.join(' ')
        if(num - count <= 0) {
           $('.top').classList.add('out')
         }
      })
    }
  });
  // 開始觀察「標誌位」元素
  io.observe($('.sentinels'));
})

因爲 IntersectionObserver 沒法監聽動態建立的 dom,因此咱們設置一個「標誌位」元素 span.sentinels 做爲監聽的目標對象。

<ul class="container">
  <span class="sentinels">....</span>
</ul>

若是目標元素正處於交叉狀態 entries[0].isIntersecting == true,則表明 .sentinels 進入了可視區域,從而加載新的列表數據。

if(entries[0].isIntersecting) {
  ...
  requestAnimationFrame(() => {
    loadData(startIndex, endIndex)
  })
  ...
}

最後將新的列表 insertBefore 到其前面,進而實現無限加載。

$('.container').insertBefore(fragment, $('.sentinels'));

同系列文章:

相關文章
相關標籤/搜索