197 行
5.5 KiB
Plaintext
197 行
5.5 KiB
Plaintext
<template>
|
|
<view style="flex: 1; background-color: aliceblue">
|
|
<view class="tips">list-view组件虽然在渲染层有recycle机制,但长列表的vnode/Element太多也会造成初始化卡顿。
|
|
本示例有2000条数据,通过分批创建列表项,减少初始化卡顿。并通过闲时创建机制避免影响列表滚动等UI操作</view>
|
|
<list-view style="flex: 1" :refresher-enabled="true" :refresher-triggered="refresherTriggered" @scroll="onScroll"
|
|
@scrolltolower="onScrollToLower" @refresherrefresh="onRefresh" @refresherpulling="onRefresherPulling" @refresherrestore="onRefresherRestore">
|
|
<list-item v-for="(item, index) in list" :key="index + '_' + item.id">
|
|
<view class="item">
|
|
<view><text>{{ item.name }}</text></view>
|
|
<view><text style="font-size: 12px; color: #999999">{{
|
|
item.info
|
|
}}</text></view>
|
|
</view>
|
|
</list-item>
|
|
</list-view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
/**
|
|
* @description: 支持把超长列表数据拆分成多个批次创建,降低首屏一次性创建过多列表项的卡顿。
|
|
* @logic: 页面先准备完整大数据源,再把数据拆成批次任务;滚动和下拉刷新时控制批次任务的暂停与恢复,逐步把数据插入列表。
|
|
* @optimization: 通过任务队列和闲时分批插入机制延后列表项创建,避免 2000 条数据一次性进入渲染层影响滚动流畅度。
|
|
*/
|
|
type Item = {
|
|
id : number;
|
|
name : string;
|
|
info : string;
|
|
};
|
|
|
|
class Jobs {
|
|
private jobs : (() => Promise<void>)[] = [];
|
|
paused : boolean = true;
|
|
constructor() { }
|
|
add(job : () => Promise<void>) {
|
|
this.jobs.push(job);
|
|
}
|
|
pause() {
|
|
this.paused = true;
|
|
}
|
|
private execute() {
|
|
if (this.paused) {
|
|
return;
|
|
}
|
|
if (this.jobs.length == 0) {
|
|
this.paused = true
|
|
return;
|
|
}
|
|
const job = this.jobs.shift();
|
|
if (job != null) {
|
|
job().then(() => {
|
|
this.execute();
|
|
});
|
|
}
|
|
}
|
|
resume() {
|
|
if(!this.paused) {
|
|
return
|
|
}
|
|
this.paused = false;
|
|
setTimeout(() => {
|
|
this.execute();
|
|
}, 0)
|
|
}
|
|
reset() {
|
|
this.jobs = [];
|
|
this.paused = true;
|
|
}
|
|
get done() : boolean {
|
|
return this.jobs.length == 0;
|
|
}
|
|
}
|
|
|
|
function delay(time : number) : Promise<void> {
|
|
return new Promise(resolve => {
|
|
setTimeout(() => {
|
|
resolve()
|
|
}, time)
|
|
})
|
|
}
|
|
|
|
const bigList = ref<Item[]>([]);
|
|
const list = ref<Item[]>([]);
|
|
const jobs = ref<Jobs>(new Jobs());
|
|
const batchSize = ref<number>(100);
|
|
let scrolling = false;
|
|
const refresherTriggered = ref<boolean>(false);
|
|
let scrollendTimeout = -1;
|
|
let pulling = false;
|
|
|
|
function init(autoResumeJobs: boolean = true) {
|
|
// 将数据分批放入任务队列
|
|
const batchCount = Math.ceil(bigList.value.length / batchSize.value);
|
|
for (let i = 0; i < batchCount; i++) {
|
|
const start = i * batchSize.value;
|
|
const end = Math.min(start + batchSize.value, bigList.value.length);
|
|
jobs.value.add(async () => {
|
|
list.value.push(...bigList.value.slice(start, end));
|
|
// 两批数据之间增加延迟,防止卡顿时间太久
|
|
await nextTick();
|
|
await delay(100)
|
|
});
|
|
}
|
|
if(autoResumeJobs) {
|
|
jobs.value.resume();
|
|
}
|
|
}
|
|
|
|
const onScrollEnd = () => {
|
|
scrolling = false;
|
|
// 滚动结束,继续执行分批加载逻辑
|
|
jobs.value.resume();
|
|
};
|
|
|
|
const onScroll = () => {
|
|
// 部分平台不支持scrollend事件,使用定时器模拟
|
|
clearTimeout(scrollendTimeout)
|
|
scrollendTimeout = setTimeout(() => {
|
|
onScrollEnd()
|
|
}, 100)
|
|
if (scrolling) {
|
|
return;
|
|
}
|
|
scrolling = true;
|
|
// 滚动期间暂停分批加载,保证滚动流畅度
|
|
jobs.value.pause();
|
|
};
|
|
|
|
const onScrollToLower = () => {
|
|
// 分批加载进行中,暂不执行滚动到底部加载更多数据的逻辑
|
|
if (!jobs.value.done) {
|
|
return;
|
|
}
|
|
// 加载更多数据
|
|
};
|
|
|
|
const onRefresh = () => {
|
|
// 下拉刷新触发,重置任务队列
|
|
jobs.value.reset();
|
|
refresherTriggered.value = true
|
|
setTimeout(() => {
|
|
refresherTriggered.value = false;
|
|
list.value.splice(0, list.value.length);
|
|
/**
|
|
* refreshRestore事件会触发继续分批加载,此处init不自动调用resume。这样做能减少一些下拉刷新复位期间加载数据引发的卡顿。
|
|
* TODO 清空列表也会导致下拉刷新复位时发生卡顿,后续再优化
|
|
*/
|
|
init(false);
|
|
}, 500)
|
|
};
|
|
|
|
const onRefresherPulling = () => {
|
|
if(pulling) {
|
|
return
|
|
}
|
|
pulling = true
|
|
// 在下拉刷新时暂停分批加载,避免影响刷新操作
|
|
jobs.value.pause()
|
|
};
|
|
|
|
const onRefresherRestore = () => {
|
|
pulling = false
|
|
// 下拉刷新结束后恢复分批加载
|
|
jobs.value.resume();
|
|
};
|
|
|
|
// 模拟大列表数据 - 相当于 created 生命周期
|
|
for (let i = 0; i < 2000; i++) {
|
|
bigList.value.push({
|
|
id: i,
|
|
name: `Wifi_` + i,
|
|
info: `信号强度: -${Math.floor(Math.random() * 60) + 40
|
|
} db, 安全性: WPA/WPA2/WPA3-Personal`,
|
|
} as Item);
|
|
}
|
|
|
|
onReady(() => {
|
|
init();
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.tips {
|
|
margin: 10px;
|
|
border-radius: 5px;
|
|
padding: 10px;
|
|
background-color: white;
|
|
}
|
|
|
|
.item {
|
|
margin: 5px 10px;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
background-color: white;
|
|
}
|
|
</style>
|