狠狠色丁香婷婷综合尤物/久久精品综合一区二区三区/中国有色金属学报/国产日韩欧美在线观看 - 国产一区二区三区四区五区tv

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

【JavaScript】WEB開發時如何實現一次性渲染十萬條數據

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

本文轉載于稀土掘金技術社區,作者:反應熱

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

前言

當面試官問:給你十萬條數據,你會怎么辦?這時我們該如何應對呢?

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

正文

最直接的方法

下面是最直接的方法,一次性創建所有的列表項并添加到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運行耗時',Date.now()-now)
            setTimeout(()=>{
                console.log('運行耗時',Date.now()-now)
            })
        </script>

    </body>
    </html>

image.png

代碼解釋:

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

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

setTimeout分批渲染

為了避免一次性操作引起瀏覽器卡頓,我們可以使用setTimeout將創建和添加操作分散到多個時間點,每次只渲染一部分數據。

    <!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>

代碼解釋:

  • 這里我們將所有數據分批渲染,每批次添加20個元素,因為到最后可能會不足20個所有我們用Math.min(once,curTotal)取兩者小的那個,如果還有剩余的元素需要添加,則遞歸調用loop函數繼續處理,每次遞歸減去相應數量。
  • 首先上來執行一遍,同步,異步,然后渲染,啥也沒有渲染對吧,然后執行setTimeout也就是宏任務,然后再向剛剛一樣同步,異步,然后渲染,這時候可以渲染20條數據,接著再這樣一直遞歸到數據加載完畢。

結論:

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

使用requestAnimationFrame

requestAnimationFrame是一個比setTimeout更優秀的解決方案,因為它就是屏幕刷新率的時間。

    <!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,這使得操作更加流暢,就是在屏幕畫面刷新的時候渲染,就避免了上面的問題。

結論: 通過requestAnimationFrame代替setTimeout,在屏幕畫面刷新的時候渲染,就避免了上面setTimeout可能出現的問題。

使用文檔碎片(requsetAnimationFrame+DocuemntFragment )

文檔碎片是一種可以暫時存放DOM節點的“容器”,它不會出現在文檔流中。當所有節點都準備好之后,再一次性添加到DOM中,可以減少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 once= 20
            let page=total/once
            let index=0

            function loop(curTotal,curIndex){
                let fragment =document.createDocumentFragment(); //創建文檔碎片
                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>

代碼解釋:

  • 創建一個DocumentFragment實例fragment來暫存<li>元素,在循環內部,將生成的<li>元素添加到fragment中,你可以理解為一個虛假的標簽,把<li>掛在這個標簽上,只不過這個標簽不會出現在DOM中。
  • 循環結束后,一次性將fragment添加到<ul>元素中,這樣就減少了DOM操作次數,提高了性能。

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

用虛擬滾動(Virtual Scrolling)

對于非常大的數據集,最佳實踐是使用虛擬滾動技術,現在很多公司都是用的這種方法。虛擬滾動只渲染當前可視區域內的數據,當用戶滾動時,動態替換這些數據。

這里使用vue實現一個簡單的虛擬滾動列表。

image.png

就兩個文件

App.vue

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

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

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


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

virtualList.vue

    <template>
      <!-- 可視區域 -->
      <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>
    </template>

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

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

    // 反應式狀態
    const state = reactive({
      screenHeight0// 可視區域高度
      startOffset0// 當前偏移量
      start0// 開始索引
      end0 // 結束索引
    });

    // 計算屬性
    const visibleCount = computed(() => {
      return Math.ceil(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; // 列表總高度
    });

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

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

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

    // 滾動事件處理
    const scrollEvent = () => {
      const 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 {
      height100%/* 占滿整個父容器高度 */
      overflow: auto; /* 允許滾動 */
      position: relative; /* 使內部元素可以相對于它定位 */
    }

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

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

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



**代碼解釋:**

`可視區域`

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

這個是可視的區域,就好比電腦和手機能看到東西的窗口大小,這是用戶實際可以看到的區域,它有一個固定的大小,并且允許滾動

`虛擬高度占位符`

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

 

這個占位符的作用是模擬整個數據集的高度,即使實際上并沒有渲染所有的數據項。它是一個不可見的元素,高度等于所有數據項的高度之和。

動態渲染數據的區域

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

image.png

這部分負責實際顯示數據項,和可視化的區域一樣大,它通過 transform 屬性調整位置,確保只顯示當前可視區域內的數據項。

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

結論: 虛擬滾動的核心思想是只渲染當前可視區域的數據,而不是一次性渲染整個數據集。這在處理大數據量時尤為重要,因為它可以顯著提高應用的性能和響應速度。

總結


通過上述五個方法,我們從最基本的DOM操作的方法到使用現代前端技術使用的方法,本文到此就結束了,希望對你有所幫助!


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