知識(shí)
不管是網(wǎng)站,軟件還是小程序,都要直接或間接能為您產(chǎn)生價(jià)值,我們?cè)谧非笃湟曈X表現(xiàn)的同時(shí),更側(cè)重于功能的便捷,營銷的便利,運(yùn)營的高效,讓網(wǎng)站成為營銷工具,讓軟件能切實(shí)提升企業(yè)內(nèi)部管理水平和效率。優(yōu)秀的程序?yàn)楹笃谏?jí)提供便捷的支持!
您當(dāng)前位置>首頁 » 新聞資訊 » 小程序相關(guān) >
在微信小程序中實(shí)現(xiàn)virtual-list
發(fā)表時(shí)間:2021-1-5
發(fā)布人:葵宇科技
瀏覽次數(shù):68
背景
小程序在很多場景下面會(huì)遇到長列表的交互,當(dāng)一個(gè)頁面渲染過多的wxml節(jié)點(diǎn)的時(shí)候,會(huì)造成小程序頁面的卡頓和白屏。原因主要有以下幾點(diǎn):
1.列表數(shù)據(jù)量大,初始化setData和初始化渲染列表wxml耗時(shí)都比較長;
2.渲染的wxml節(jié)點(diǎn)比較多,每次setData更新視圖都需要?jiǎng)?chuàng)建新的虛擬樹,和舊樹的diff操作耗時(shí)比較高;
3.渲染的wxml節(jié)點(diǎn)比較多,page能夠容納的wxml是有限的,占用的內(nèi)存高。
微信小程序本身的scroll-view沒有針對(duì)長列表做優(yōu)化,官方組件recycle-view就是一個(gè)類似virtual-list的長列表組件?,F(xiàn)在我們要剖析虛擬列表的原理,從零實(shí)現(xiàn)一個(gè)小程序的virtual-list。
實(shí)現(xiàn)原理
首先我們要了解什么是virtual-list,這是一種初始化只加載「可視區(qū)域」及其附近dom元素,并且在滾動(dòng)過程中通過復(fù)用dom元素只渲染「可視區(qū)域」及其附近dom元素的滾動(dòng)列表前端優(yōu)化技術(shù)。相比傳統(tǒng)的列表方式可以到達(dá)極高的初次渲染性能,并且在滾動(dòng)過程中只維持超輕量的dom結(jié)構(gòu)。
虛擬列表最重要的幾個(gè)概念:
-
可滾動(dòng)區(qū)域:比如列表容器的高度是600,內(nèi)部元素的高度之和超過了容器高度,這一塊區(qū)域就可以滾動(dòng),就是「可滾動(dòng)區(qū)域」;
-
可視區(qū)域:比如列表容器的高度是600,右側(cè)有縱向滾動(dòng)條可以滾動(dòng),視覺可見的內(nèi)部區(qū)域就是「可視區(qū)域」。
實(shí)現(xiàn)虛擬列表的核心就是監(jiān)聽scroll事件,通過滾動(dòng)距離offset和滾動(dòng)的元素的尺寸之和totalSize動(dòng)態(tài)調(diào)整「可視區(qū)域」數(shù)據(jù)渲染的頂部距離和前后截取索引值,實(shí)現(xiàn)步驟如下:
1.監(jiān)聽scroll事件的scrollTop/scrollLeft,計(jì)算「可視區(qū)域」起始項(xiàng)的索引值startIndex和結(jié)束項(xiàng)索引值endIndex;
2.通過startIndex和endIndex截取長列表的「可視區(qū)域」的數(shù)據(jù)項(xiàng),更新到列表中;
3.計(jì)算可滾動(dòng)區(qū)域的高度和item的偏移量,并應(yīng)用在可滾動(dòng)區(qū)域和item上。
1.列表項(xiàng)的寬/高和滾動(dòng)偏移量
在虛擬列表中,依賴每一個(gè)列表項(xiàng)的寬/高來計(jì)算「可滾動(dòng)區(qū)域」,而且可能是需要自定義的,定義itemSizeGetter函數(shù)來計(jì)算列表項(xiàng)寬/高。
itemSizeGetter(itemSize) {
return (index: number) => {
if (isFunction(itemSize)) {
return itemSize(index);
}
return isArray(itemSize) ? itemSize[index] : itemSize;
};
}
復(fù)制代碼
滾動(dòng)過程中,不會(huì)計(jì)算沒有出現(xiàn)過的列表項(xiàng)的itemSize,這個(gè)時(shí)候會(huì)使用一個(gè)預(yù)估的列表項(xiàng)estimatedItemSize,目的就是在計(jì)算「可滾動(dòng)區(qū)域」高度的時(shí)候,沒有測(cè)量過的itemSize用estimatedItemSize代替。
getSizeAndPositionOfLastMeasuredItem() {
return this.lastMeasuredIndex >= 0
? this.itemSizeAndPositionData[this.lastMeasuredIndex]
: { offset: 0, size: 0 };
}
getTotalSize(): number {
const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();
return (
lastMeasuredSizeAndPosition.offset +
lastMeasuredSizeAndPosition.size +
(this.itemCount - this.lastMeasuredIndex - 1) * this.estimatedItemSize
);
}
復(fù)制代碼
這里看到了是直接通過緩存命中最近一個(gè)計(jì)算過的列表項(xiàng)的itemSize和offset,這是因?yàn)樵讷@取每一個(gè)列表項(xiàng)的兩個(gè)參數(shù)時(shí)候,都對(duì)其做了緩存。
getSizeAndPositionForIndex(index: number) {
if (index > this.lastMeasuredIndex) {
const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();
let offset =
lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size;
for (let i = this.lastMeasuredIndex + 1; i <= index; i++) {
const size = this.itemSizeGetter(i);
this.itemSizeAndPositionData[i] = {
offset,
size,
};
offset += size;
}
this.lastMeasuredIndex = index;
}
return this.itemSizeAndPositionData[index];
}
復(fù)制代碼
2.根據(jù)偏移量搜索索引值
在滾動(dòng)過程中,需要通過滾動(dòng)偏移量offset計(jì)算出展示在「可視區(qū)域」首項(xiàng)數(shù)據(jù)的索引值,一般情況下可以從0開始計(jì)算每一列表項(xiàng)的itemSize,累加到一旦超過offset,就可以得到這個(gè)索引值。但是在數(shù)據(jù)量太大和頻繁觸發(fā)的滾動(dòng)事件中,會(huì)有較大的性能損耗。好在列表項(xiàng)的滾動(dòng)距離是完全升序排列的,所以可以對(duì)已經(jīng)緩存的數(shù)據(jù)做二分查找,把時(shí)間復(fù)雜度降低到 O(lgN) 。
js代碼如下:
findNearestItem(offset: number) {
offset = Math.max(0, offset);
const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();
const lastMeasuredIndex = Math.max(0, this.lastMeasuredIndex);
if (lastMeasuredSizeAndPosition.offset >= offset) {
return this.binarySearch({
high: lastMeasuredIndex,
low: 0,
offset,
});
} else {
return this.exponentialSearch({
index: lastMeasuredIndex,
offset,
});
}
}
private binarySearch({
low,
high,
offset,
}: {
low: number;
high: number;
offset: number;
}) {
let middle = 0;
let currentOffset = 0;
while (low <= high) {
middle = low + Math.floor((high - low) / 2);
currentOffset = this.getSizeAndPositionForIndex(middle).offset;
if (currentOffset === offset) {
return middle;
} else if (currentOffset < offset) {
low = middle + 1;
} else if (currentOffset > offset) {
high = middle - 1;
}
}
if (low > 0) {
return low - 1;
}
return 0;
}
復(fù)制代碼
對(duì)于搜索沒有緩存計(jì)算結(jié)果的查找,先使用指數(shù)查找縮小查找范圍,再使用二分查找。
private exponentialSearch({
index,
offset,
}: {
index: number;
offset: number;
}) {
let interval = 1;
while (
index < this.itemCount &&
this.getSizeAndPositionForIndex(index).offset < offset
) {
index += interval;
interval *= 2;
}
return this.binarySearch({
high: Math.min(index, this.itemCount - 1),
low: Math.floor(index / 2),
offset,
});
}
}
復(fù)制代碼
3.計(jì)算startIndex、endIndex
我們知道了「可視區(qū)域」尺寸containerSize,滾動(dòng)偏移量offset,在加上預(yù)渲染的條數(shù)overscanCount進(jìn)行調(diào)整,就可以計(jì)算出「可視區(qū)域」起始項(xiàng)的索引值startIndex和結(jié)束項(xiàng)索引值endIndex,實(shí)現(xiàn)步驟如下:
1.找到距離offset最近的索引值,這個(gè)值就是起始項(xiàng)的索引值startIndex;
2.通過startIndex獲取此項(xiàng)的offset和size,再對(duì)offset進(jìn)行調(diào)整;
3.offset加上containerSize得到結(jié)束項(xiàng)的maxOffset,從startIndex開始累加,直到越過maxOffset,得到結(jié)束項(xiàng)索引值endIndex。
js代碼如下:
getVisibleRange({
containerSize,
offset,
overscanCount,
}: {
containerSize: number;
offset: number;
overscanCount: number;
}): { start?: number; stop?: number } {
const maxOffset = offset + containerSize;
let start = this.findNearestItem(offset);
const datum = this.getSizeAndPositionForIndex(start);
offset = datum.offset + datum.size;
let stop = start;
while (offset < maxOffset && stop < this.itemCount - 1) {
stop++;
offset += this.getSizeAndPositionForIndex(stop).size;
}
if (overscanCount) {
start = Math.max(0, start - overscanCount);
stop = Math.min(stop + overscanCount, this.itemCount - 1);
}
return {
start,
stop,
};
}
復(fù)制代碼
3.監(jiān)聽scroll事件,實(shí)現(xiàn)虛擬列表滾動(dòng)
現(xiàn)在可以通過監(jiān)聽scroll事件,動(dòng)態(tài)更新startIndex、endIndex、totalSize、offset,就可以實(shí)現(xiàn)虛擬列表滾動(dòng)。
js代碼如下:
getItemStyle(index) {
const style = this.styleCache[index];
if (style) {
return style;
}
const { scrollDirection } = this.data;
const {
size,
offset,
} = this.sizeAndPositionManager.getSizeAndPositionForIndex(index);
const cumputedStyle = styleToCssString({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
[positionProp[scrollDirection]]: offset,
[sizeProp[scrollDirection]]: size,
});
this.styleCache[index] = cumputedStyle;
return cumputedStyle;
},
observeScroll(offset: number) {
const { scrollDirection, overscanCount, visibleRange } = this.data;
const { start, stop } = this.sizeAndPositionManager.getVisibleRange({
containerSize: this.data[sizeProp[scrollDirection]] || 0,
offset,
overscanCount,
});
const totalSize = this.sizeAndPositionManager.getTotalSize();
if (totalSize !== this.data.totalSize) {
this.setData({ totalSize });
}
if (visibleRange.start !== start || visibleRange.stop !== stop) {
const styleItems: string[] = [];
if (isNumber(start) && isNumber(stop)) {
let index = start - 1;
while (++index <= stop) {
styleItems.push(this.getItemStyle(index));
}
}
this.triggerEvent('render', {
startIndex: start,
stopIndex: stop,
styleItems,
});
}
this.data.offset = offset;
this.data.visibleRange.start = start;
this.data.visibleRange.stop = stop;
},
復(fù)制代碼
在調(diào)用的時(shí)候,通過render事件回調(diào)出來的startIndex, stopIndex,styleItems,截取長列表「可視區(qū)域」的數(shù)據(jù),在把列表項(xiàng)目的itemSize和offset通過絕對(duì)定位的方式應(yīng)用在列表上
代碼如下:
let list = Array.from({ length: 10000 }).map((_, index) => index);
Page({
data: {
itemSize: index => 50 * ((index % 3) + 1),
styleItems: null,
itemCount: list.length,
list: [],
},
onReady() {
this.virtualListRef =
this.virtualListRef || this.selectComponent('#virtual-list');
},
slice(e) {
const { startIndex, stopIndex, styleItems } = e.detail;
this.setData({
list: list.slice(startIndex, stopIndex + 1),
styleItems,
});
},
loadMore() {
setTimeout(() => {
const appendList = Array.from({ length: 10 }).map(
(_, index) => list.length + index,
);
list = list.concat(appendList);
this.setData({
itemCount: list.length,
list: this.data.list.concat(appendList),
});
}, 500);
},
});
復(fù)制代碼
<view class="container">
<virtual-list scrollToIndex="{{ 16 }}" lowerThreshold="{{50}}" height="{{ 600 }}" overscanCount="{{10}}" item-count="{{ itemCount }}" itemSize="{{ itemSize }}" estimatedItemSize="{{100}}" bind:render="slice" bind:scrolltolower="loadMore">
<view wx:if="{{styleItems}}">
<view wx:for="{{ list }}" wx:key="index" style="{{ styleItems[index] }};line-height:50px;border-bottom:1rpx solid #ccc;padding-left:30rpx">{{ item + 1 }}view>
view>
virtual-list>
{{itemCount}}
view>
復(fù)制代碼
參考資料
在寫這個(gè)微信小程序的virtual-list組件過程中,主要參考了一些優(yōu)秀的開源虛擬列表實(shí)現(xiàn)方案:
- react-tiny-virtual-list
- react-virtualized
- react-window
總結(jié)
通過上述解釋已經(jīng)初步實(shí)現(xiàn)了在微信小程序環(huán)境中實(shí)現(xiàn)了虛擬列表,并且對(duì)虛擬列表的原理有了更加深入的了解。但是對(duì)于瀑布流布局,列表項(xiàng)尺寸不可預(yù)測(cè)等場景依然無法適用。在快速滾動(dòng)過程中,依然會(huì)出現(xiàn)來不及渲染而白屏,這個(gè)問題可以通過增加「可視區(qū)域」外預(yù)渲染的item條數(shù)overscanCount來得到一定的緩解。
作者:用戶8824932366654
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
相關(guān)案例查看更多
相關(guān)閱讀
- 網(wǎng)站開發(fā)哪家好
- 云南建設(shè)廳官方網(wǎng)站
- 網(wǎng)站建設(shè)高手
- 網(wǎng)站優(yōu)化哪家好
- 云南電商網(wǎng)站建設(shè)
- vue開發(fā)小程序
- 云南網(wǎng)站建設(shè)方法
- 小程序定制
- 云南小程序開發(fā)公司推薦
- 網(wǎng)站建設(shè)專家
- 汽車報(bào)廢系統(tǒng)
- 生成海報(bào)
- 報(bào)廢車管理系統(tǒng)
- 網(wǎng)站建設(shè)價(jià)格
- 網(wǎng)站建設(shè)專業(yè)品牌
- 昆明網(wǎng)站設(shè)計(jì)
- 迪慶小程序開發(fā)
- 北京小程序開發(fā)
- 云南網(wǎng)站建設(shè) 網(wǎng)絡(luò)服務(wù)
- 云南小程序開發(fā)費(fèi)用
- 汽車拆解管理系統(tǒng)
- 云南網(wǎng)絡(luò)推廣
- web學(xué)習(xí)路線
- 汽車報(bào)廢拆解管理系統(tǒng)
- 網(wǎng)站建設(shè)制作
- 云南花農(nóng)小程序
- 云南旅游網(wǎng)站建設(shè)
- 昆明網(wǎng)站制作
- 網(wǎng)站開發(fā)
- 網(wǎng)站沒排名