LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

十万条数据的优雅加载:分批渲染与虚拟列表

admin
2024年9月26日 13:34 本文热度 563

一:问题分析

最近秋招开始面试了,在前端岗的面试中遇到这样的一个情景题,这题目考察的是对前端性能优化的理解以及处理大数据量时的技术方案。下面带友友们来剖一剖,首先我们先来一个小demo来看看一次性渲染十万条数据的效果是怎么样的,我们在一个HTML页面中创建一个包含10万个<li>元素的<ul>列表,并记录整个过程的时间开销

<body>

  <ul id="container"></ul>

  <script>

    let ul = document.getElementById("container");

    const total = 100000;

    let now = Date.now();

    for (let i = 0; i < total; i++) {

      let li = document.createElement("li")

      li.innerHTML = ~~(Math.random() * total)

      ul.appendChild(li)

    }

    console.log('js运行耗时:', Date.now() - now);

    setTimeout(() => {

      console.log('页面加载总时长:', Date.now() - now);    

    })     

  </script>

</body>

?

可以看到,导致页面加载缓慢的是页面渲染速度过慢,js的运行速度还算可以的。以上demo中是暴力渲染,接下来带友友们了解两个方法来加快渲染速度,提升用户体验感以及页面渲染效率

二:分批渲染

递归渲染函数

  • loop函数接收两个参数:当前还需要渲染的总数(curTotal)和当前已渲染的数量(curIndex),计算本次需要渲染的数量(pageCount),并且不超过剩余的数量。
  • 使用setTimeout来异步执行DOM操作,这允许浏览器有时间去处理其他任务(如事件处理、绘制等)。
  • setTimeout的回调函数中,使用for循环创建<li>元素,并将其追加到<ul>容器中。
  • 如果还有剩余数据需要渲染,则继续递归调用loop函数。

<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.innerHTML = curIndex + i + ':' + ~~(Math.random() * total);

          ul.appendChild(li);

        }

        loop(curTotal - pageCount, curIndex + pageCount);

      })

    }

    loop(total, index);

  </script>

</body>

浏览器的刷新频率通常是每秒60帧,即大约每16.7毫秒刷新一次,虽然使用setTimeout可以减轻浏览器的负担,但setTimeout默认延迟时间为0,这意味着它会在当前任务队列结束后执行,也就是说定时器生效时间并不是固定的。v8引擎的事件循环机制中,下一个事件不一定要等到16.7ms,但如果v8引擎没有跟上,在一个或者多个16.7ms后没有进入到下一个事件中,由于是非阻塞的,就可能造成它的执行时间与页面的刷新时间并不完全同步。这意味着浏览器在渲染时可能无法及时更新屏幕,特别是在大量DOM操作的情况下。这可能导致以下问题:

  • 闪屏:当浏览器试图渲染大量的DOM元素时,如果DOM操作过于密集,浏览器可能无法及时完成渲染,导致用户看到部分渲染的内容,造成屏幕闪烁。
  • 白屏:在极端情况下,如果DOM操作过于复杂或耗时,浏览器可能无法在短时间内完成渲染,导致屏幕呈现为空白状态,直到渲染完成。

我们将setTimeout改为requestAnimationFrame,可以很好解决这个不同步的问题,requestAnimationFrame具有以下特性:

  1. 同步刷新频率requestAnimationFrame  虽然是嵌入到事件循环机制中的,但它是在渲染阶段之前执行,而不是像  setTimeout  或  setInterval  那样在回调队列中排队执行,并且requestAnimationFrame会在浏览器准备绘制下一帧前调用提供的回调函数,这样可以确保动画与屏幕刷新频率同步。
  2. 性能优化:如果浏览器处于后台或者标签页不可见状态,requestAnimationFrame  会自动暂停,从而节省CPU资源。

解决了以上不同步的问题,还有性能方面的细节我们也要注意。由于不知道优化队列具体能装多少条数据,并且每循环一次就要回流重绘一次,因此以上的分批渲染会引起多次回流重绘。为了避免上述问题,可以使用文档片段(Document Fragment)来构建DOM结构。

文档片段是一个没有标签的节点,可以在内存中构建完整的DOM结构,然后再一次性插入到文档中,这样可以显著减少页面的回流次数。

requestAnimationFrame(() => {

  let fragment = document.createDocumentFragment();

  for (let i = 0; i < pageCount; i++) {

    let li = document.createElement("li");

    li.innerHTML = curIndex + i + ':' + ~~(Math.random() * total);

    fragment.appendChild(li);

  }

  ul.appendChild(fragment);

  loop(curTotal - pageCount, curIndex + pageCount);

})

三:虚拟列表

虚拟列表通过只渲染当前可视区域的数据,而不是整个数据集,从而减少DOM操作和提高了应用性能,虚拟列表的关键在于动态计算和渲染当前可视区域内的数据,并在用户滚动时更新这些数据。

核心思路

  1. 获取整个页面的真实数据的高度。
  2. 计算可视区的高度以及其中可以放置的数据条数。
  3. 在用户滚动页面时,实时计算出起始下标和结束下标。
  4. 对样式进行偏移,避免屏幕错误的移动。

下面用vue项目进行展示,带友友们实现虚拟列表,主要涉及两个页面:App.vue和自定义组件virtualList.vue

3.1: 主组件App.vue

创建一个容器,用于展示虚拟列表组件, 将虚拟列表组件  virtualList  渲染到容器中,并传递  listData  属性。

<template>

  <div class="app">

    <virtualList :listData="data" />

  </div>

</template>

导入  virtualList  组件。初始化  data  数组,包含 10 万个对象,每个对象都有  id  和  value  属性。

<script setup>

import virtualList from './components/virtualList.vue';

const data = []

for (let i = 0; i < 100000; i++) {

  data.push({id: i, value: i})

}

</script>

3.2: 自定义组件virtualList.vue

1: 模板部分

<div ref="listRef" class="infinite-list-container" @scroll="scrollEvent()">

  <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>

  <div class="infinite-list" :style="{ transform: getTransform }">

    <div 

      class="infinite-list-item" 

      v-for="item in visibleData" 

      :key="item.id"

      :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }"

    >

      {{ item.value }}

    </div>

  </div>

</div>

  • 容器

<div ref="listRef" class="infinite-list-container" @scroll="scrollEvent()">
    • 创建一个名为  .infinite-list-container  的  <div>  容器。
    • 通过  ref  获取该元素的引用。
    • 添加  @scroll  事件监听器,当容器滚动时触发  scrollEvent  函数。
  • 占位符

<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
    • 创建一个名为  .infinite-list-phantom  的  <div>  占位符。
    • 设置占位符的高度为  listHeight
  • 实际列表

<div class="infinite-list" :style="{ transform: getTransform }">

  <div 

    class="infinite-list-item" 

    v-for="item in visibleData" 

    :key="item.id"

    :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }"

  >

    {{ item.value }}

  </div>

</div>

    • 创建一个名为  .infinite-list  的  <div>  实际列表。
    • 使用  :style  绑定属性  transform  为  getTransform  的值。
    • 使用  v-for  循环遍历  visibleData  数组,并渲染每个  item
    • 设置每个列表项的高度和行高。

2: 脚本部分

  1. 初始化容器和数据

    • 使用  ref  获取列表容器的引用。
    • 初始化状态对象  state,包括可视区高度、偏移量、起始索引和结束索引。
  2. 计算可视区数据

    • 计算可视区高度和可显示的数据条数。
    • 通过  slice  方法截取当前可视区域内的数据片段。
  3. 动态更新列表

    • 在滚动事件中实时更新起始索引和结束索引,从而更新当前可视区域的数据。
    • 使用  transform  属性对列表进行偏移,确保列表随用户的滚动而平滑移动。
  4. 样式优化

    • 使用绝对定位和变换来控制列表的位置,减少DOM重排和重绘。
    • 通过占位符(phantom)来模拟整个列表的高度,确保滚动流畅。?

import { computed, nextTick, onMounted, reactive, ref } 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(state.end, props.listData.length))

})

// 当前列表总高度

const listHeight = computed(() => {

  return props.listData.length * props.itemSize

})

// list跟着父容器移动了,现在列表要移动回来

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)

}

  • 导入和定义属性

import { computed, nextTick, onMounted, reactive, ref } from 'vue';

const props = defineProps({

  listData: [],

  itemSize: {

    type: Number,

    default: 50

  }

})

const state = reactive({

  screenHeight: 0, 

  startOffset: 0,

  start: 0,

  end: 0

})

    • 导入必要的Vue Composition API函数。
    • 定义  props  属性,包含  listData  和  itemSize
    • 使用  reactive  创建响应式状态对象  state
  • 计算属性

const visibleCount = computed(() => {

  return state.screenHeight / props.itemSize

3})

const visibleData = computed(() => {

  return props.listData.slice(state.start, Math.min(state.end, props.listData.length))

})

const listHeight = computed(() => {

  return props.listData.length * props.itemSize

})

const getTransform = computed(() => {

  return `translateY(${state.startOffset}px)`

})

    • visibleCount  计算可视区可以显示的数据条数。
    • visibleData  计算当前可视区域的实际数据。
    • listHeight  计算整个列表的高度。
    • getTransform  计算列表的偏移量。
  • 引用和生命周期钩子

const listRef = ref(null)

onMounted(() => {

  state.screenHeight = listRef.value.clientHeight

  state.end = state.start + visibleCount.value

})

    • 使用  ref  获取容器的引用。
    • 在  onMounted  生命周期钩子中初始化  screenHeight  和  end
  • 滚动事件处理

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)

}

    • scrollEvent  函数在滚动时更新  startend  和  startOffset

四:总结

在前端面试中探讨一次性渲染十万条数据的问题时,面试官主要想考察的是,是否理解性能优化的重要性,比如通过分页或无限滚动来减少单次加载的数据量,是否掌握虚拟滚动技术,仅渲染当前可视区域的内容,以及是否了解如何利用虚拟DOMWeb Workers等技术来提升应用性能,确保良好的用户体验。

本文带友友们实现了前两种,至于Web Workers之后会单开一篇仔细讲讲。此外,在面试中遇到这样的问题,友友们要有  性能意识,最好可以掌握  分页技术懒加载(lazy loading)  ,  无限滚动(infinite scrolling)  ,  虚拟滚动(virtual scrolling)  。当然,像数据压缩,服务器端渲染在某些场景下的优势(如SEO),或者利用流式数据处理技术来逐步加载和渲染数据,也可以对性能进行优化。


本文来源于稀土掘金技术社区,作者:midsummer18

原文链接:https://juejin.cn/post/7414732910240874531


该文章在 2024/9/27 11:56:03 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2024 ClickSun All Rights Reserved