最近在做一個微信小程序
的直播模塊
,模塊里的聊天室功能是用scroll-view + 一維數組
的形式展示的,而且也沒有進行任何的優化,導致用戶的體驗感比較差
首先模擬一下優化前的聊天室
情況
肉眼可見的蛋疼~
但是優化還是得優化滴,不優化是不可能滴,但是在開始之前,我覺得有必要把優化步驟拆分為以下兩點?
1. 不再使用
scroll-into-view
設置錨點由于舊版本使用的是
scroll-view + 一維數組
的形式實現的,這就導致在數據添加后頁面總會顯示加載后的最后一條信息
,而不是加載前的最后一條信息
,因此上一任開發者使用了scroll-into-view
屬性作為數據加載后的回位錨點
,但是由于錨點指向
的切換和數據加載
并不是同步發生的,這就導致出現回彈
的現象2. 大量數據的處理
因為是
聊天室
功能,因此不可避免的需要加載大量的用戶對話、圖片等內容,又因為scroll-view
本身并不適合加載大量的數據(太菜了想不出來其他辦法),故而需要在數據的加載和顯示部分下點功夫處理一下3. 附加功能處理
聊天室原本還有
返回底部
等功能存在,因此在完成優化后原本的功能也不能忽略
OK開工~
1、倒置scroll-view
為什么要倒置scroll-view
呢?從上面的第一點我們可以看出,如果需要正序地插入數據,那么就會不可避免地出現數據加載后無法顯示后面數據
的情況,但是想要解決這種情況又需要使用scroll-into-view
屬性,那么如果需要徹底地解決這個問題,就需要從問題的根源scroll-view
下手
首先是修改前
的代碼?
<view class="live-content">這是一個直播畫面</view> <scroll-view class="scroll" :scroll-y="true" :scroll-into-view="scrollIntoView" @scrolltoupper="upper" > <view :id="item.index" :style="{ backgroundColor: item.color, height: '200rpx', lineHeight: '200rpx', textAlign: 'center', }" v-for="item in scrollData" :key="item.index" > {{ item.data }} </view> </scroll-view>
const scrollIntoView = ref("index1"); const upper = () => { let lastNum = scrollData.value[0].data; let newArr = []; for (let index = 1; index <= 10; index++) { newArr.push({ color: getRandomColor(), data: lastNum + index, index: `index${lastNum + index}`, }); } scrollData.value.unshift(...newArr.reverse()); // 這里可以使用nextTick來替換一下,結果也是一樣的,但是為了更明顯的回彈效果我使用了定時器 setTimeout(() => { scrollIntoView.value = `index${lastNum}`; console.log("scrollIntoView :>>", scrollIntoView.value); }, 100); }; const getRandomColor = () => { return "#" + Math.random().toString(16).substr(2, 6); };
那么就先來試一下倒置scroll-view
到底也沒有效果
首先我們需要給scroll-view
套上一個transform:rotate(180deg)
的屬性,然后再給內部的子元素也套上同樣的屬性,別忘了給存放數據的數組
也倒置一下,最重要的,把scroll-view
上的scroll-into-view
屬性去掉,就會得到這樣的效果?
還有就是此時滾動條
的位置是在左邊的,如果有需要可以使用CSS
屬性去掉,或者自行模擬,下面是去去除滾動條的CSS樣式
?
::-webkit-scrollbar { display:none; width:0; height:0; color:transparent; }
到這里還只是第一步
,下一步是如何下拉加載數據
。
此時我們的scroll-view
是處于倒置
的狀態,也就是說頂部是底,底部才是頂
(擱著繞口令呢),所以之前使用的scrolltoupper觸頂方法
要替換成scrolltolower觸底方法
才能實現“下拉加載”
下面是目前的聊天室
看起來好多了
2、大量數據的處理
處理完回彈問題
后,就需要考慮如何處理大量數據
。由于uni-app
官方也在文檔中提到scroll-view
加載大批量數據的時候性能較差,但無奈手頭上也沒有別的辦法,只能死馬當活馬醫了
我第一個想法就是非常經典的虛擬列表
,但是此前所看的很多關于虛擬列表的文章都是在web端
實現的,似乎小程序領域里并不是一個被經常采用的方法,但是所幸還是找到了如何在微信小程序實現虛擬列表
的資料
OK說干就干,那么第一步就是要明確實現虛擬列表需要什么樣的數據結構
,虛擬列表其實簡單地說就是當某一個模塊的數據超出了可視范圍就將其隱藏
,那么如何將數據分為多個模塊呢?答案就是二維數組
首先將當前的頁碼
存儲起來(默認為0),當觸發下拉加載動作時頁碼+1
,然后以當前頁碼作為下標
存入數組
const currentShowPage=ref(0) const upper = () => { let len = scrollData.value[currentShowPage.value].length - 1; let lastNum = scrollData.value[currentShowPage.value][len].data; let newArr = []; currentShowPage.value += 1; for (let index = 1; index <= 10; index++) { newArr.push({ color: getRandomColor(), data: lastNum + index, index: `index${lastNum + index}`, }); } scrollData.value[currentShowPage.value] = newArr; };
當然別忘了在頁面中也需要以二維數組
的形式循環數據
<scroll-view style="transform:rotate(180deg)" :scroll-y="true" @scrolltolower="upper"> <view v-for="(data, index) in scrollData" :key="index"> <view style="transform:rotate(180deg)" :style="{ backgroundColor: item.color, height: '200rpx', lineHeight: '200rpx', textAlign: 'center', }" v-for="item in data" :key="item.index" > {{ item.data }} </view> </view> </scroll-view>
數據結構
的問題解決了,那么接下來就是如何判斷數據模塊是否超出可視范圍
。
首先我們需要知道每個數據模塊的高度
,其實很簡單,只需要為每個模塊定義一個id
,然后在數據展示之后根據id
獲取到該模塊的節點信息
然后按順序存儲到數組中
即可
const pagesHeight = [] onReady(()=>{ setPageHeight() }) const upper = () => { ... nextTick(() => { // 每次獲取新數據都調用一下 setPageHeight(); }); }; const setPageHeight = () => { let query = uni.createSelectorQuery(); query .select(`#item-${currentShowPage.value}`) .boundingClientRect(res => { pagesHeight[currentShowPage.value] = res && res.height; }) .exec(); };
OK,現在我們已經知道每個模塊的高度
了,然后就是監聽模塊與可視窗口的交叉范圍
。這里有兩種方法,一種是JS獲取可視窗口的高度與模塊scrollTop進行差值計算
,另一種是使用小程序的createIntersectionObserver方法讓程序自行監聽交叉區域
這里我展示的是第二種方法,如果對第一種方法感興趣的朋友可以向上看第二章開頭我推薦的《微信小程序虛擬列表》文章
關于createIntersectionObserver方法
的使用其實很簡單,我們只需要把可視窗口的id
以及需要監聽的模塊id
傳入即可,詳情看官方文檔
onReady(() => { ... observer(currentShowPage.value); }); const upper = () => { ... nextTick(() => { // 每次獲取新數據都調用一下 observer(); }); }; // 允許渲染的數組下標,需要設置默認值 const visiblePagesList = ref([-1,0,1]) const observer = pageNum => { const observeView = wx .createIntersectionObserver() .relativeTo("#scroll", { top: 0, bottom: 0 }); observeView.observe(`#item-${pageNum}`, res => { if (res.intersectionRatio > 0) visiblePagesList.value = [pageNum - 1, pageNum, pageNum + 1]; }); };
最后就是在頁面中判斷該模塊是否允許被渲染(也就是是否存儲在visiblePagesList數組中)
,這里就很簡單了,只需要寫一個方法在頁面中調用即可
<scroll-view id="scroll" class="scroll" :scroll-y="true" @scrolltolower="upper"> <view v-for="(data, index) in scrollData" :key="index" :id="'item-' + index"> <template v-if="includePage(index)"> <view class="scroll-item" :style="{ ... }" v-for="item in data" :key="item.index" > {{ item.data }} </view> </template> <view v-else :style="{ height: pagesHeight[index] }"></view> </view> </scroll-view>
const includePage = index => { return visiblePagesList.value.indexOf(index) > -1; };
來看看效果如何
額...似乎沒有太大區別,那我們看看頁面結構到底也沒有將可視區域外的內容切換為空白view
成功!
3、功能調整
聊天室原本還有回底功能
等,也不能忘了加上
這個部分就比較簡單了,只需要直接使用scroll-view
的scroll-top屬性
,然后通過在scroll回調中動態記載scroll-top的值即可
下面是部分代碼
<scroll-view id="scroll" class="scroll" :scroll-y="true" :scroll-top="currentTop" @scroll="handle_scroll" @scrolltolower="upper" > ... </scroll-view> <view v-show="showGoBottom" class="go-back-btn" @click="handle_goBottom">回底</view>
let scrollTop; const currentTop = ref(0); const showGoBottom = ref(false); const handle_scroll = throttle(event => { scrollTop = event[0].detail.scrollTop; if (scrollTop > 300) { showGoBottom.value = true; } }, 100); const handle_goBottom = () => { currentTop.value = scrollTop; nextTick(() => { currentTop.value = 0; }); showGoBottom.value = false; };
大功告成~
最后附上demo倉庫
https://gitee.com/huang-qihao123/virtual-list-demo