当前位置: 首页 > news >正文

列表首屏毫秒级加载与自动滚动定位方案

引用自 摸鱼wiki

场景

<template><div ref="commentsRef"><divv-for="comment in displayComments":key="comment.id":data-cell-id="comment.id"class="card">{{ comment.data }}</div></div>
</template><script lang="ts" setup>
// 假设comment有2000条数据,首次只加载200条
const comments = ref<{ id: string; data: string }[]>();
const displayComments = computed(() => comments.slice(0, 200));
</script>

说明:需要一个场景,用户首屏需要渲染200条数据,且后续如果comments列表发生变动时,页面上能够保持当前显示的条目不进行滚动。

分批加载

如果直接全量数据加载,200个节点肯定不能在1帧内渲染完毕,这样会出现较长时间的白屏。这时候可以使用时间分片的思路,在每个小时间段内渲染少量节点,提高首屏的渲染效率。

<template><div><divv-for="comment in displayComments.slice(0, loadCount)":key="comment.id">{{ comment.data }}</div></div>
</template><script lang="ts" setup>
// 假设comment有2000条数据,首次只加载200条
const commentsRef = ref();
const comments = ref<{ id: string; data: string }[]>();
const displayComments = computed(() => comments.slice(0, 200));// 首屏动态加载的数量
const loadCount = ref<number>(0);
// 如果少于20条,直接全量加载;否则每隔4ms渲染40条数据
watch(displayComments, () => {clearInterval(timer);loadCount.value = 20;if (loadCount.value < nv.length) {timer = setInterval(() => {loadCount.value = Math.min(loadCount.value + 40, nv.length);if (loadCount.value >= nv.length) {clearInterval(timer);}}, 4);}
})
</script>

滚动定位

实现在列表数据更新时自动定位到用户当前可视区域,尽可能保证可视区域的内容不因数据更新而发生剧烈抖动。

获取当前可视内容

通过浏览器提供的 IntersectionObserver API 监听列表中的元素,获取元素滚动时在可视区域内的元素,记录他们的id。

const visibleCardIds: Set<string> = new Set();
const intersection: IntersectionObserver = new IntersectionObserver((entries) => {entries.forEach((e) => {const id = e.target.getAttribute('data-cell-id');if (!id) return;if (e.isIntersecting && e.intersectionRatio > 0.91) {visibleCardIds.add(id);} else {visibleCardIds.delete(id);}});},{threshold: [0, 0.9, 1],},
);const addIntersectionObserve = () => {visibleCardIds.clear();intersection?.disconnect();nextTick(() => {const nodes = document.body.querySelectorAll('.card') || [];for (const node of nodes) {intersection?.observe(node);}});
};

滚动处理

从上一次拿到的可视区域元素中拿到在更新后的数据中仍存在的、最靠近顶部的节点,记录它的位置,并把容器节点滚动到该节点的 scrollTop 高度。

const getNearestVisibleId = () => {return displayComment.value.find(c => visibleCardIds.has(c.id));
}const silenceScroll = () => {const id = getNearestVisibleId();nextTick(() => {commentScrollTop(id);});
};const commentScrollTop = (id: string) => {const n = commentsRef.value?.querySelector(`[data-cell-id="${id}"]`,) as HTMLElement;if (n) {commentsRef.value?.scrollTo({top: n.offsetTop,behavior: 'instant',});}
};

结合起来

在数据更新后的下一帧触发滚动即可实现定位效果

<template><div><divv-for="comment in displayComments.slice(0, loadCount)":key="comment.id">{{ comment.data }}</div></div>
</template><script lang="ts" setup>
// 假设comment有2000条数据,首次只加载200条
const commentsRef = ref();
const comments = ref<{ id: string; data: string }[]>();
const displayComments = computed(() => comments.slice(0, 200));// 首屏动态加载的数量
const loadCount = ref<number>(0);
// 如果少于20条,直接全量加载;否则每隔4ms渲染40条数据
watch(displayComments, () => {clearInterval(timer);loadCount.value = 20;if (loadCount.value < nv.length) {timer = setInterval(() => {loadCount.value = Math.min(loadCount.value + 40, nv.length);if (loadCount.value >= nv.length) {clearInterval(timer);silenceScroll();addIntersectionObserve();}}, 4);} else {silenceScroll();addIntersectionObserve();}
})
</script>

定位优化

首屏加载速度上去了,也实现了自动滚动到指定的条目位置。但这时候遇到个新问题,如果列表的数据发生了变动,比如从前面插入了一条新数据,那么这时候可视范围内的数据肯定会发生改变。

如果使用批量重绘,即使本次修改只有一条数据更新,会出现页面先重新渲染,等10+ms后再滚动到指定位置,中间会出现画面的闪动。

这时候可以利用vue VDom算法的一些小Trick。先判断前后数据的差异程度,如果差异数量小于一个阈值(比如小于10),那么可以借用vue的diff算法,尽可能保留原有节点,渲染少量新节点。这时候可以大大提升页面的渲染效率,即使全量渲染也不会有性能问题。

<template><div><divv-for="comment in displayComments.slice(0, loadCount)":key="comment.id">{{ comment.data }}</div></div>
</template><script lang="ts" setup>
// 假设comment有2000条数据,首次只加载200条
const commentsRef = ref();
const comments = ref<{ id: string; data: string }[]>();
const displayComments = computed(() => comments.slice(0, 200));// 首屏动态加载的数量
const loadCount = ref<number>(0);
// 如果少于20条,直接全量加载;否则每隔4ms渲染40条数据
watch(displayComments, () => {clearInterval(timer);const nSet = new Set(nv.map((v) => v.id));const oSet = new Set(ov.map((v) => v.id));const n = new Set([...nSet].filter((x) => !oSet.has(x)));if (n.size <= 10) {loadCount.value = nv.length;silenceScroll();addIntersectionObserve();} else {loadCount.value = 20;if (loadCount.value < nv.length) {timer = setInterval(() => {loadCount.value = Math.min(loadCount.value + 40, nv.length);if (loadCount.value >= nv.length) {clearInterval(timer);silenceScroll();addIntersectionObserve();}}, 4);} else {silenceScroll();addIntersectionObserve();}}
})
</script>
http://www.lryc.cn/news/140966.html

相关文章:

  • 小区物业业主管理信息系统设计的设计与实现(论文+源码)_kaic
  • Fortran 微分方程求解 --ODEPACK
  • 8路光栅尺磁栅尺编码器或16路高速DI脉冲信号转Modbus TCP网络模块 YL99-RJ45
  • 【Python】函数
  • centos安装MySQL 解压版完整教程(按步骤傻瓜式安装
  • 【后端速成 Vue】第一个 Vue 程序
  • Macbook pro M1 安装Ubuntu教程
  • 前端console.log打印内容与后端请求返回数据不一致
  • SQL入门:多表查询
  • 【C++】进一步认识模板
  • Mysql Oracle 区别
  • 华为OD-第K长的连续字母字符串长度
  • 【编程题】有效三角形的个数
  • 【mysql是怎样运行的】-EXPLAIN详解
  • 数据结构例题代码及其讲解-链表
  • [Open-source tool] 可搭配PHP和SQL的表單開源工具_Form tools(1):簡介和建置
  • 移动数据业务价值链的整合
  • 合并两个链表
  • 测试框架pytest教程(9)跳过测试skip和xfail
  • HTML <textarea> 标签
  • 探索图结构:从基础到算法应用
  • Redis之GEO类型解读
  • uniapp 微信小程序 路由跳转
  • 【android12-linux-5.1】【ST芯片】HAL移植后没调起来
  • Redis Lua脚本执行原理和语法示例
  • 百望云华为云共建零售数字化新生态 聚焦数智新消费升级
  • JMETER基本原理
  • elementUI自定义上传文件 前端后端超详细过程
  • 快速排序笔记
  • JAVA:(JSON反序列化Long变成了Integer)java.lang.Integer cannot be cast to java.lang.Long