RecycleScroller虛擬列表的實(shí)戰(zhàn)。
背景
當(dāng)渲染大型列表時(shí),由于瀏覽器需要處理大量的 DOM 節(jié)點(diǎn),會(huì)帶來頁(yè)面卡頓、內(nèi)存占用過高等問題。此時(shí)可以通過將列表虛擬化技術(shù),即只渲染可見區(qū)域內(nèi)的數(shù)據(jù)項(xiàng),而不是全部渲染,且在滾動(dòng)過程中移除舊的添加新的,這樣無論列表有多少項(xiàng),只會(huì)有一小部分在DOM中,從而提高性能和內(nèi)存效率、保持流暢的滾動(dòng)體驗(yàn)。
vue-virtual-scroller正是一個(gè)實(shí)現(xiàn)了列表和表格虛擬滾動(dòng)的社區(qū)庫(kù),主要提供兩個(gè)組件:
-
RecycleScroller:一個(gè)基礎(chǔ)的虛擬滾動(dòng)列表,需要item高度是固定的 -
DynamicScroller:一個(gè)更高級(jí)的虛擬滾動(dòng)列表,可以處理動(dòng)態(tài)的item高度
這里以RecycleScroller為例,先安裝和導(dǎo)入:
// 安裝
npm install --save vue-virtual-scroller@next
// 導(dǎo)入RecycleScroller
import { RecycleScroller } from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
app.component("RecycleScroller", RecycleScroller);
RecycleScroller實(shí)戰(zhàn)
RecycleScroller提供很多屬性,主要使用到以下四個(gè)核心屬性,接下來依次解釋:
<RecycleScroller
v-slot="{ item, index, active }"
:items="items"
:item-size=""
key-field=""
>
<div class="item-root">
...
</div>
</RecycleScroller>
v-slot
當(dāng)前渲染item的相關(guān)信息,提供信息包括:
-
item:當(dāng)前渲染的item數(shù)據(jù),即items[index] -
index: 當(dāng)前渲染的item所在位置 -
active:當(dāng)前渲染的item是否可見
items
必填,渲染的列表數(shù)據(jù)源。注意,如果數(shù)據(jù)源有變化,比如上拉刷新、下拉加載更多,此時(shí)需要修改items屬性,否則RecycleScroller不會(huì)更新items,參考官方demo的做法:
const items = computed(() => {
return props.list.map((item) =>
Object.assign({}, { random: Math.random() }, item),
);
});
item-size
必填,item的高度。若有必要需適配手機(jī)端和網(wǎng)頁(yè)端。
key-field
關(guān)鍵屬性名,這個(gè)屬性是item唯一標(biāo)識(shí),默認(rèn)名稱是id。一般在數(shù)據(jù)會(huì)有提供如key、id、index這樣的屬性,如果都沒有的話,需要對(duì)源數(shù)據(jù)進(jìn)行加工補(bǔ)充一個(gè)id屬性。如果不存在這個(gè)屬性或?qū)傩灾挡晃ㄒ唬瑫?huì)發(fā)現(xiàn)列表存在空白項(xiàng),即渲染異常的item。
const items = computed(() => {
return props.list.map((item, index) =>
Object.assign({}, { id: `test-${index}` }, item),
);
});
實(shí)際應(yīng)用一下,現(xiàn)在有這樣一個(gè)長(zhǎng)列表,可以看到dom樹很長(zhǎng):

相關(guān)代碼如下:
<div class="output-content-container">
<div
v-for="(item) in props.data"
class="output-item-root"
>
...
</div>
</div>
.output-content-container {
width: 100%;
display: flex;
flex-direction: column;
}
改用RecycleScroller僅需要改幾行代碼:
<RecycleScroller
v-slot="{ item }"
class="output-content-container"
:item-size="isMobileRef ? 100 : 75"
:items="items"
>
<div class="output-item-root">
...
</div>
</RecycleScroller>
const items = computed(() => {
return props.data.map((item, index) =>
Object.assign({}, { id: `data-id-${index}` }, item),
);
});
.output-content-container {
width: 100%;
max-height: 100vh;
}
需要強(qiáng)調(diào)的一點(diǎn)是,RecycleScroller的可視高度一定是可知的,比如這里設(shè)置了最大高度和屏幕高度一樣,否則它無法計(jì)算出來哪些列表項(xiàng)是在可視區(qū)內(nèi)的,會(huì)認(rèn)為全部能展示,導(dǎo)致復(fù)用能力失效。改完后能成功復(fù)用了:

template
在這個(gè)例子中還有一處細(xì)節(jié),就是滑動(dòng)效果發(fā)生變化了,可以看到原來只有一個(gè)頁(yè)面整體的滑動(dòng)條(右),現(xiàn)在內(nèi)部多了一個(gè)的滑動(dòng)條(左):

這個(gè)很好理解,RecycleScroller本身也要知道渲染視圖和滾動(dòng)位置,如果想和原有保持一致,也就是將列表上方的信息和列表拼接一起成為一個(gè)整體,只要將頭部信息填空到#before中即可,代碼如下:
<RecycleScroller
class="output-content-container"
:item-size="isMobileRef ? 100 : 75"
:items="items"
>
<template #before>
...
</template>
<template v-slot="{ item }">
<div class="output-item-root">
...
</div>
</template>
</RecycleScroller>
網(wǎng)格布局
RecycleScroller還可以實(shí)現(xiàn)網(wǎng)格布局,還要涉及兩個(gè)關(guān)鍵屬性:
-
item-secondary-size:item寬度,未設(shè)置時(shí)會(huì)使用item-size的值 -
grid-items:一行展示的個(gè)數(shù),即列數(shù)
比如優(yōu)化這樣一個(gè)網(wǎng)格列表:

修改前:
<div class="content-gridview">
<div
v-for="(item, index) in items"
class="content-grid-item"
@click="onChooseItem(item)"
>
...
</div>
</div>
.content-gridview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 10px;
max-height: 600px;
}
修改后:
<RecycleScroller
v-slot="{ item, index }"
:item-secondary-size="360"
:item-size="90"
:items="items"
:grid-items="2"
key-field="indexId"
class="content-gridview"
>
<div class="content-grid-item" @click="onChooseItem(item)">
...
</div>
</RecycleScroller>
.content-gridview {
max-height: 600px;
}
.content-grid-item {
...
margin-right: 10px;
}
由于不支持設(shè)置grid-gap,這里通過給item設(shè)置右間距方式實(shí)現(xiàn)的,最終效果:
