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

【JavaScript】WEB開發(fā)時(shí)如何實(shí)現(xiàn)一次性渲染十萬條數(shù)據(jù)

admin
2024年8月28日 22:39 本文熱度 930

本文轉(zhuǎn)載于稀土掘金技術(shù)社區(qū),作者:反應(yīng)熱

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

前言

當(dāng)面試官問:給你十萬條數(shù)據(jù),你會(huì)怎么辦?這時(shí)我們?cè)撊绾螒?yīng)對(duì)呢?

在實(shí)際的Web開發(fā)中,有時(shí)我們需要在頁面上展示大量的數(shù)據(jù),比如用戶評(píng)論、商品列表等。如果一次性渲染太多的數(shù)據(jù)(如100,000條數(shù)據(jù)),直接將所有數(shù)據(jù)一次性渲染到頁面上會(huì)導(dǎo)致瀏覽器卡頓,用戶體驗(yàn)變差。下面我們從一個(gè)簡(jiǎn)單的例子開始,逐步改進(jìn)代碼,直到使用現(xiàn)代框架的虛擬滾動(dòng)技術(shù)來解決這個(gè)問題,看完本文后,你就可以跟面試官侃侃而談了。

正文

最直接的方法

下面是最直接的方法,一次性創(chuàng)建所有的列表項(xiàng)并添加到DOM樹中。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <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.innerText=~~(Math.random()*total)
               ul.appendChild(li)
            }
            
            console.log('js運(yùn)行耗時(shí)',Date.now()-now)
            setTimeout(()=>{
                console.log('運(yùn)行耗時(shí)',Date.now()-now)
            })
        </script>

    </body>
    </html>

image.png

代碼解釋:

  • 我們獲取了一個(gè)<ul>元素,并定義了一個(gè)總數(shù)total為1000,使用for循環(huán)來創(chuàng)建<li>元素,并給每個(gè)元素設(shè)置一個(gè)文本值,~~為向下取整, 每個(gè)新創(chuàng)建的<li>都被添加到<ul>元素中。
  • 我們記錄了整個(gè)過程的耗時(shí),可以看到js引擎在編譯完代碼只花了92ms還是非常快的。
  • 而定時(shí)器耗時(shí)了3038ms,我們知道js引擎是單線程工作的,首先它會(huì)執(zhí)行同步代碼,然后再執(zhí)行微任務(wù),接著再在瀏覽器上渲染,最后執(zhí)行宏任務(wù),setTimeout這里我們?nèi)藶榈膶懸粋€(gè)宏任務(wù),這個(gè)打印的出來時(shí)間可以看成開始運(yùn)行代碼再到瀏覽器把數(shù)據(jù)渲染所花的時(shí)間對(duì)吧,可以看到還是要一會(huì)的對(duì)吧。

結(jié)論: 這種方法雖然實(shí)現(xiàn)起來簡(jiǎn)單直接,但由于它在一個(gè)循環(huán)中創(chuàng)建并添加了所有列表項(xiàng)至DOM樹,因此在執(zhí)行過程中,瀏覽器需要等待JavaScript完全執(zhí)行完畢才能開始渲染頁面。當(dāng)數(shù)據(jù)量非常大(例如本例中的100,000個(gè)列表項(xiàng))時(shí),這種大量的DOM操作會(huì)導(dǎo)致瀏覽器的渲染隊(duì)列積壓大量工作,從而引發(fā)頁面的回流與重繪,瀏覽器無法進(jìn)行任何渲染操作,導(dǎo)致了所謂的“阻塞”渲染。

setTimeout分批渲染

為了避免一次性操作引起瀏覽器卡頓,我們可以使用setTimeout將創(chuàng)建和添加操作分散到多個(gè)時(shí)間點(diǎn),每次只渲染一部分數(shù)據(jù)。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <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>
    </html>

代碼解釋:

  • 這里我們將所有數(shù)據(jù)分批渲染,每批次添加20個(gè)元素,因?yàn)榈阶詈罂赡軙?huì)不足20個(gè)所有我們用Math.min(once,curTotal)取兩者小的那個(gè),如果還有剩余的元素需要添加,則遞歸調(diào)用loop函數(shù)繼續(xù)處理,每次遞歸減去相應(yīng)數(shù)量。
  • 首先上來執(zhí)行一遍,同步,異步,然后渲染,啥也沒有渲染對(duì)吧,然后執(zhí)行setTimeout也就是宏任務(wù),然后再向剛剛一樣同步,異步,然后渲染,這時(shí)候可以渲染20條數(shù)據(jù),接著再這樣一直遞歸到數(shù)據(jù)加載完畢。

結(jié)論:

  • 這里就是把瀏覽器渲染時(shí)的壓力分?jǐn)偨o了js引擎js引擎是單線程工作的,先執(zhí)行同步,異步,然后瀏覽器渲染,再宏任務(wù),這里就很好的利用了這一點(diǎn),把渲染的任務(wù)分批執(zhí)行,減輕了瀏覽器一次要渲染大量數(shù)據(jù)造成的渲染“阻塞”,也很好的解決了數(shù)據(jù)過多時(shí)可能造成頁面卡頓或白屏的問題,
  • 但是有點(diǎn)小問題,我們現(xiàn)在用的電腦屏幕刷新率基本上都是60Hz,意味著它每秒鐘可以刷新顯示60次新的畫面。如果我們以此為例計(jì)算,那么兩次刷新之間的時(shí)間間隔大約是16.67毫秒,如果說當(dāng)執(zhí)行本次宏任務(wù)里的同步,異步,然后渲染這個(gè)時(shí)間點(diǎn)是在16.67ms以后也就是屏幕畫面剛刷新完以后,是不是得等到下一次的16.67ms屏幕畫面刷新才能有數(shù)據(jù)看到,所有當(dāng)用戶往下翻的時(shí)候有可能那一瞬間看不到東西,但是很快馬上就有了,這個(gè)問題不是你迅速往下拉數(shù)據(jù)沒加載那個(gè),這個(gè)問題現(xiàn)在是不法完成避免的。

使用requestAnimationFrame

requestAnimationFrame是一個(gè)比setTimeout更優(yōu)秀的解決方案,因?yàn)樗褪瞧聊凰⑿侣实臅r(shí)間。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <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)
                requestAnimationFrame(()=>{
                    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>
    </html>

代碼解釋:

  • 和使用setTimeout類似,這里我們也使用分批處理。
  • 不同之處在于使用了requestAnimationFrame代替setTimeout,這使得操作更加流暢,就是在屏幕畫面刷新的時(shí)候渲染,就避免了上面的問題。

結(jié)論: 通過requestAnimationFrame代替setTimeout,在屏幕畫面刷新的時(shí)候渲染,就避免了上面setTimeout可能出現(xiàn)的問題。

使用文檔碎片(requsetAnimationFrame+DocuemntFragment )

文檔碎片是一種可以暫時(shí)存放DOM節(jié)點(diǎn)的“容器”,它不會(huì)出現(xiàn)在文檔流中。當(dāng)所有節(jié)點(diǎn)都準(zhǔn)備好之后,再一次性添加到DOM中,可以減少DOM操作次數(shù)。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <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 fragment =document.createDocumentFragment(); //創(chuàng)建文檔碎片
                let pageCount=Math.min(once,curTotal)
                requestAnimationFrame(()=>{
                    for(let i=0;i<pageCount;i++){
                        let li=document.createElement('li');
                        li.innerText=curIndex+i+':'+~~(Math.random()*total)
                        fragment.appendChild(li)
                    }
                    ul.appendChild(fragment)
                    loop(curTotal-pageCount,curIndex+pageCount)
                })
            }
            loop(total,index)
        </script>

    </body>
    </html>

代碼解釋:

  • 創(chuàng)建一個(gè)DocumentFragment實(shí)例fragment來暫存<li>元素,在循環(huán)內(nèi)部,將生成的<li>元素添加到fragment中,你可以理解為一個(gè)虛假的標(biāo)簽,把<li>掛在這個(gè)標(biāo)簽上,只不過這個(gè)標(biāo)簽不會(huì)出現(xiàn)在DOM中。
  • 循環(huán)結(jié)束后,一次性將fragment添加到<ul>元素中,這樣就減少了DOM操作次數(shù),提高了性能。

結(jié)論: 通過使用 DocumentFragment,可以在內(nèi)存中暫存一組 DOM 節(jié)點(diǎn),直到這些節(jié)點(diǎn)被一次性添加到 DOM 樹中。這樣做可以減少 DOM 的重排和重繪次數(shù),從而提高性能這對(duì)于提高頁面性能是非常重要的,尤其是在進(jìn)行大量的DOM更新時(shí)。

用虛擬滾動(dòng)(Virtual Scrolling)

對(duì)于非常大的數(shù)據(jù)集,最佳實(shí)踐是使用虛擬滾動(dòng)技術(shù),現(xiàn)在很多公司都是用的這種方法。虛擬滾動(dòng)只渲染當(dāng)前可視區(qū)域內(nèi)的數(shù)據(jù),當(dāng)用戶滾動(dòng)時(shí),動(dòng)態(tài)替換這些數(shù)據(jù)。

這里使用vue實(shí)現(xiàn)一個(gè)簡(jiǎn)單的虛擬滾動(dòng)列表。

image.png

就兩個(gè)文件

App.vue

    <template>
      <div class="app">
        <virtualList :listData="data"></virtualList>
      </div>
    </template>

    <script setup>
    import virtualList from './components/virtualList.vue'

    // 創(chuàng)建一個(gè)包含10萬條數(shù)據(jù)的大數(shù)組
    const data = []
    for (let i = 0; i < 100000; i++) {
      data.push({ id: i, value: i })
    }
    </script>


    <style lang="css" scoped>
    .app {
      height400px/* 設(shè)置可視區(qū)域的高度 */
      width300px/* 設(shè)置可視區(qū)域的寬度 */
      border1px solid #000/* 邊框,便于看到邊界 */
    }
    </style>

virtualList.vue

    <template>
      <!-- 可視區(qū)域 -->
      <div ref="listRef" class="infinite-list-container" @scroll="scrollEvent()">
        <!-- 虛擬高度占位符 -->
        <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>

        <!-- 動(dòng)態(tài)渲染數(shù)據(jù)的區(qū)域 -->
        <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>
    </template>

    <script setup>
    import { computed, nextTick, onMounted, ref } from 'vue';

    // 定義接收的屬性
    const props = defineProps({
      listDataArray,
      itemSize: {
        typeNumber,
        default50
      }
    });

    // 反應(yīng)式狀態(tài)
    const state = reactive({
      screenHeight0// 可視區(qū)域高度
      startOffset0// 當(dāng)前偏移量
      start0// 開始索引
      end0 // 結(jié)束索引
    });

    // 計(jì)算屬性
    const visibleCount = computed(() => {
      return Math.ceil(state.screenHeight / props.itemSize); // 可視區(qū)域內(nèi)能顯示的項(xiàng)目數(shù)量
    });

    const visibleData = computed(() => {
      return props.listData.slice(state.start, Math.min(state.end, props.listData.length)); // 當(dāng)前可視數(shù)據(jù)
    });

    const listHeight = computed(() => {
      return props.listData.length * props.itemSize; // 列表總高度
    });

    const getTransform = computed(() => {
      return `translateY(${state.startOffset}px)`// 計(jì)算transform值
    });

    // 引用元素
    const listRef = ref(null);

    // 生命周期鉤子
    onMounted(() => {
      state.screenHeight = listRef.value.clientHeight; // 初始化可視區(qū)域高度
      state.end = state.start + visibleCount.value; // 初始化結(jié)束索引
    });

    // 滾動(dòng)事件處理
    const scrollEvent = () => {
      const scrollTop = listRef.value.scrollTop; // 當(dāng)前滾動(dòng)距離
      state.start = Math.floor(scrollTop / props.itemSize); // 計(jì)算開始索引
      state.end = state.start + visibleCount.value; // 更新結(jié)束索引
      state.startOffset = scrollTop - (scrollTop % props.itemSize); // 更新偏移量
    };
    </script>


    <style lang="css" scoped>
    .infinite-list-container {
      height100%/* 占滿整個(gè)父容器高度 */
      overflow: auto; /* 允許滾動(dòng) */
      position: relative; /* 使內(nèi)部元素可以相對(duì)于它定位 */
    }

    .infinite-list-phantom {
      position: absolute; /* 絕對(duì)定位 */
      left0;
      right0/* 寬度充滿整個(gè)容器 */
      top0/* 頂部對(duì)齊 */
      z-index: -1/* 放在底層 */
    }

    .infinite-list {
      position: absolute; /* 絕對(duì)定位 */
      left0;
      right0/* 寬度充滿整個(gè)容器 */
      top0/* 頂部對(duì)齊 */
      text-align: center; /* 文本居中 */
    }

    .infinite-list-item {
      border-bottom1px solid #eee/* 分隔線 */
      box-sizing: border-box; /* 包含邊框和內(nèi)邊距 */
    }
    </style>



**代碼解釋:**

`可視區(qū)域`

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

![image.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/db74bf871da94fb3b45d8e91cdb1e782~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc29ycnloYw==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzA2MTQ3NjEzMDA0NDQ4NyJ9\&rk3s=e9ecf3d6\&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018\&x-orig-expires=1724928998\&x-orig-sign=p2QyI1b1YnbRxmYCIkATvAiwBuc%3D)

這個(gè)是可視的區(qū)域,就好比電腦和手機(jī)能看到東西的窗口大小,這是用戶實(shí)際可以看到的區(qū)域,它有一個(gè)固定的大小,并且允許滾動(dòng)

`虛擬高度占位符`

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

 

這個(gè)占位符的作用是模擬整個(gè)數(shù)據(jù)集的高度,即使實(shí)際上并沒有渲染所有的數(shù)據(jù)項(xiàng)。它是一個(gè)不可見的元素,高度等于所有數(shù)據(jù)項(xiàng)的高度之和。

動(dòng)態(tài)渲染數(shù)據(jù)的區(qū)域

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

image.png

這部分負(fù)責(zé)實(shí)際顯示數(shù)據(jù)項(xiàng),和可視化的區(qū)域一樣大,它通過 transform 屬性調(diào)整位置,確保只顯示當(dāng)前可視區(qū)域內(nèi)的數(shù)據(jù)項(xiàng)。

核心實(shí)現(xiàn)原理: 先拿到所有數(shù)據(jù)的占的區(qū)域,當(dāng)往下滾動(dòng)的時(shí)候,整個(gè)所有區(qū)域的數(shù)據(jù)會(huì)往上走(也就是這個(gè)div class="infinite-list-phantom"),而我們現(xiàn)在這個(gè)區(qū)域(div class="infinite-list")就是跟用戶看到的數(shù)據(jù)區(qū)域一樣大的區(qū)域也會(huì)往上滾,可以保證給的數(shù)據(jù)是正確的數(shù)據(jù),當(dāng)往上滾時(shí),用戶看到數(shù)據(jù)會(huì)更新并且會(huì)往上移動(dòng),變得越來越少,我們通過 transform 屬性調(diào)整位置把它移動(dòng)到我們固定的可視化的區(qū)域(div ref="listRef" class="infinite-list-container"),給用戶看的數(shù)據(jù)就是完整的數(shù)據(jù)了。也就相當(dāng)于我們這個(gè)有全部的虛假數(shù)據(jù)大小,我們只截取用戶能看到的真實(shí)的部分?jǐn)?shù)據(jù)給他們看。

結(jié)論: 虛擬滾動(dòng)的核心思想是只渲染當(dāng)前可視區(qū)域的數(shù)據(jù),而不是一次性渲染整個(gè)數(shù)據(jù)集。這在處理大數(shù)據(jù)量時(shí)尤為重要,因?yàn)樗梢燥@著提高應(yīng)用的性能和響應(yīng)速度。

總結(jié)


通過上述五個(gè)方法,我們從最基本的DOM操作的方法到使用現(xiàn)代前端技術(shù)使用的方法,本文到此就結(jié)束了,希望對(duì)你有所幫助!


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

黄频国产免费高清视频,久久不卡精品中文字幕一区,激情五月天AV电影在线观看,欧美国产韩国日本一区二区
亚洲成aV人片在线观看m | 色综合久久综合一区二区三区 | 日本三级2020亚洲视频 | 中文字幕制服丝袜在线播放 | 九九九热视频最新在线 | 久久久国产99久久国产久首页 |