性能优化小册 - 渲染十万条数据:基于 IntersectionObserver 的虚拟化长列表
前端笔记 2021-06-03 16:20:09

 1014292343-5ee9d1df0626b.gif

 

前置

1. 什么是虚拟列表?

首先,虚拟列表只是一个概念,本人对虚拟列表这个表述不置可否。

虚拟列表是对于列表形态数据展示的一种按需渲染,是对长列表渲染的一种优化。

虚拟列表不会一次性完整地渲染长列表,而是按需显示的一种方案,以提高无限滚动的性能。

 

2. 虚拟列表的实现原理?

根据容器元素的高度 clientHeight 以及列表项元素的高度 offsetHeight 来显示长列表数据中的某一个部分,而不是去完整地渲染整个长列表。

3272580642-4375102a99963283_fix732.png

 

实现一个虚拟列表需要:

  • 得知容器元素的高度 clientHeight

  • 得知列表项元素的高度 offsetHeight

  • 计算可视区域应该渲染的列表项的个数 count = clientHeight / offsetHeight

  • 计算可视区域数据渲染的起始位置 start

  • 计算可视区域数据渲染的结束位置 end

  • 对完整长列表数据进行截断 sliceList = dataList.slice(start, end)

  • 渲染截断后的列表数据,进而实现无限加载

 

3. 虚拟列表与懒加载有何不同?

懒加载与虚拟列表其实都是延时加载的一种实现,原理相同但场景略有不同。

  • 懒加载的应用场景偏向于网络资源请求,解决网络资源请求过多时,造成的网站响应时间过长的问题。

  • 虚拟列表是对长列表渲染的一种优化,解决大量数据渲染时,造成的渲染性能瓶颈的问题。

 

 4. IntersectionObserver 介绍

IntersectionObserver 提供了一种异步观察目标元素与视口的交叉状态,简单地说就是能监听到某个元素是否会被我们看到,当我们看到这个元素时,可以执行一些回调函数来处理某些事务。

JavaScript Code复制内容到剪贴板
  1. let io = new IntersectionObserver(callback, option);  

 

callback 会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。

更多介绍 Intersection Observer

 


 

实现

1. 模拟十万条数据:

JavaScript Code复制内容到剪贴板
  1. function getDataList() {  
  2.   let data = []  
  3.   for(let i = 0; i < 100000; i++) {  
  4.     data.push({id: "item" + i, value: Math.random() * i})  
  5.   }  
  6.   return data;  
  7. }  

 

2. Dom 创建及列表渲染:

不依赖框架的情况下,需要命令性的去创建 DOM 以及操作 DOM。 

JavaScript Code复制内容到剪贴板
  1. <ul class="container">  
  2.   <span class="sentinels">....</span>  
  3. </ul>  

 

JavaScript Code复制内容到剪贴板
  1. function $(selector) {  
  2.   return document.querySelector(selector)  
  3. }  
  4.   
  5. function loadData(start, end) {  
  6.   // 截取数据  
  7.   let sliceData = getDataList().slice(start, end)  
  8.   // 现代浏览器下,createDocumentFragment 和 createElement 的区别其实没有那么大  
  9.   let fragment = document.createDocumentFragment();   
  10.   for(let i = 0; i < sliceData.length; i++) {  
  11.     let li = document.createElement('li');  
  12.     li.innerText = JSON.stringify(sliceData[i])  
  13.     fragment.appendChild(li);  
  14.   }  
  15.   $('.container').insertBefore(fragment, $('.sentinels'));  
  16. }  

 

如果是基于 Virtual DOM 的框架,直接操作数据即可(伪代码): 

JavaScript Code复制内容到剪贴板
  1. // 父组件  
  2. <virtual-list :listData="listData"></virtual-list>  
  3.   
  4. // 子组件  
  5. <ul class='container'>  
  6.   <li  
  7.     v-for="item in sliceData"   
  8.     :key="item.id"  
  9.   >{{ item }}</li>  
  10. </ul>  
  11. ...  
  12.   
  13. // js  
  14. this.sliceData = this.data.slice(start, index)  

 

3. 使用 IntersectionObserver API 创建监听器: 

JavaScript Code复制内容到剪贴板
  1. let count = Math.ceil(document.body.clientHeight / 120);  
  2. let startIndex = 0;  
  3. let endIndex = 0;  
  4. ...  
  5. let io = new IntersectionObserver(function(entries) {  
  6.     loadData(startIndex, count)  
  7.     // 标志位元素进入视口  
  8.     if(entries[0].isIntersecting) {  
  9.       // 更新列表数据起始和结束位置  
  10.       startIndex = startIndex += count;  
  11.       endIndex = startIndex + count;  
  12.       if(endIndex >= getDataList().length) {  
  13.         // 数据加载完取消观察  
  14.         io.unobserve(entries[0].target)  
  15.       }  
  16.       // requestAnimationFrame 由系统决定回调函数的执行时机  
  17.       requestAnimationFrame(() => {  
  18.         loadData(startIndex, endIndex)  
  19.         let num = Number(getDataList().length - startIndex)  
  20.         let info = ['还有', num , '条数据']  
  21.         $('.top').innerText = info.join(' ')  
  22.         if(num - count <= 0) {  
  23.            $('.top').classList.add('out')  
  24.          }  
  25.       })  
  26.     }  
  27.   });  
  28.   // 开始观察“标志位”元素  
  29.   io.observe($('.sentinels'));  
  30. })  

 

由于 IntersectionObserver 无法监听动态创建的 dom,所以我们设置一个「标志位」元素 span.sentinels 作为监听的目标对象。 

XML/HTML Code复制内容到剪贴板
  1. <ul class="container">  
  2.   <span class="sentinels">....</span>  
  3. </ul>  

 

如果目标元素正处于交叉状态 entries[0].isIntersecting == true,则代表 .sentinels 进入了可视区域,从而加载新的列表数据。 

JavaScript Code复制内容到剪贴板
  1. if(entries[0].isIntersecting) {  
  2.   ...  
  3.   requestAnimationFrame(() => {  
  4.     loadData(startIndex, endIndex)  
  5.   })  
  6.   ...  
  7. }  

 

最后将新的列表 insertBefore 到其前面,进而实现无限加载。 

JavaScript Code复制内容到剪贴板
  1. $('.container').insertBefore(fragment, $('.sentinels'));  

 

 

 

 

 

本文来自于:https://segmentfault.com/a/1190000022956784

下一篇 Mac安装gulp
Powered by yoyo苏ICP备15045725号