利用虚拟列表改造索引列表(IndexList)
引言
在一个倡导“快速开发”的团队中,交付日期往往是衡量工作的第一标准。而遇到问题的解决方式也会偏暴力,暴力的方式往往大脑都会厌恶和失声,尤其是在面试官问开发过程中的难点
的时候更是无法回答,只能无底气的回一句“感觉开发过程很顺利,并没有碰到什么难以解决的问题。”。
以下便是我想到的非暴力方式来改造原有问题。
问题
需求与问题描述
关键词:在进行小程序
、index list
、卡顿
、白屏
、500条
、1M
小程序
项目开发过程中,遇到索引列表的需求,于是采用vant
的IndexBar
作为进行开发,完成并发布线上,但是由于item编写的确实不行以及小程序承载最终将问题暴露了出来。经过测试发现当数据大于500条
时部分手机已经开始出现卡顿
情况,其中对item操作(删除、增加)时卡顿明显;当数据大小大于1MB
后更是出现首屏渲染长时间的白屏
。IndexList如下图所示。文章图片
问题分析
由于表达能力弱上方描述可能不太清楚,所以将关键词提取出来。
小程序
: 项目环境index list
: 需求卡顿/白屏
: 问题500条/1M
: 产生问题的前提
第一条
。搜索结果如下图所示。文章图片
2018的提出问题,2019年官方给出了解决方案recycle-view微信小程序长列表卡顿,但是这个只能解决部分问题,对于嵌套数据可能并不能适配。而且内部实现也是按虚拟列表渲染的思路去操作的。
方案和实现 在后续方案实现细节和环境将换成浏览器环境并采用Vue进行编码。
ps: vite + vue 在写demo方面实在是太丝滑了。
前提
采用小程序开发工具进行编码对个人来说较为难受,考虑到方案和实现以及迁移都成本相对低,所以后续实现采用浏览器实现后移植小程序。
开发环境: vscode + vite + vue
mock数据 domo环境,采用mock数据为后续开发提供数据支持。
ps: 暂时不考虑keys顺序问题
mock结构
{
"A":[ ... ],
...
"Z":[ ... ]
}
mock生成代码如下。
import { Random } from 'mockjs'export const indexListData = https://www.it610.com/article/Array(26).fill('A'.codePointAt()).reduce((pv, indexCode, index) => {
const currentCharAt = indexCode + index
const currentChar = String.fromCharCode(currentCharAt)
pv[currentChar] = Array(Math.random() * 460 | 0).fill(0).map((_, itemIndex) => {
const id = currentCharAt + '-' + itemIndex
return {
id,
index: currentChar,
pic: "https://image.notbucai.com/logo.png",
title: Random.ctitle(5, 20),
group: id,
content: Random.ctitle(100, 150),
user: {
id: 123,
name: '不才',
avatar: 'https://image.notbucai.com/logo.png',
age: 12,
sex: 1,
},
createAt: Date.now(),
updateAt: Date.now(),
}
})
return pv;
}, {})
业务代码 渲染图
文章图片
没有改造之前的代码。
只做部分实现,未完全实现
{{ key }}
文章图片
{{ item.title }}【利用虚拟列表改造索引列表(IndexList)】{{ item.content }}
{{ key }}* {
padding: 0;
margin: 0;
}
.list-page-box {
position: relative;
}
.list-box {
.group-box {
margin-bottom: 24px;
.gropu-index {
background-color: #f4f5f6;
padding: 10px;
font-weight: bold;
position: sticky;
top: 0;
}
.group-content {
.group-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
.group-item-pic {
width: 68px;
min-width: 68px;
height: 68px;
margin-right: 12px;
}
.group-item-content {
display: flex;
flex-direction: column;
height: 100%;
h1 {
font-size: 16px;
font-weight: bold;
color: #333333;
}
p {
color: #666666;
font-size: 14px;
}
}.group-item-aciton {
min-width: 60px;
display: flex;
align-items: center;
justify-content: end;
}
}
}
}
}
.index-field-box {
position: fixed;
top: 0;
right: 0;
z-index: 10;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
方案
采用虚拟列表,参考云中桥-「前端进阶」高性能渲染十万条数据(虚拟列表)的方案。
文章图片
根据上面对虚拟列表的描述,编写了一个简单的虚拟列表,代码如下。
{{ item }}.list-page-box {
position: relative;
height: 100vh;
width: 100vw;
overflow: hidden;
overflow-y: auto;
}
.list-box {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.item {
padding: 30px;
border-bottom: 1px solid #000;
}
改造难点
在这个改造中主要问题就是当前是一个
嵌套的数据列表
。- 需要将原来单层结构改造成双层结构
- 偏移方案,transform 对 sticky 有冲突
- index key的高度问题
- 可视区域多个 index list item
- 点击右侧Index Key跳转到指定位置
通过上方虚拟列表代码进行后续的改造和实现,这里先放实现代码,后面将分别解决上述问题。
{{ key }}{{ item.title }}{{ item.content }}
{{ key }}* {
padding: 0;
margin: 0;
}
.list-page-box {
position: relative;
height: 100vh;
width: 100vw;
overflow: hidden;
overflow-y: auto;
}.list-box {
position: absolute;
top: 0;
left: 0;
right: 0;
.group-box {
/* padding-top: 24px;
*/
box-sizing: border-box;
.gropu-index {
background-color: #f4f5f6;
padding: 10px;
font-weight: bold;
// todo bug
position: sticky;
top: 0;
height: 42px;
box-sizing: border-box;
}
.group-content {
.group-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
// 固定的高度
height: 80px;
box-sizing: border-box;
/* 不做其他处理 保证高度一致 */
overflow: hidden;
.group-item-pic {
width: 68px;
min-width: 68px;
height: 68px;
margin-right: 12px;
}
.group-item-content {
display: flex;
flex-direction: column;
height: 100%;
h1 {
font-size: 16px;
font-weight: bold;
color: #333333;
}
p {
color: #666666;
font-size: 14px;
}
}.group-item-aciton {
min-width: 60px;
display: flex;
align-items: center;
justify-content: end;
}
}
}
}
}
.index-field-box {
position: fixed;
top: 0;
right: 0;
z-index: 10;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px;
}
难点解决
渲染位置和偏移位置 由于是双层数据中单个IndexList包含Index和List的高度,所以在拿到数据后先对数据高度进行预测,这里预测方式为固定的item和key高度。
前提: item 高度为80,index 高度为42;
这里后续可以先进行预渲染然后拿到渲染的高度。
高度计算
// 总高度 用于固定 scroll height
let total = 0;
// 循环计算所有高度
for (let key in originList) {
const value = https://www.it610.com/article/originList[key]
// 记录所欲key 用于右侧列表的渲染
keys.value.push(key)
// 缓存
originListHeight.value.push({
index: 42,
list: value.length * 80,
total: value.length * 80 + 42,
key
})
total += value.length * 80 + 42
}
scrollHeight.value = total
对于渲染数据的计算是根据滚动位置和数据高度。对于渲染数据来说,双层数据只需要分别计算出第一层和第二层的数据下标即可。
对于第一层只需要计算滚动高和数据高度的大小即可得到。
第二层位置拿到与第一层数据高度和滚动高度的差额再除去单个元素的高度。
// 只关注 scrollTop 的变化
watchEffect(() => {
// 分离一下 计算过程 减少列表更新 无意义渲染
// 这里主要计算 index
if (originListHeight.value.length == 0) {
// 分别赋值 减少无意义的list渲染
showListIndexs.key = 'A'
showListIndexs.index = 0
showListIndexs.sonIndex = 0
return
}// 找到第一层数据位置
let total = 0;
let index = originListHeight.value.findIndex(item => {
// 找到高度和比当前滚动高度 大的第一个
let t = total + item.total
if (t > scrollTop.value) {
return true;
}
total = t;
return false;
});
// 处理 首次 top 为0的情况
// todo 这里还有点小问题 晚点说明
if (index === -1) return {
key: 'A',
sonIndex: 0
};
const key = originListHeight.value[index].key;
// total 为最近的
const sonListTop = scrollTop.value - total
// 得到子列表开始下标
const sonIndex = sonListTop / 80 | 0
// console.log('sonIndex',sonIndex);
// 计算偏移 ok
offsetTop.value = https://www.it610.com/article/total + sonIndex * 80;
showListIndexs.key = key
showListIndexs.index = index
showListIndexs.sonIndex = sonIndex
}, [scrollTop])
渲染数据的计算 采用计算属性根据
showListIndexs
的变化来进行更新,通过scrollTop计算位置后,拿到一二层下标进行数据截取,不过滚动位置的变化导致第二层数据可能无法满足渲染整个可视区域。所以需要额外的数据补充的计算,这里补充计算暂时只做两层。// 需要渲染的数据
const list = computed(() => {
// 获取key
const { key, index, sonIndex } = showListIndexs;
// 获取数据
// todo 这里的10个元素 后期需要进行计算 目前无所谓
const showList = originList[key].slice(sonIndex, sonIndex + 10)
// todo 实际上目前的key: value的机构还是有些问题的(无序),这个暂时按下不表
const showData = https://www.it610.com/article/{
[key]: showList
}
// 计算 数据长度不够时的处理
// todo 需要再细致化 需要一个循环
if (showList.length < 10) {
// 处理 数据不够时的问题
const nextIndex = index + 1if (nextIndex>= originListHeight.value.length) return showData
const nextHeightData = https://www.it610.com/article/originListHeight.value[nextIndex];
if (!nextHeightData) return showData;
const nextKey = nextHeightData.key;
const nextShowList = originList[nextKey].slice(0, 10 - showList.length)
showData[nextKey] = nextShowList
}return showData
})
右侧点击跳转 由于提前对预渲染高度进行了计算,所以这个问题约等于不存在。
// 由于数据加载后已经对预渲染的高度进行了一个计算
// 所以这里只要改变滚动的高度即可完成其他所有操作
if (!scrollRef.value) return;
// 计算高度
let height = 0;
const heightData = https://www.it610.com/article/originListHeight.value.find(item => {
if (item.key === key) return true;
height += item.total;
return false;
})
if (!heightData) return;
scrollRef.value.scrollTo(0, height)
移植问题 只需要替换监听和滚动位置,即可完成大体功能的移植。所以这里不做细节的描述。
参考 前端进阶」高性能渲染十万条数据(虚拟列表)
推荐阅读
- 虚拟DOM-Diff算法详解
- Spring|Spring Boot 自动配置的原理、核心注解以及利用自动配置实现了自定义 Starter 组件
- 【万伽复利】什么是复利(如何利用复利赚钱?)
- 苹果手机如何利用库乐队自制铃声
- Xshell5|Xshell5 远程连接本地虚拟机Ubuntu16
- 废材自救记录|虚拟机-无法安装vmware tools的解决方法
- HTML5学习之ul|HTML5学习之ul li列表实战
- “没有利用价值的人是很受冷遇的。”
- Ubuntu|Ubuntu 16.04(LTS)---- 系统安装(1)
- list