LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

十萬條數(shù)據(jù)的優(yōu)雅加載:分批渲染與虛擬列表

admin
2024年9月26日 13:34 本文熱度 1429

一:問題分析

最近秋招開始面試了,在前端崗的面試中遇到這樣的一個情景題,這題目考察的是對前端性能優(yōu)化的理解以及處理大數(shù)據(jù)量時的技術(shù)方案。下面帶友友們來剖一剖,首先我們先來一個小demo來看看一次性渲染十萬條數(shù)據(jù)的效果是怎么樣的,我們在一個HTML頁面中創(chuàng)建一個包含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中是暴力渲染,接下來帶友友們了解兩個方法來加快渲染速度,提升用戶體驗感以及頁面渲染效率

二:分批渲染

遞歸渲染函數(shù)

  • loop函數(shù)接收兩個參數(shù):當前還需要渲染的總數(shù)(curTotal)和當前已渲染的數(shù)量(curIndex),計算本次需要渲染的數(shù)量(pageCount),并且不超過剩余的數(shù)量。
  • 使用setTimeout來異步執(zhí)行DOM操作,這允許瀏覽器有時間去處理其他任務(wù)(如事件處理、繪制等)。
  • setTimeout的回調(diào)函數(shù)中,使用for循環(huán)創(chuàng)建<li>元素,并將其追加到<ul>容器中。
  • 如果還有剩余數(shù)據(jù)需要渲染,則繼續(xù)遞歸調(diào)用loop函數(shù)。

<body>

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

  <script>

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

    const total = 100000;

    let once = 20 //單次渲染數(shù)

    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,這意味著它會在當前任務(wù)隊列結(jié)束后執(zhí)行,也就是說定時器生效時間并不是固定的。v8引擎的事件循環(huán)機制中,下一個事件不一定要等到16.7ms,但如果v8引擎沒有跟上,在一個或者多個16.7ms后沒有進入到下一個事件中,由于是非阻塞的,就可能造成它的執(zhí)行時間與頁面的刷新時間并不完全同步。這意味著瀏覽器在渲染時可能無法及時更新屏幕,特別是在大量DOM操作的情況下。這可能導致以下問題:

  • 閃屏:當瀏覽器試圖渲染大量的DOM元素時,如果DOM操作過于密集,瀏覽器可能無法及時完成渲染,導致用戶看到部分渲染的內(nèi)容,造成屏幕閃爍。
  • 白屏:在極端情況下,如果DOM操作過于復雜或耗時,瀏覽器可能無法在短時間內(nèi)完成渲染,導致屏幕呈現(xiàn)為空白狀態(tài),直到渲染完成。

我們將setTimeout改為requestAnimationFrame,可以很好解決這個不同步的問題,requestAnimationFrame具有以下特性:

  1. 同步刷新頻率requestAnimationFrame  雖然是嵌入到事件循環(huán)機制中的,但它是在渲染階段之前執(zhí)行,而不是像  setTimeout  或  setInterval  那樣在回調(diào)隊列中排隊執(zhí)行,并且requestAnimationFrame會在瀏覽器準備繪制下一幀前調(diào)用提供的回調(diào)函數(shù),這樣可以確保動畫與屏幕刷新頻率同步。
  2. 性能優(yōu)化:如果瀏覽器處于后臺或者標簽頁不可見狀態(tài),requestAnimationFrame  會自動暫停,從而節(jié)省CPU資源。

解決了以上不同步的問題,還有性能方面的細節(jié)我們也要注意。由于不知道優(yōu)化隊列具體能裝多少條數(shù)據(jù),并且每循環(huán)一次就要回流重繪一次,因此以上的分批渲染會引起多次回流重繪。為了避免上述問題,可以使用文檔片段(Document Fragment)來構(gòu)建DOM結(jié)構(gòu)。

文檔片段是一個沒有標簽的節(jié)點,可以在內(nèi)存中構(gòu)建完整的DOM結(jié)構(gòu),然后再一次性插入到文檔中,這樣可以顯著減少頁面的回流次數(shù)。

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);

})

三:虛擬列表

虛擬列表通過只渲染當前可視區(qū)域的數(shù)據(jù),而不是整個數(shù)據(jù)集,從而減少DOM操作和提高了應用性能,虛擬列表的關(guān)鍵在于動態(tài)計算和渲染當前可視區(qū)域內(nèi)的數(shù)據(jù),并在用戶滾動時更新這些數(shù)據(jù)。

核心思路

  1. 獲取整個頁面的真實數(shù)據(jù)的高度。
  2. 計算可視區(qū)的高度以及其中可以放置的數(shù)據(jù)條數(shù)。
  3. 在用戶滾動頁面時,實時計算出起始下標和結(jié)束下標。
  4. 對樣式進行偏移,避免屏幕錯誤的移動。

下面用vue項目進行展示,帶友友們實現(xiàn)虛擬列表,主要涉及兩個頁面:App.vue和自定義組件virtualList.vue

3.1: 主組件App.vue

創(chuàng)建一個容器,用于展示虛擬列表組件, 將虛擬列表組件  virtualList  渲染到容器中,并傳遞  listData  屬性。

<template>

  <div class="app">

    <virtualList :listData="data" />

  </div>

</template>

導入  virtualList  組件。初始化  data  數(shù)組,包含 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()">
    • 創(chuàng)建一個名為  .infinite-list-container  的  <div>  容器。
    • 通過  ref  獲取該元素的引用。
    • 添加  @scroll  事件監(jiān)聽器,當容器滾動時觸發(fā)  scrollEvent  函數(shù)。
  • 占位符

<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
    • 創(chuàng)建一個名為  .infinite-list-phantom  的  <div>  占位符。
    • 設(shè)置占位符的高度為  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>

    • 創(chuàng)建一個名為  .infinite-list  的  <div>  實際列表。
    • 使用  :style  綁定屬性  transform  為  getTransform  的值。
    • 使用  v-for  循環(huán)遍歷  visibleData  數(shù)組,并渲染每個  item
    • 設(shè)置每個列表項的高度和行高。

2: 腳本部分

  1. 初始化容器和數(shù)據(jù)

    • 使用  ref  獲取列表容器的引用。
    • 初始化狀態(tài)對象  state,包括可視區(qū)高度、偏移量、起始索引和結(jié)束索引。
  2. 計算可視區(qū)數(shù)據(jù)

    • 計算可視區(qū)高度和可顯示的數(shù)據(jù)條數(shù)。
    • 通過  slice  方法截取當前可視區(qū)域內(nèi)的數(shù)據(jù)片段。
  3. 動態(tài)更新列表

    • 在滾動事件中實時更新起始索引和結(jié)束索引,從而更新當前可視區(qū)域的數(shù)據(jù)。
    • 使用  transform  屬性對列表進行偏移,確保列表隨用戶的滾動而平滑移動。
  4. 樣式優(yōu)化

    • 使用絕對定位和變換來控制列表的位置,減少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

})

// 可視區(qū)顯示的數(shù)據(jù)條數(shù)

const visibleCount = computed(() => {

  return state.screenHeight / props.itemSize

})

// 可視區(qū)域顯示的真實數(shù)據(jù)

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跟著父容器移動了,現(xiàn)在列表要移動回來

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函數(shù)。
    • 定義  props  屬性,包含  listData  和  itemSize
    • 使用  reactive  創(chuàng)建響應式狀態(tài)對象  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  計算可視區(qū)可以顯示的數(shù)據(jù)條數(shù)。
    • visibleData  計算當前可視區(qū)域的實際數(shù)據(jù)。
    • 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  函數(shù)在滾動時更新  startend  和  startOffset

四:總結(jié)

在前端面試中探討一次性渲染十萬條數(shù)據(jù)的問題時,面試官主要想考察的是,是否理解性能優(yōu)化的重要性,比如通過分頁或無限滾動來減少單次加載的數(shù)據(jù)量,是否掌握虛擬滾動技術(shù),僅渲染當前可視區(qū)域的內(nèi)容,以及是否了解如何利用虛擬DOMWeb Workers等技術(shù)來提升應用性能,確保良好的用戶體驗。

本文帶友友們實現(xiàn)了前兩種,至于Web Workers之后會單開一篇仔細講講。此外,在面試中遇到這樣的問題,友友們要有  性能意識,最好可以掌握  分頁技術(shù)懶加載(lazy loading)  ,  無限滾動(infinite scrolling)  ,  虛擬滾動(virtual scrolling)  。當然,像數(shù)據(jù)壓縮,服務(wù)器端渲染在某些場景下的優(yōu)勢(如SEO),或者利用流式數(shù)據(jù)處理技術(shù)來逐步加載和渲染數(shù)據(jù),也可以對性能進行優(yōu)化。


本文來源于稀土掘金技術(shù)社區(qū),作者:midsummer18

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


該文章在 2024/9/27 11:56:03 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點晴ERP是一款針對中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點晴PMS碼頭管理系統(tǒng)主要針對港口碼頭集裝箱與散貨日常運作、調(diào)度、堆場、車隊、財務(wù)費用、相關(guān)報表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點,圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點晴WMS倉儲管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務(wù)都免費,不限功能、不限時間、不限用戶的免費OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved

黄频国产免费高清视频,久久不卡精品中文字幕一区,激情五月天AV电影在线观看,欧美国产韩国日本一区二区
日韩精品欧美国产精品亚 | 日本久久高清视频 | 亚洲欧美动漫少妇自拍 | 亚洲色影视在线播放 | 亚洲人成在线俺来了 | 亚洲A综合一区二区三区 |