面试常见问题:
如何渲染十万条数据
最直接的方法就是直接渲染出来,但是这样的做法肯定是不可取的,因为直接渲染太耗性能了。
提高渲染性能的解决方案有如下:
虚拟列表是最主流的解决方案,不渲染所有的数据,只渲染可视区域中的数据。当用户滑(滚)动时,通过监听 scroll 来判断是上滑还是下拉,从而更新数据。同理 IntersectionObserver 和 getBoundingClientRect 都能实现
时间分片主要是分批渲染DOM,使用 requestAnimationFrame 来让动画更加流畅
01
直接渲染
通过for 直接渲染,太消耗性能
<ul id="container"></ul>
// 记录任务开始时间
let now = Date.now();
// 插入十万条数据
const total = 100000;
// 获取容器
let ul = document.getElementById('container');
// 将数据插入容器中
for (let i = 0; i < total; i++) {
let li = document.createElement('li');
li.innerText = (Math.random() * total)
ul.appendChild(li);
}
console.log('JS运行时间:',Date.now() - now);
setTimeout(()=>{
console.log('总运行时间:',Date.now() - now);
},0)
0 2
虚拟列表
只渲染可视区域中的数据, 可以通过scroll 或IntersectionObserver(交叉观察者,异步的,性能消耗小) 和 getBoundingClientRect 都可以使用
注意:区分虚拟列表与懒加载
懒加载与虚拟列表其实都是延时加载的一种实现,原理相同但场景略有不同
03
时间分片
方法一:使用 setTimeout
页面的卡顿是由于同时渲染大量DOM所引起的,所以我们考虑将渲染过程分批进行,可以使用setTimeout来实现分批渲染
<ul id="container"></ul>
//需要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
if(curTotal <= 0){
return false;
}
//每页多少条
let pageCount = Math.min(curTotal , once);
setTimeout(()=>{
for(let i = 0; i < pageCount; i++){
let li = document.createElement('li');
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount,curIndex + pageCount)
},0)
}
loop(total,index);
此方法可以使用页面加载的时间变快,但是当我们快速滚动页面的时候,会发现页面出现闪屏或白屏的现象
大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次;大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次
大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。
setTimeout
的执行时间并不是确定的,当 setTimeout 的执行步调和屏幕的刷新步调不一致,就会出现丢帧的情况,从而出现闪屏
方法二:requestAnimationFrame
与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机,换句话说就是,requestAnimationFrame
的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象。
<ul id="container"></ul>
//需要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
if(curTotal <= 0){
return false;
}
//每页多少条
let pageCount = Math.min(curTotal , once);
window.requestAnimationFrame(function(){
for(let i = 0; i < pageCount; i++){
let li = document.createElement('li');
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount,curIndex + pageCount)
})
}
loop(total,index);
方法三:文档碎片 + requestAnimationFrame
我们还可以DOM操作上去优化,通过 DocumentFragment(文档碎片 )添加节点。
DocumentFragments是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。
可以将要渲染的节点,添加到碎片节点中,然后再将碎片节点,添加到DOM树中,从而提高性能 。
<ul id="container"></ul>
//需要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
if(curTotal <= 0){
return false;
}
//每页多少条
let pageCount = Math.min(curTotal , once);
window.requestAnimationFrame(function(){
let fragment = document.createDocumentFragment();
for(let i = 0; i < pageCount; i++){
let li = document.createElement('li');
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
loop(curTotal - pageCount,curIndex + pageCount)
})
}
loop(total,index);