在经典的面试题中:”如果后端返回了十万条数据要你插入到页面中,你会怎么处理?”除了像 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 来拆分任务,以获得更流畅的渲染效果
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。