如果不考虑DOM节点回收,也就是一直不停向后加入节点,这样的长列表是比较容易的。那我们为什么要考虑DOM节点回收呢?
DOM节点本身并非耗能大户,但是也不是一点都不消耗性能,每一个节点都会增加一些额外的内存、布局、样式和绘制。如果一个站点的DOM节点过多,在低端设备上会发现明显的变慢,如果没有彻底卡死的话。同样需要注意的一点是,在一个较大的DOM中每一次重新布局或重新应用样式(在节点上增加或删除样式所触发的过程)的系统开销都会比较昂贵。所以进行DOM回收意味着我们会保持DOM节点在一个比较低的数量上,进而加快上面提到的这些处理过程。

那我们针对DOM节点回收来实现一个无尽的长列表。(最终实例代码在底部)

实现思路

我们先把这个存在数据,但不在视图显示的列表叫做虚拟列表。

具体效果就像这样

判断临界点

那么如何判断到达DOM回收的时机呢,也就是实际ViewList到达顶部或者顶部,要回收或者释放DOM。有两种主流的判断方式:
一、Scroll事件

    window.addEventListener("scroll",function(e){if(window.scrollY===顶部||window.scrollY===底部){...}})

这个方法麻烦的地方在于,你需要去实时计算viewport顶部和底部的Y值。像我的实现代码里最大放三个ul在viewport中,而且HTML结构简单,我只要取得顶部ul.offsetTop(元素到offsetParent顶部的距离),底部ul.offsetTop+offsetHeight就是对于的Y值了。需要注意的是,我的ul父级简单,大小位置几乎和body一样,如果你的parentDiv嵌套太复杂,还有各自的offsetTop,就另当别论。只有元素显示了(渲染完成)才会计算入offsetTop。
二、使用IntersectionObserver

IntersectionObserver接口(从属于IntersectionObserverAPI)为开发者提供了一种可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段。祖先元素与视窗(viewport)被称为根(root)。

应用在这个实例中就是,在顶部和底部各自加一个小元素,监听他们,当他出现的时候,就会及其祖先交叉,就会发出事件。

    function addIntersectionObserver(){var intersectionObserver = new IntersectionObserver(function(entries) {if (entries[0].intersectionRatio <= 0) return;if(entries[0].target === topDiv){//到达顶部}else if(entries[0].target === bottomDiv){//到达底部}});intersectionObserver.observe(topDiv);intersectionObserver.observe(bottomDiv);}

DOM回收

时机确定,接下来就是关键的DOM回收了。

//存放被回收顶部List
const beforeFragment = document.createDocumentFragment();
//存放被回收底部List
const afterFragment = document.createDocumentFragment();
//存放当前显示的List
const fragment = document.createDocumentFragment();

简单介绍下DocumentFragment接口,它他表示没有父级的最小文档对象,也就不会加入真实的DOMTree,进行渲染,只是一个虚拟的dom节点,存在于内存中,所以对片段所做的更改不会影响文档,导致回流,或者在进行更改时可能会发生任何性能影响。但操作方法属性还是像标准节点一样,故被用作轻量级版本,像标准文档一样存储由节点组成的文档结构的片段。

switch(临界情况):case 到达顶部: if(beforeFragment.lastChild)beforeFragment弹出最后一个节点lastChild1加入fragment头部fragment弹出最后一个节点lastChild2加入afterFragment头部case 到达底部:if(afterFragment.firstChild)afterFragment弹出第一个节点firstChild1加入fragment头部fragment弹出最后一个节点firstChild2加入beforeFragment头部else向后端请求数据加入fragment
重新加入DomTree
box.appendChild(fragment);

滚动条的问题

如果元素直接从视图删除一些加入一些,还要保持当前视图在当前DOM位置,会造成滚动条的跳动。总结了两种方法解决这种问题:
一、直接隐藏滚动条,再重新绘制一个自己控制的滚动条

body::-webkit-scrollbar {display: none;
}

首先隐藏这个CSS只有Chrome支持,而且绘制也比较麻烦,如果适合业务场景可以考虑。

二、使用padding或者其他什么占位符替换被回收的DOM位置
这样滚动条只可能被缩小,而不会跳动。我的实例使用的是padding,也可以用其他。
根据被回收的DOM节点大小,分别更新顶部和底部的padding。

topDiv.style.paddingTop = `${paddingTop}px`;
bottomDiv.style.paddingBottom = `${paddingBottom}px`;

当然我的实例还存在一些问题,比如说极其快速向上或者向下滚动时,来不及释放被回收的元素并绘制出来,会出现白屏。

最终代码:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Document</title><style type="text/css">*{padding: 0;margin: 0;}body{height: 100vh;}#box{width: 90%;margin: 20px auto;}#top,#bottom{height: 40px;}ul{list-style: none;margin: 5px;text-align: center;}#box ul div{width: 100%;margin: 5px;height: 100px;border: 2px solid red;font-size: 48px;}</style>
</head>
<body><ul id="top">到顶了</ul><div id="box"></div><ul id="bottom">加载中</ul>
</body><script type="text/javascript">let index= 1;var box = document.getElementById("box");var body = document.querySelector("body");const beforeFragment = document.createDocumentFragment();const afterFragment = document.createDocumentFragment();const fragment = document.createDocumentFragment();const [topDiv,bottomDiv] = [document.getElementById("top"),document.getElementById("bottom")];//函数节流,就是指连续触发事件但是在 n 秒中只执行一次函数function throttle(fn, wait,self=null) {let _fn = fn,       timer;return function(...args) {if (timer) return false;_fn.apply(self, [...args]); timer = setTimeout(()=> { clearTimeout(timer);  timer = null;  }, wait);return true;}}//获取图片(这是非真实场景,用了定时器假装异步请求)function createImg (count) {const ul = document.createElement("ul");for(var i = 0; i < count; i++) {var div = document.createElement("div");div.innerText = index;index++;var li = document.createElement("li");li.appendChild(div);ul.appendChild(li);}return new Promise(function(resolve, reject) {let timer = setTimeout(function() { clearTimeout(timer);  resolve(ul);timer = null;  }, 500);});}/*** 维护长列表(只在视图内显示60条)* type:0为向上,1为向下*/let paddingTop = 0;let paddingBottom = 0;async function removeOverDom(type){let y =0;const scrollY = window.scrollY;if(box.children.length>2){if(type===0&&!beforeFragment.lastChild) return true;//为了防止白屏,先异步请求完成后再操作if(type===1&&!afterFragment.firstChild) ul = await createImg(20);// appendChil加入后box就会移除这个元素,随后获取不到其高度y = type?-box.children[0].scrollHeight:box.children[2].scrollHeight;fragment.appendChild(box.children[0]);fragment.appendChild(box.children[0]);fragment.appendChild(box.children[0]);switch (type) {case 0:                   afterFragment.prepend(fragment.lastChild);fragment.prepend(beforeFragment.lastChild);break;case 1:beforeFragment.appendChild(fragment.firstChild);if(afterFragment.firstChild){fragment.appendChild(afterFragment.firstChild);}else{fragment.appendChild(ul);}break;default:break;}box.appendChild(fragment);if(y<0||(y>0&&paddingTop>0)) paddingTop -=y;if(y>0||(y<0&&paddingBottom>0)) paddingBottom +=y;topDiv.style.paddingTop = `${paddingTop}px`;bottomDiv.style.paddingBottom = `${paddingBottom}px`;window.scrollTo(0,scrollY,"smooth");body.style.overflow="scroll";}if(box.children.length<3){ul = await createImg(20);fragment.appendChild(ul);box.appendChild(fragment);}return true;}//判断是否滚到了顶部或者底部const removeOverDomT = throttle(removeOverDom,10);function addIntersectionObserver(){var intersectionObserver = new IntersectionObserver(function(entries) {if (entries[0].intersectionRatio <= 0) return;if(entries[0].target === topDiv){removeOverDomT(0);}else if(entries[0].target === bottomDiv){removeOverDomT(1);}});intersectionObserver.observe(topDiv);intersectionObserver.observe(bottomDiv);}addIntersectionObserver();
</script>
</html>

参考文章:
[译] 无尽滚动的复杂度 – 来自 Google 大神的拆解
一个简洁、有趣的无限下拉方案

怎么完成一个无尽的长列表相关推荐

  1. 微信小程序全栈开发实践 第二章 微信小程序组件介绍及使用 -- 2.6 scroll-view组件,在小程序中如何实现滚动锚定,如何渲染一个滚动的长列表?

    scroll-view 是一个可以滚动的视图区域的容器组件. 一.重要属性 scroll-view 的滚动属性,实现了两套功能 左右或上下滚动 下拉更新 1.1 与滚动有关的属性: scroll-x ...

  2. React解决长列表方案(react-virtualized)

    github地址 高效渲染大型列表的响应式组件 使用窗口特性,即在一个滚动的范围内,呈现你给定数据的一小部分,大量缩减了呈现组件所需的时间,以及创建DOM节点的数量. 缺点:滑动过快,可能会出现空白的 ...

  3. 判断 小程序 是否 滚动到页面底部 scrolltolower_微信小程序长列表性能优化——recycle-view

    背景: 第七次人口普查项目使用是微信小程序原生框架,组件是根据用户需求由项目组前端组组长封装完成的.采集小程序正式登记首页列表页面,根据腾讯老哥在sentry上的监控可以看出,列表页面前端性能比较差, ...

  4. 如何实现一个优秀的散列表!

    前言 假设现在有一篇很长的文档,如果希望统计文档中每个单词在文档中出现了多少次,应该怎么做呢? 很简单! 我们可以建一个HashMap,以String类型为Key,Int类型为Value: 遍历文档中 ...

  5. React前端性能提升长列表优化解决方案

    1.超长列表优化思路 1.1 概念 数据量较大且无法使用分页方式来加载的列表.比如淘宝的商品列表页,一次请求10个商品,一次请求10个商品和50个商品数据返回所需要的时间相差不大.但是却会多出4次的接 ...

  6. 【前端性能优化】长列表优化

    1 什么是长列表? 1.1  概念 前端的业务开发中会遇到一些数据量较大且无法使用分页方式来加载的列表,我们一般把这种列表叫做长列表. 1.2 参考案例 比如淘宝网的商品列表页,一个手机屏可以容纳10 ...

  7. element下拉列表触发_记一次vue长列表的内存性能分析和优化

    好久没写东西,博客又长草了,这段时间身心放松了好久,都没什么主题可以写了 上周接到一个需求,优化vue的一个长列表页面,忙活了很久也到尾声了,内存使用和卡顿都做了一点点优化,还算有点收获 写的有点啰嗦 ...

  8. React项目实战之租房app项目(四)长列表性能优化城市选择模块渲染列表

    前言 目录 前言 一.长列表性能优化 1.1 概述 1.2 懒渲染 1.3 可视区渲染(React-virtualized) 二.react-virtualized组件 2.1 概述 2.2 基本使用 ...

  9. 微信小程序 — 长列表组件 recycle-view 详细教学

    微信小程序 - 长列表组件 recycle-view 踩坑问题全解 写在前面 引入长列表组件 recycle-view 长列表组件 recycle-view 的使用 问题一.如何增加下拉刷新功能? 问 ...

最新文章

  1. Handler消息处理机制详解
  2. C#中DataTable的一些用法
  3. git idea 本地历史版本回滚_如何为IDEA项目创建GitHub存储库和本地Git存储库
  4. 如何在 SAP BTP 上 手动执行 workflow
  5. centos 生成 ssh-key github 连接 配置
  6. 新知|你知道生气有多可怕吗?“气死人”是有科学依据的
  7. LINQ to DataSet
  8. u-boot移植随笔:System.map文件格式
  9. 零基础如何入门数据分析师?
  10. mysql中的order语句_【MySql】1.2 mysql中 Order By 语句的用法
  11. 计算机处理io和cpu,虚拟化技术原理(CPU、内存、IO)
  12. 有一个计算机可以知道手机密码,要不是他,你根本不会忘记密码
  13. 数据结构与算法之多路查找树(2-3树、2-3-4树、B树、B+树)
  14. Java代码审计前置知识——Spring框架AOP和IoC
  15. 1050: 平方和与立方和
  16. BG-sentry的安装和配置
  17. windows10关闭火绒开机自启动
  18. Blender建模模块:快速建一棵枯树
  19. Idea Dependencies爆红所有能想到的解决办法
  20. 华为外部Portal认证 Radius认证计费 实现基于Mac快速认证的Mac无感知认证和结合CAS单点登录统一认证平台和AD域LDAP对接配置

热门文章

  1. 无事街上走,提壶去打酒,逢店加一倍,遇花喝一斗,编程题。
  2. 新车提车验车步骤和细节
  3. MySQL-视图-触发器-事务-存储过程-函数-流程控制-索引与慢查询优化-06
  4. 每日优鲜完成新一轮4.5亿美元融资,自有品牌商品占比j将提升至50%
  5. 动手学深度学习(三十九)——门控循环单元GRU
  6. scaner从外网到内网域渗透笔记
  7. 六轴机器人光机_六款小型六轴机器人性能数据大比拼
  8. Spring Boot 项目 - API 文档搜索引擎
  9. Molecular weight相对分子质量
  10. css3 logo 自上而下动画 渐渐出现