12.28
1.渲染几万条数据不卡住页面
https://wsydxiangwang.github.io/web/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/7.html
是常见的面试题,也是大型项目必备的优化知识点
const oUl = document.querySelector('ul')
for (let i = 0; i < 200000; i++) {
const oLi = document.createElement('li')
oLi.innerHTML = i;
oUl.appendChild(oLi)
}
解决办法:
创建文档片段Fragment,将标签全部放入该片段中,再统一插入document,这样只会渲染一次,只会操作一次DOM
console.time('over')
let oUl = document.querySelector('ul')
let fragment = document.createDocumentFragment()
for (let i = 0; i < 200000; i++) {
const oLi = document.createElement('li')
oLi.innerHTML = i;
fragment.appendChild(oLi)
}
oUl.appendChild(fragment)
// 据说下面这样子世界会更清净
fragment = null
console.timeEnd('over')
深入
把 10 万次 for 循环的代码插到 html 中间,会有什么现象?出现卡顿现象怎么解决?添加 defer 属性之后脚本会在什么时候执行?采用 defer 之后,用户点击页面会怎么样?如果禁用 WebWoker,还有其他方法吗
一、十万次循环代码插入 body 中,页面会出现卡顿
十万次循环代码插入 body 中,页面会出现卡顿,代码后的 DOM 节点加载不出来
二、解决
设置 script 标签 defer 属性,浏览器其它线程将下载脚本,待到文档解析完成脚本才会执行。
三、采用 defer 之后,用户点击问题
- 若 button 中的点击事件在 defer 脚本前定义,则在 defer 脚本加载完后,响应点击事件。
- 若 button 中的点击事件在 defer 脚本后定义,则用户点击 button 无反应,待脚本加载完后,再次点击有响应。
代码示例
<!-- test.html -->
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div class="test1">test1</div>
<div id="hello"></div>
<script>
// 待defer脚本下载完成后响应
function alertMsg() {
alert("123");
}
</script>
<input type="button" id="button1" onclick="alertMsg()" />
<script src="./test.js" defer></script>
<div class="test2">test2</div>
</body>
<style>
.test1 {
color: red;
font-size: 50px;
}
.test2 {
color: yellow;
font-size: 50px;
}
</style>
</html>
复制代码
// test.js
for (let i = 0; i < 100000; i++) {
console.log(i);
}
document.getElementById("hello").innerHTML = "hello world";
复制代码
四、如果禁用 WebWoker,还有其他方法吗?
4.1 使用 Concurrent.Thread.js
Concurrent.Thread.js 用来模拟多线程,进行多线程开发。
Concurrent.Thread.create(function () {
$("#test").click(function () {
alert(1);
});
for (var i = 0; i < 100000; i++) {
console.log(i);
}
});
复制代码
4.2 使用虚拟列表
若该情形是渲染十万条数据的情况下,则可以使用虚拟列表。虚拟列表即只渲染可视区域的数据,使得在数据量庞大的情况下,减少 DOM 的渲染,使得列表流畅地无限滚动。
实现方案:
基于虚拟列表是渲染可视区域的特性,我们需要做到以下三点
- 需计算顶部和底部不可视区域留白的高度,撑起整个列表高度,使其高度与没有截断数据时一样,这两个高度分别命名为 topHeight、bottomHeight
- 计算截断开始位置 start 和结束位置 end,则可视区域的数据为 list.slice(start,end)
- 滚动过程中需不断更新 topHeight、bottomHeight、start、end,从而更新可视区域视图。当然我们需要对比老旧 start、end 来判断是否需要更新。
topHeight 的计算比较简单,就是滚动了多少高度,topHeight=scrollTop。
start 的计算依赖于 topHeight 和每项元素的高度 itemHeight,假设我们向上移动了两个列表项,则 start 为 2,如此,我们有 start = Math.floor(topHeight / itemHeight)
。
end 的计算依赖于屏幕的高度能显示多少个列表项,我们称之为 visibleCount,则有 visibleCount = Math.ceil(clientHeight / itemHeight)
,向上取整是为了避免计算偏小导致屏幕没有显示足够的内容,则 end = start + visibleCount
。 bottomHeight 需要我们知道整个列表没有被截断前的高度,减去其顶部的高度,计算顶部的高度有了 end 就很简单了,假设我们的整个列表项的数量为 totalItem,则 bottomHeight = (totalItem - end - 1) \* itemHeight
。
相关成熟的库:vue-virtual-scroller
https://akryum.github.io/vue-virtual-scroller/#/
补充
面试题
如何渲染几万条数据并不卡住界面
这道题考察了如何在不卡住页面的情况下渲染数据,也就是说不能一次性将几万条都渲染出来,而应该一次渲染部分 DOM,那么就可以通过 requestAnimationFrame
来每 16 ms 刷新一次。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<ul>
控件
</ul>
<script>
setTimeout(() => {
// 插入十万条数据
const total = 100000
// 一次插入 20 条,如果觉得性能不好就减少
const once = 20
// 渲染数据总共需要几次
const loopCount = total / once
let countOfRender = 0
let ul = document.querySelector('ul')
function add() {
// 优化性能,插入不会造成回流
const fragment = document.createDocumentFragment()
for (let i = 0; i < once; i++) {
const li = document.createElement('li')
li.innerText = Math.floor(Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
countOfRender += 1
loop()
}
function loop() {
if (countOfRender < loopCount) {
window.requestAnimationFrame(add)
}
}
loop()
}, 0)
</script>
</body>
</html>