12.28

1.渲染几万条数据不卡住页面

https://wsydxiangwang.github.io/web/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/7.html

是常见的面试题,也是大型项目必备的优化知识点

  1. const oUl = document.querySelector('ul')
  2. for (let i = 0; i < 200000; i++) {
  3. const oLi = document.createElement('li')
  4. oLi.innerHTML = i;
  5. oUl.appendChild(oLi)
  6. }

解决办法:

创建文档片段Fragment,将标签全部放入该片段中,再统一插入document,这样只会渲染一次,只会操作一次DOM

  1. console.time('over')
  2. let oUl = document.querySelector('ul')
  3. let fragment = document.createDocumentFragment()
  4. for (let i = 0; i < 200000; i++) {
  5. const oLi = document.createElement('li')
  6. oLi.innerHTML = i;
  7. fragment.appendChild(oLi)
  8. }
  9. oUl.appendChild(fragment)
  10. // 据说下面这样子世界会更清净
  11. fragment = null
  12. console.timeEnd('over')


深入

把 10 万次 for 循环的代码插到 html 中间,会有什么现象?出现卡顿现象怎么解决?添加 defer 属性之后脚本会在什么时候执行?采用 defer 之后,用户点击页面会怎么样?如果禁用 WebWoker,还有其他方法吗

一、十万次循环代码插入 body 中,页面会出现卡顿

十万次循环代码插入 body 中,页面会出现卡顿,代码后的 DOM 节点加载不出来

二、解决

设置 script 标签 defer 属性,浏览器其它线程将下载脚本,待到文档解析完成脚本才会执行。

三、采用 defer 之后,用户点击问题

  • 若 button 中的点击事件在 defer 脚本前定义,则在 defer 脚本加载完后,响应点击事件。
  • 若 button 中的点击事件在 defer 脚本后定义,则用户点击 button 无反应,待脚本加载完后,再次点击有响应。
  • 代码示例

    1. <!-- test.html -->
    2. <!DOCTYPE html>
    3. <html>
    4. <head>
    5. <title></title>
    6. </head>
    7. <body>
    8. <div class="test1">test1</div>
    9. <div id="hello"></div>
    10. <script>
    11. // 待defer脚本下载完成后响应
    12. function alertMsg() {
    13. alert("123");
    14. }
    15. </script>
    16. <input type="button" id="button1" onclick="alertMsg()" />
    17. <script src="./test.js" defer></script>
    18. <div class="test2">test2</div>
    19. </body>
    20. <style>
    21. .test1 {
    22. color: red;
    23. font-size: 50px;
    24. }
    25. .test2 {
    26. color: yellow;
    27. font-size: 50px;
    28. }
    29. </style>
    30. </html>
    31. 复制代码
    1. // test.js
    2. for (let i = 0; i < 100000; i++) {
    3. console.log(i);
    4. }
    5. document.getElementById("hello").innerHTML = "hello world";
    6. 复制代码

    四、如果禁用 WebWoker,还有其他方法吗?

    4.1 使用 Concurrent.Thread.js

  • Concurrent.Thread.js 用来模拟多线程,进行多线程开发。

    1. Concurrent.Thread.create(function () {
    2. $("#test").click(function () {
    3. alert(1);
    4. });
    5. for (var i = 0; i < 100000; i++) {
    6. console.log(i);
    7. }
    8. });
    9. 复制代码

    4.2 使用虚拟列表

    若该情形是渲染十万条数据的情况下,则可以使用虚拟列表。虚拟列表即只渲染可视区域的数据,使得在数据量庞大的情况下,减少 DOM 的渲染,使得列表流畅地无限滚动。
    实现方案:
    基于虚拟列表是渲染可视区域的特性,我们需要做到以下三点

  1. 需计算顶部和底部不可视区域留白的高度,撑起整个列表高度,使其高度与没有截断数据时一样,这两个高度分别命名为 topHeight、bottomHeight
  2. 计算截断开始位置 start 和结束位置 end,则可视区域的数据为 list.slice(start,end)
  3. 滚动过程中需不断更新 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 刷新一次。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <ul>
  11. 控件
  12. </ul>
  13. <script>
  14. setTimeout(() => {
  15. // 插入十万条数据
  16. const total = 100000
  17. // 一次插入 20 条,如果觉得性能不好就减少
  18. const once = 20
  19. // 渲染数据总共需要几次
  20. const loopCount = total / once
  21. let countOfRender = 0
  22. let ul = document.querySelector('ul')
  23. function add() {
  24. // 优化性能,插入不会造成回流
  25. const fragment = document.createDocumentFragment()
  26. for (let i = 0; i < once; i++) {
  27. const li = document.createElement('li')
  28. li.innerText = Math.floor(Math.random() * total)
  29. fragment.appendChild(li)
  30. }
  31. ul.appendChild(fragment)
  32. countOfRender += 1
  33. loop()
  34. }
  35. function loop() {
  36. if (countOfRender < loopCount) {
  37. window.requestAnimationFrame(add)
  38. }
  39. }
  40. loop()
  41. }, 0)
  42. </script>
  43. </body>
  44. </html>