高性能渲染十万条数据,基于IntersectionObserver虚拟列表

前置

1. 什么是虚拟列表?

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

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

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

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

实现一个虚拟列表需要:

  • 得知容器元素的高度 clientHeight
  • 得知列表项元素的高度 offsetHeight
  • 计算可视区域应该渲染的列表项的个数 count = clientHeight / offsetHeight
  • 计算可视区域数据渲染的起始位置 start
  • 计算可视区域数据渲染的结束位置 end
  • 对完整长列表数据进行截断 sliceList = dataList.slice(start, end)
  • 渲染截断后的列表数据,进而实现无限加载

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

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

  • 懒加载的应用场景偏向于网络资源请求,解决网络资源请求过多时,造成的网站响应时间过长的问题。
  • 虚拟列表是对长列表渲染的一种优化,解决大量数据渲染时,造成的渲染性能瓶颈的问题。

4. IntersectionObserver 介绍

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

let io = new IntersectionObserver(callback, option);

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

更多介绍 Intersection Observer

实现

1. 生成十万条数据:

function getDataList() {

let data = []

for(let i = 0; i < 100000; i++) {

data.push({id: "item" + i, value: Math.random() * i})

}

return data;

}

2. Dom 创建及列表渲染:

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

<ul class="container">

<span class="sentinels">....</span>

</ul>

function $(selector) {

return document.querySelector(selector)

}

function loadData(start, end) {

// 截取数据

let sliceData = getDataList().slice(start, end)

// 现代浏览器下,createDocumentFragment 和 createElement 的区别其实没有那么大

let fragment = document.createDocumentFragment();

for(let i = 0; i < sliceData.length; i++) {

let li = document.createElement('li');

li.innerText = JSON.stringify(sliceData[i])

fragment.appendChild(li);

}

$('.container').insertBefore(fragment, $('.sentinels'));

}

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

// 父组件

<virtual-list :listData="listData"></virtual-list>

// 子组件

<ul class='container'>

<li

v-for="item in sliceData"

:key="item.id"

>{{ item }}</li>

</ul>

...

// js

this.sliceData = this.data.slice(start, index)

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

let count = Math.ceil(document.body.clientHeight / 120);

let startIndex = 0;

let endIndex = 0;

...

let io = new IntersectionObserver(function(entries) {

loadData(startIndex, count)

// 标志位元素进入视口

if(entries[0].isIntersecting) {

// 更新列表数据起始和结束位置

startIndex = startIndex += count;

endIndex = startIndex + count;

if(endIndex >= getDataList().length) {

// 数据加载完取消观察

io.unobserve(entries[0].target)

}

// requestAnimationFrame 由系统决定回调函数的执行时机

requestAnimationFrame(() => {

loadData(startIndex, endIndex)

let num = Number(getDataList().length - startIndex)

let info = ['还有', num , '条数据']

$('.top').innerText = info.join(' ')

if(num - count <= 0) {

$('.top').classList.add('out')

}

})

}

});

// 开始观察“标志位”元素

io.observe($('.sentinels'));

})

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

<ul class="container">

<span class="sentinels">....</span>

</ul>

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

if(entries[0].isIntersecting) {

...

requestAnimationFrame(() => {

loadData(startIndex, endIndex)

})

...

}

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

$('.container').insertBefore(fragment, $('.sentinels'));

同系列文章:

  • 性能优化小册 - 异步堆栈追踪:为什么 await 胜过 Promise
  • 性能优化小册 - 分类构建:利用好 webpack hash
  • 性能优化小册 - 提高网页响应速度:优化你的 CDN 性能
  • 性能优化小册 - 可编程式缓存:Service Workers
  • 性能优化小册 - 让页面更早的渲染:使用 preload 提升资源加载优先级
  • 性能优化小册 - React 搜索优化:防抖、缓存、LRU

以上是 高性能渲染十万条数据,基于IntersectionObserver虚拟列表 的全部内容, 来源链接: utcz.com/a/25424.html

回到顶部