引言
在前端开发中,性能优化是一个永恒的话题。我们经常需要处理和展示大量的数据,所以当面试官问到:“如何一次性渲染十万条数据而不影响用户体验?”你会怎么回答?直接渲染十万条数据可能会导致页面卡顿、响应迟缓,甚至浏览器崩溃。本篇文章详细介绍时间分片和虚拟列表的解决方案,帮助你轻松拿下面试~
前置知识
js是单线程的,会有一个同步和异步的概念,为了确保主线程不会被长时间阻塞,js引擎就会依照事件循环机制来执行代码:
- 先执行同步代码(也属于是宏任务)
- 同步执行完毕后,检查是否有异步代码需要执行
- 执行所有的微任务
- 微任务执行完毕后,若有需要就会渲染页面
- 执行宏任务(也就是下一次事件循环开始)
- 微任务:Promise.then()、process.nextTick()、async/await、MutationObserver()等
- 宏任务:script(开启一整份代码的执行)、setTimeout、setInterval、setImmediate、I/O操作、UI-Rendering、同步代码等
时间分片
v8引擎执行 js 代码速度很快,然而渲染页面时间相对来说要长很多。如果直接将十万条数据给到渲染引擎,很容易造成页面卡顿或白屏,所以一次性渲染十万条数据的关键在于——要让浏览器的渲染线程尽量均匀流畅地将数据渲染上去。
时间分片的核心思想是将一个大的任务分解成多个小的任务,使用 setTimeout
或requestAnimationFrame
分批次地渲染一部分数据。
使用 setTimeout
- 初始化:定义总数据条数
total
、每次渲染的数据条数once
、需要渲染的总次数page
和当前渲染的索引index
。 - 递归渲染:
loop
函数通过递归调用来逐步渲染数据。每次for循环渲染once
条数据,并使用setTimeout
将渲染操作放入下一个事件循环中。 - 定时器:
setTimeout
确保每次渲染操作不会阻塞主线程,从而保持页面的流畅性和响应性。 - 结束条件:当所有数据都渲染完毕后(即
curTotal - pageCount <= 0
),递归调用停止。
<body>
<ul id="container">ul>
<script>
let ul = document.getElementById('container');
const total = 100000
let once = 20
let page = total / once
let index = 0
function loop(curTotal, curIndex) {
let pageCount = Math.min(once, curTotal)
setTimeout(() => {
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li');
li.innerText = curIndex + i + ':' + ~~(Math.random() * total);
ul.appendChild(li);
}
loop(curTotal - pageCount, curIndex + pageCount)
})
}
loop(total, index)
script>
body>
不让v8一次事件循环就把js部分执行掉,浏览器一次性暴力渲染十万条,而是让v8执行五千次事件循环,浏览器每次只渲染二十条。这样v8分摊了浏览器渲染线程的压力,能减少页面加载时间
requestAnimationFrame + document.createDocumentFragment()
使用 setTimeout
将渲染操作放入下一个事件循环会有个小问题:假设浏览器页面刷新时间为 16.7ms,如果v8引擎的性能不够高,进行完一次事件循环的时间比 16.7ms 要长,那么浏览器在 16.7ms 内渲染完了 20 条数据,而 v8引擎还没将下一个20条数据给出来,这样就很可能会造成页面的闪屏或卡顿。
要解决定时器带来的事件循环与屏幕刷新不同步的问题,我们可以用 requestAnimationFrame()
取代 setTimeout()
。
在此基础上,我们需要尽量人为地减少回流重绘次数。如果每一次for循环都渲染一条数据,那这样高频地操作DOM会浪费开销影响性能。
所以,我们可以使用文档碎片 document.createDocumentFragment()
——一个虚拟的DOM, 来批量插入 li
元素。使得生成好一条数据后,先不要往 ul 里面添加,固定每20个li只回流一次
id="container">
虚拟列表
虚拟列表技术通过只渲染当前可见区域的数据来提高性能,而不是一次性渲染所有数据。这样可以显著减少 DOM 元素的数量,从而提高页面的加载速度和滚动流畅性。
核心思想
- 初始化容器和数据:创建固定高度的容器,准备数据源。
- 计算可视区域:获取容器高度,计算每个项的高度和可视区域的数据条数。
- 渲染初始可见区域:计算起始和结束索引,渲染初始数据。
- 监听滚动事件:绑定滚动事件,计算新的起始和结束索引,更新渲染数据。
- 调整样式:计算偏移量,处理实际列表跟随父容器一起移动的情况
接下来用 vue 技术栈展示虚拟列表实现步骤:
根组件 App.vue 中:
- 定义数据源
data
,里面存放一千个对象,每个对象包含id
和value
属性 - 引入自定义的
virtualList
组件,并通过:listData
属性将data
传递给它 - 设置
.app
类的样式,使其具有固定的宽度和高度,并添加边框以区分容器区域
<template>
<div class="app">
<virtualList :listData="data" />
div>
template>
<script setup>
import { ref } from 'vue';
import virtualList from './components/virtualList.vue';
const data = ref([])
for(let i = 0; i < 1000; i++) {
data.value.push({id: i, value: i})
}
script>
<style lang="css" scoped>
.app{
width: 300px;
height: 400px;
border: 1px solid #000;
}
style>
自定义 virtualList 组件中:
模板部分——
- 根元素
infinite-list-container
:绑定了 ref 为 listRef,用于后续获取 DOM 元素,并且绑定滚动事件处理器 scrollEvent - 虚拟占位元素
infinite-list-phantom
:用于撑开父容器的高度,确保可以滚动。其高度由 listHeight 计算得出 - 实际列表元素
infinite-list
:使用 transform 属性来控制列表的位置 - 列表项元素
infinite-list-item
:通过 v-for 循环渲染 visibleData 中的数据项。每个项的高度和行高由 itemSize 控制
"listRef" class="infinite-list-container" @scroll="scrollEvent()">
class="infinite-list-phantom" :style="{height: listHeight + 'px'}">
class="infinite-list" :style="{transform: getTransform}">
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{height: itemSize + 'px', lineHeight: itemSize + 'px'}"
>
{{ item.value }}
</div>
>
</div>
>
脚本及样式部分——
<script setup>
import { onMounted, reactive, ref, computed } from 'vue';
const props = defineProps({
listData: [],
itemSize: {
type: Number,
default: 50
}
})
const state = reactive({
screenHeight: 0,
startOffset: 0,
start: 0,
end: 0
})
const visibleCount = computed(() => {
return state.screenHeight / props.itemSize
})
const visibleData = computed(() => {
return props.listData.slice(state.start, Math.min(props.listData.length, state.end))
})
const listHeight = computed(() => {
return props.listData.length * props.itemSize
})
const getTransform = computed(() => {
return `translateY(${state.startOffset}px)`
})
const listRef = ref(null)
onMounted(() => {
state.screenHeight = listRef.value.clientHeight
state.end = state.start + visibleCount.value
})
const scrollEvent = () => {
let scrollTop = listRef.value.scrollTop
state.start = Math.floor(scrollTop / props.itemSize)
state.end = state.start + visibleCount.value
state.startOffset = scrollTop - (scrollTop % props.itemSize)
}
script>
<style lang="css" scoped>
.infinite-list-container {
height: 100%;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.infinite-list-phantom{
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.infinite-list{
position: absolute;
left: 0;
top: 0;
right: 0;
text-align: center;
}
.infinite-list-item {
border-bottom: 1px, solid, #000;
box-sizing: border-box;
}
style>
效果:
总结
- 时间分片:
- 根据事件循环机制,每一次事件循环都会先进行页面渲染,再执行宏任务。
- 所以可以使用
setTimeout
或 requestAnimationFrame
定时器将生成数据的js线程操作和渲染数据的渲染线程操作隔离到两次事件循环中,这样浏览器就能分批次地渲染一部分数据。
- 再配合文档碎片
document.createDocumentFragment()
减少回流次数,提高性能
- 虚拟列表:
- 拿到所有数据,计算出所有数据应有列表高度;
- 获取可视区域的高度,计算出可视区域中能渲染的数据条数
- 在实时滚动时计算要渲染的数据起始和截止下标,仅渲染那些在当前视口中的数据项
- 计算偏移量,调整样式
除了以上这两种方法,还可以采用懒加载、Web Workers等方法对渲染大量数据的操作进行优化,希望对你有所帮助。
(都看到这了,点赞收藏一下再走吧~)
阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=20921,转载请注明出处。
评论0