寫在前面:不知不覺 ~2020年的日子已經過去了3/4,看到微博熱搜說:2020年還剩下3個月的時候,心情突然驟降~哈哈哈哈
切入正題:
什么是長列表優化?
我們為什么需要長列表優化?
我們怎樣進行長列表優化
長列表優化
在我們的日常工作中,會越到各種各樣的列表,比如,我們通常采用分頁的方式進行內容的逐漸獲取,但是不可否認的是,當我們列表內容過多的時候,就會出現頁面滑動卡頓、數據渲染較慢的問題,本次將以10000條數據為基准,進行長列表的優化說明
我們為什么要進行長列表優化
進行長列表優化,其實和js在瀏覽器中執行的順序有莫大的關系,在我們進行頁面訪問的時候,瀏覽器會根據頁面進行渲染的進行,主要包含
- Gui 渲染線程(頁面渲染)
- Js引擎線程(執行js腳本)
- 事件觸發線程(EventLoop輪詢處理線程)
- 事件(onClick)、定時器(setTimeout)、ajax(xhr)獨立線程
- Gui線程和js引擎線程是互斥的
我們的每一步操作,是否恰當或者是否阻塞瀏覽器的執行機制,都是有莫大的關聯的
l瀏覽器中對於js的執行順序
如何使用列表優化呢?
1.先進行10000的數據初渲染
<ul id="container"></ul> <script> let total =100000; var con = document.getElementById("container") let timer = Date.now(); //新版本瀏覽器,當存在循環的時候會統計 for(let i =0;i<total;i++){ let li = document.createElement("li"); li.innerHTML = i; container.appendChild(li) } setTimeout(()=>{ //此時是渲染時間 console.log(Date.now()-timer)//3884 }) </script>
我們在懟頁面進行操作的時候,能夠感覺到此時是卡頓的,白屏的時間也是達到了3秒多
2.切片,分段渲染頁面
<ul id="container"></ul> <script> let total =100000; var con = document.getElementById("container") let timer = Date.now(); let index = 0; let id = 0;//遞增的內容 //分片 根據數據大小划分,每次加載固定的數據 function load(){ index+=50; if(index<total){ //定時器是一個宏任務,會等待ui渲染完成后執行 //setTimeout 是一個宏任務,requestAnimationFrame也是一個額宏任務歐 requestAnimationFrame(()=>{ for(let i=0;i<50;i++){ //ie的瀏覽器需要實現文檔碎片 var fragment = document.createDocumentFragment();//文檔碎片 let li = document.createElement("li"); li.innerHTML=i fragment.appendChild(li) } load(); con.appendChild(fragment) },0) } } //分片加載 會導致頁面dom元素過多,造成頁面的卡頓 load();
此時進行分片的處理方式,先讓頁面渲染50條數據,然后在逐漸渲染50條數據,達到了分片的目的,但是如果用戶此時拉動比較快的話,頁面數據還沒有渲染出來,導致存在空白屏幕的情況,但是可以解決簡單的分頁數據獲取的問題
vue的長列表優化
不管是切片還是分時進行加載,無非是解決dom節點過多的時候,給我們頁面帶來的壓力問題,因此嘗試動態加載固定數據的節點項目,進而讓頁面中的dom節點減少,渲染一定數據的節點數據;

我們dom的節點只加載可視區域的內容
列表固定高度分析
思路分析
- 首先進行外側的容器的scroll事件進行監聽
- 計算當前滾動的高度
- 計算開始位置和結束位置
- 計算向上滾動的偏移量
- 進行可視區域數據計算
1.構造數據
import Mock from 'mockjs' let items =[]; for(var i=0;i<500;i++){ //使用mock.js進行生成數據 items.push({id:i,value:Mock.Random.sentence(5,50)}) }
2. 使用列表進行數據傳入
<virtalList :size=40 :remain=8 :items="items" :varlable=true>
<!-- 獲得scope的數據 -->
<itemL slot-scope="{item}" :item="item"></itemL>
</virtalList>
<!-- 能滾動的盒子 --> <div class="viewport" ref="viewport" @scroll="handlScroll"> <!-- 滾動條 --> <div class="scroll-bar" ref="scrollbar"></div> <!-- 列表位置 --> <div class="scroll-list" :style="{transform:`translate3d(0,${offset}px,0)`}"> <div v-for="item in visibleData" ref="items" :key="item.id" :vid="item.id"> <!-- 通過插槽傳出去 --> <slot :item="item"></slot> </div> </div> </div>
.viewport {
overflow-y: scroll;
position: relative;
}
.scroll-list {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
3.參數說明
size: Number, //當前每一項的高度 remain: Number, //可見多少個 items: Array, //當前項目
4.滾動監聽后handlScroll的處理函數
- 初始化視口和scrollBar的高度
this.$refs.viewport.style.height = this.size * this.remain + "px"; //當前的一項高度*顯示的項目高度 this.$refs.scrollbar.style.height = this.items.length * this.size + "px"; //當前總長度*當前的項目高度
- 頁面滾動的時候
this.start = Math.floor(scrollTop / this.size); //列表開始的位置應該額等於滾動的位置/列表的高度
this.end = this.start + this.remain; //要渲染列表結束的位置等於列表開始的位置加上每一頁數據展示的高度
//如果前面有預留渲染,應該把這個位置向上移動
this.offset = this.start * this.size - this.prevCount * this.size;
5.計算可視化的數據列表,渲染該列表即可
computed: { //可見數據有哪些 visibleData() { let start = this.start - this.prevCount; let end = this.end + this.nextCount; return this.items.slice(start, end); }, },
6.此時看到了prevCount 和nextCount ,是為了解決問題不一致的原因,比如上滑倒一半時候,可視化數據的前一個數據還沒有渲染出來,此時導致數據是空白的,后面的數據也是一樣,如果存在一般沒有加載出來的時候,可能導致后面就是白色空白的頁面,因此,此時需要將可視區域的前面幾項和后面幾項多加載出來,防止出現空白化的問題,定義prevCount和nextCount
computed: { //前面預留幾個 prevCount() { return Math.min(this.start, this.remain); }, //后面預留幾個 nextCount() { return Math.min(this.remain, this.items.length - this.end); }, },
這樣就可以有效的防止滑動到可視區域的第一項的時候或者最后一項的時候為空而出現空白的頁面
列表高度不固定的時候
- 預估列表的初始高度值,和實際值的列表相差幅度不是很大
- 根據預估值存儲每個列表的頂部top,高度、底部bottom
- 滾動時候根據二分查找方法查找到當前的初始區域,
- 更新列表中的實際高度
1.設置列表的 varlable 屬性,表示當前的item的子項目是不固定高度的
<virtalList :size=40 :remain=8 :items="items" :varlable=true> <!-- 獲得scope的數據 --> <itemL slot-scope="{item}" :item="item"></itemL> </virtalList>
2.初始化列表的存儲高度 預估高度
//緩存當前的高度,top志等信息,還有bottom
cacheList() {
this.positions = this.items.map((item, index) => {
return {
height: this.size,
top: index * this.size,
bottom: (index + 1) * this.size,
};
});
3.滾動時候根據scrollTop查找到當前可視區域開始顯示位置和結束顯示位置
let scrollTop = this.$refs.viewport.scrollTop; if (this.varlable) { //要使用二分查找方法進行查找 this.start = this.getStartIndex(scrollTop) this.end = this.start +this.remain; //設置偏移量 this.offset = this.positions[this.start - this.prevCount] ? this.positions[this.start-this.prevCount].top:0 console.log(this.offset) } else{ ...固定高度的處理項目 }
4.二分查找方法進行查找開始位置和結束位置
getStartIndex(value){ let start = 0; //開始 let end = this.positions.length -1;//結束位置 let temp = null; //記錄當前的高度臨時值 //當開始位置小於結束的位置的時候,就一直往下找 ----todo while(start <= end){ //找到中金的位置 let middleIndex = parseInt((start+end)/2); //中間位置bottom位置 let middleValue = this.positions[middleIndex].bottom; //如果當前的middleValue與value相等,則可進行 if(middleValue ==value){ return middleIndex +1; }else if(middleValue <value){//當前要查找的在右邊 start = middleIndex+1 }else if(middleValue >value){ //當前要查找的在左邊 // temp為存儲的臨時數據 如果不存在middleValue == value的時候 返回這個臨時的數據 if(temp == null || temp > middleIndex){ temp = middleIndex //找到范圍 } end = middleIndex -1; } } return temp; },
二分查找法進行步驟分析
5.更新組件內存儲的列表高度
updated() { //頁面渲染完成后需要根據當前展示的數據,更新緩存區的內容 this.$nextTick(()=>{ //根據當前顯示的,更新緩存中的height、bottom\top 最終更新滾動體哦的高度 //獲取當前可視化區域的item對應的值 let nodes = this.$refs.items; if(!(nodes && nodes.length>0)){ return ; } //遍歷當前所在節點信息, nodes.forEach(node=>{ //獲取當前items的真實高度 let {height} = node.getBoundingClientRect();//真實高度 let id = node.getAttribute('vid')-0; //獲取當前的item的位置,id表示index let oldHeight = this.positions[id].height; //老的高度 let val = oldHeight - height; //高度差值 //如果當前元素存在高度差值,則當前高度需要進行增加/減少,這樣,當前元素的后面元素的表示位置也需要在原高度上進行更改 if(val){ //當前節點的高度進行更新 this.positions[id].height = height; this.positions[id].bottom = this.positions[id].bottom - val ;//底部增加了 //將后續的所有人都向后移動 for(let i = id+1;i<this.positions.length;i++){ this.positions[i].top = this.positions[i-1].bottom; this.positions[i].bottom = this.positions[i].bottom- val; } } }) //動態的計算滾動條的高度 滾動條的高度就等於positions的最后一個元素的最底部的位置 this.$refs.scrollbar.style.height = this.positions[this.positions.length-1].bottom +'px' }) },
效果圖展示:
留下一些疑問,我們在滾動的時候其實做着大量的計算操作,會不會影響性能呢?
這樣做的好處又是什么呢?