• 前端时间分片渲染


    在经典的面试题中:”如果后端返回了十万条数据要你插入到页面中,你会怎么处理?

    除了像 useVirtualList 这样的虚拟列表来处理外,我们还可以通过 时间分片 来处理

    通过 setTimeout

    直接上一个例子:

     

    1. <!--
    2.  * @Author: Jolyne
    3.  * @Date2023-09-22 15:45:45
    4.  * @LastEditTime: 2023-09-22 15:47:24
    5.  * @LastEditors: Jolyne
    6.  * @Description: 
    7. -->
    8. <!DOCTYPE html>
    9. <html lang="en">
    10. <head>
    11.   <meta charset="UTF-8" />
    12.   <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    13.   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    14.   <title>十万数据渲染</title>
    15. </head>
    16. <body>
    17.   <ul id="list-container"></ul>
    18.   <script>
    19.     const oListContainer = document.getElementById('list-container')
    20.     const fetchData = () => {
    21.       return new Promise(resolve => {
    22.         const response = {
    23.           code0,
    24.           msg: 'success',
    25.           data: [],
    26.         }
    27.         for (let i = 0; i < 100000; i++) {
    28.           response.data.push(`content-${i + 1}`)
    29.         }
    30.         setTimeout(() => {
    31.           resolve(response)
    32.         }, 100)
    33.       })
    34.     }
    35.     // 模拟请求后端接口返回十万条数据
    36.     // 渲染 total 条数据中的第 page 页,每页 pageCount 条数据
    37.     const renderData = (data, total, page, pageCount) => {
    38.       // base case -- total 为 0 时没有数据要渲染 不再递归调用
    39.       if (total <= 0return
    40.       // total 比 pageCount 少时只渲染 total 条数据
    41.       pageCount = Math.min(pageCount, total)
    42.       setTimeout(() => {
    43.         const startIdx = page * pageCount
    44.         const endIdx = startIdx + pageCount
    45.         const dataList = data.slice(startIdx, endIdx)
    46.         // 将 pageCount 条数据插入到容器中
    47.         for (let i = 0; i < pageCount; i++) {
    48.           const oItem = document.createElement('li')
    49.           oItem.innerText = dataList[i]
    50.           oListContainer.appendChild(oItem)
    51.         }
    52.         renderData(data, total - pageCount, page + 1, pageCount)
    53.       }, 0)
    54.     }
    55.     fetchData().then(res => {
    56.       renderData(res.data, res.data.length0200)
    57.     })
    58.   </script>
    59. </body>
    60. </html>

    上面的例子中,我们使用了 setTimeout,在每一次宏任务中插入一页数据,然后设置多个这样地宏任务,直到把所有数据都插入为止。

    但是很明显能看到的问题是,快速拖动滚动条时,数据列表中会有闪烁的情况

    这是因为:

    当使用 setTimeout 来拆分大量的 DOM 插入操作时,虽然我们将延迟时间设置为 0ms,但实际上由于 JavaScript 是单线程的,任务执行时会被放入到事件队列中,而事件队列中的任务需要等待当前任务执行完成后才能执行。所以即使设置了 0ms 延迟,setTimeout 的回调函数也不一定会立即执行,可能会受到其他任务的阻塞。

    当 setTimeout 的回调函数执行的间隔超过了浏览器每帧更新的时间间隔(一般是 16.7ms),就会出现丢帧现象。丢帧指的是浏览器在更新页面时,没有足够的时间执行全部的任务,导致部分任务被跳过,从而导致页面渲染不连续,出现闪烁的情况

    所以,我们改善一下,通过 requestAnimationFrame 来处理

    通过 requestAnimationFrame

    1. <!--
    2.  * @Author: Jolyne
    3.  * @Date2023-09-22 15:45:45
    4.  * @LastEditTime: 2023-09-22 15:47:24
    5.  * @LastEditors: Jolyne
    6.  * @Description: 
    7. -->
    8. <!DOCTYPE html>
    9. <html lang="en">
    10. <head>
    11.   <meta charset="UTF-8" />
    12.   <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    13.   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    14.   <title>直接插入十万条数据</title>
    15. </head>
    16. <body>
    17.   <ul id="list-container"></ul>
    18.   <script>
    19.     const oListContainer = document.getElementById('list-container')
    20.     const fetchData = () => {
    21.       return new Promise(resolve => {
    22.         const response = {
    23.           code0,
    24.           msg: 'success',
    25.           data: [],
    26.         }
    27.         for (let i = 0; i < 100000; i++) {
    28.           response.data.push(`content-${i + 1}`)
    29.         }
    30.         setTimeout(() => {
    31.           resolve(response)
    32.         }, 100)
    33.       })
    34.     }
    35.     // 模拟请求后端接口返回十万条数据
    36.     // 渲染 total 条数据中的第 page 页,每页 pageCount 条数据
    37.     const renderData = (data, total, page, pageCount) => {
    38.       // base case -- total 为 0 时没有数据要渲染 不再递归调用
    39.       if (total <= 0return
    40.       // total 比 pageCount 少时只渲染 total 条数据
    41.       pageCount = Math.min(pageCount, total)
    42.       requestAnimationFrame(() => {
    43.         const startIdx = page * pageCount
    44.         const endIdx = startIdx + pageCount
    45.         const dataList = data.slice(startIdx, endIdx)
    46.         // 将 pageCount 条数据插入到容器中
    47.         for (let i = 0; i < pageCount; i++) {
    48.           const oItem = document.createElement('li')
    49.           oItem.innerText = dataList[i]
    50.           oListContainer.appendChild(oItem)
    51.         }
    52.         renderData(data, total - pageCount, page + 1, pageCount)
    53.       })
    54.     }
    55.     fetchData().then(res => {
    56.       renderData(res.data, res.data.length0200)
    57.     })
    58.   </script>
    59. </body>
    60. </html>

     

    很明显,闪烁的问题被解决了

    这是因为:

    requestAnimationFrame 会在浏览器每次进行页面渲染时执行回调函数,保证了每次任务的执行间隔是稳定的,避免了丢帧现象。所以在处理大量 DOM 插入操作时,推荐使用 requestAnimationFrame 来拆分任务,以获得更流畅的渲染效果

  • 相关阅读:
    Flutter入门教程(二)开发环境搭建
    线段树的区间修改
    Python的pytest框架(6)--测试钩子(hooks)
    Spring IoC 容器生命周期:Ioc容器启停过程发生了什么-13
    使用Docker搭建Nextcloud私有网盘
    SSM+Mysql实现的共享单车管理系统(功能包含分角色,登录、用户管理、服务点管理、单车管理、分类管理、学生信息管理、单车租赁、信息统计、系统设置等)
    R语言使用MASS包的polr函数构建有序多分类logistic回归模型、使用summary函数获取模型汇总统计信息
    进程间通信
    数据清洗(data clean)
    车载u盘支持什么格式音乐?怎么把音乐转成MP3格式?
  • 原文地址:https://blog.csdn.net/why_1639946449/article/details/134024370