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

前置
1. 什麼是虛擬列表?
首先本文實現的是一個無限長表,不是嚴格上的虛擬列表,虛擬列表只是一個概念,本人對虛擬列表這個表述不置能否。瀏覽器
虛擬列表是對於列表形態數據展現的一種按需渲染,是對長列表渲染的一種優化。微信
虛擬列表不會一次性完整地渲染長列表,而是按需顯示的一種方案,以提升無限滾動的性能。網絡
2. 虛擬列表的實現原理?
根據容器元素的高度 clientHeight
以及列表項元素的高度 offsetHeight
來顯示長列表數據中的某一個部分,而不是去完整地渲染長列表。app

實現一個虛擬列表須要:框架
-
得知容器元素的高度 clientHeight
-
得知列表項元素的高度 offsetHeight
-
計算可視區域應該渲染的列表項的個數 count = clientHeight / offsetHeight
-
計算可視區域數據渲染的起始位置 start
-
計算可視區域數據渲染的結束位置 end
-
對完整長列表數據進行截斷 sliceList = dataList.slice(start, end)
-
渲染截斷後的列表數據,進而實現無限加載
3. 虛擬列表與懶加載有何不一樣?
懶加載與虛擬列表其實都是延時加載的一種實現,原理相同但場景略有不一樣。dom
-
懶加載的應用場景偏向於網絡資源請求,解決網絡資源請求過多時,形成的網站響應時間過長的問題。 -
虛擬列表是對長列表渲染的一種優化,解決大量數據渲染時,形成的渲染性能瓶頸的問題。
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 建立及列表渲染:
不依賴框架的狀況下,須要命令性的去建立 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'));
}
若是是基於框架,直接操做數據便可(Vue 僞代碼):
// 父組件
<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'));
本文分享自微信公衆號 - 像素搖擺(pxDance)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。