技术不局限于框架,相同的原理只是实现方式略有不一样。
首先,虚拟列表只是一个概念,本人对虚拟列表这个表述不置能否。
虚拟列表是对于列表形态数据展现的一种按需渲染,是对长列表渲染的一种优化。html
虚拟列表不会一次性完整地渲染长列表,而是按需显示的一种方案,以提升无限滚动的性能。webpack
根据容器元素的高度 clientHeight
以及列表项元素的高度 offsetHeight
来显示长列表数据中的某一个部分,而不是去完整地渲染整个长列表。web
实现一个虚拟列表须要:编程
clientHeight
offsetHeight
count = clientHeight / offsetHeight
start
end
sliceList = dataList.slice(start, end)
懒加载与虚拟列表其实都是延时加载的一种实现,原理相同但场景略有不一样。segmentfault
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 建立及列表渲染:app
不依赖框架的状况下,须要命令性的去建立 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'));
同系列文章: