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 = nullconsole.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.jsfor (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 / oncelet countOfRender = 0let 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 += 1loop()}function loop() {if (countOfRender < loopCount) {window.requestAnimationFrame(add)}}loop()}, 0)</script></body></html>
