前端时间分片渲染

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

除了像 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 来拆分任务,以获得更流畅的渲染效果

相关推荐
剪刀石头布啊6 小时前
泛洪DFS、BFS
前端
qetfw6 小时前
Windows Server AD CS:根 CA、Web 证书模板与域内自动注册
前端·windows·windows-server
剪刀石头布啊6 小时前
为什么React组件很少前缀,而vue的不少组件都有前缀
前端
剪刀石头布啊6 小时前
gird网格布局
前端
啃火龙果的兔子6 小时前
Google Chrome(谷歌浏览器)常用快捷键
前端·chrome
剪刀石头布啊6 小时前
js真的是单线程实现异步并发么?
前端
剪刀石头布啊6 小时前
js中改变this指向的操作有哪些
前端
剪刀石头布啊6 小时前
vue、react 列表中使用 index 作为 key 的话,修改某一个元素后会发生什么
前端
方方洛6 小时前
glowglow:语言无关的语法高亮器是怎么实现的
前端·前端框架
java_nnnn7 小时前
JavaEE进阶-HTML基础认识
前端·java-ee·html