前端时间分片渲染

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

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

通过 setTimeout

直接上一个例子:

复制代码
<!--
 * @Author: Jolyne
 * @Date: 2023-09-22 15:45:45
 * @LastEditTime: 2023-09-22 15:47:24
 * @LastEditors: Jolyne
 * @Description: 
-->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>十万数据渲染</title>
</head>

<body>
  <ul id="list-container"></ul>

  <script>
    const oListContainer = document.getElementById('list-container')

    const fetchData = () => {
      return new Promise(resolve => {
        const response = {
          code: 0,
          msg: 'success',
          data: [],
        }

        for (let i = 0; i < 100000; i++) {
          response.data.push(`content-${i + 1}`)
        }

        setTimeout(() => {
          resolve(response)
        }, 100)
      })
    }

    // 模拟请求后端接口返回十万条数据
    // 渲染 total 条数据中的第 page 页,每页 pageCount 条数据
    const renderData = (data, total, page, pageCount) => {
      // base case -- total 为 0 时没有数据要渲染 不再递归调用
      if (total <= 0) return

      // total 比 pageCount 少时只渲染 total 条数据
      pageCount = Math.min(pageCount, total)

      setTimeout(() => {
        const startIdx = page * pageCount
        const endIdx = startIdx + pageCount
        const dataList = data.slice(startIdx, endIdx)

        // 将 pageCount 条数据插入到容器中
        for (let i = 0; i < pageCount; i++) {
          const oItem = document.createElement('li')
          oItem.innerText = dataList[i]
          oListContainer.appendChild(oItem)
        }

        renderData(data, total - pageCount, page + 1, pageCount)
      }, 0)
    }

    fetchData().then(res => {
      renderData(res.data, res.data.length, 0, 200)
    })

  </script>
</body>

</html>

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

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

这是因为:

当使用 setTimeout 来拆分大量的 DOM 插入操作时,虽然我们将延迟时间设置为 0ms,但实际上由于 JavaScript 是单线程的,任务执行时会被放入到事件队列中,而事件队列中的任务需要等待当前任务执行完成后才能执行。所以即使设置了 0ms 延迟,setTimeout 的回调函数也不一定会立即执行,可能会受到其他任务的阻塞。
setTimeout 的回调函数执行的间隔超过了浏览器每帧更新的时间间隔(一般是 16.7ms),就会出现丢帧现象。丢帧指的是浏览器在更新页面时,没有足够的时间执行全部的任务,导致部分任务被跳过,从而导致页面渲染不连续,出现闪烁的情况

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

通过 requestAnimationFrame

复制代码
<!--
 * @Author: Jolyne
 * @Date: 2023-09-22 15:45:45
 * @LastEditTime: 2023-09-22 15:47:24
 * @LastEditors: Jolyne
 * @Description: 
-->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>直接插入十万条数据</title>
</head>

<body>
  <ul id="list-container"></ul>

  <script>
    const oListContainer = document.getElementById('list-container')

    const fetchData = () => {
      return new Promise(resolve => {
        const response = {
          code: 0,
          msg: 'success',
          data: [],
        }

        for (let i = 0; i < 100000; i++) {
          response.data.push(`content-${i + 1}`)
        }

        setTimeout(() => {
          resolve(response)
        }, 100)
      })
    }

    // 模拟请求后端接口返回十万条数据
    // 渲染 total 条数据中的第 page 页,每页 pageCount 条数据
    const renderData = (data, total, page, pageCount) => {
      // base case -- total 为 0 时没有数据要渲染 不再递归调用
      if (total <= 0) return

      // total 比 pageCount 少时只渲染 total 条数据
      pageCount = Math.min(pageCount, total)

      requestAnimationFrame(() => {
        const startIdx = page * pageCount
        const endIdx = startIdx + pageCount
        const dataList = data.slice(startIdx, endIdx)

        // 将 pageCount 条数据插入到容器中
        for (let i = 0; i < pageCount; i++) {
          const oItem = document.createElement('li')
          oItem.innerText = dataList[i]
          oListContainer.appendChild(oItem)
        }

        renderData(data, total - pageCount, page + 1, pageCount)
      })
    }

    fetchData().then(res => {
      renderData(res.data, res.data.length, 0, 200)
    })

  </script>
</body>

</html>

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

这是因为:

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

相关推荐
范文杰1 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪2 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪2 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy2 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom3 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom3 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom3 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom3 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom3 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试
LaoZhangAI4 小时前
2025最全GPT-4o图像生成API指南:官方接口配置+15个实用提示词【保姆级教程】
前端