打不过后端,十万条数据咱咬咬牙也能干

浅聊一下

后端一次性给我传了十万条数据,我真想提刀去和他对线,奈何打不过,擦干眼泪,硬着头皮上...别误会,是硬着头皮写代码...

模拟十万条数据

html 复制代码
<!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>
    <div id="container"></div>
    <script>
        const total = 100000
        let ul = document.createElement('ul');
        let div = document.getElementById('container');
        div.appendChild(ul);
        for(let i = 0;i<total;i++){
            let li = document.createElement('li');
            li.innerText = ~~(Math.random()*total);
            ul.appendChild(li);
        }
    </script>
</body>
</html>

当我在浏览器上打开他的时候,电脑风扇已经顶不住了,开始嗡嗡作响,那我们该如何来渲染这段数据呢?

setTimeout

第一个思路,既然我们一次性加载不了这么多图片,那么我们能不能分很多次加载呢?说干就干

html 复制代码
<!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>
        const total = 10000;
        let ul = document.getElementById('container'); // 修正:获取 #container 元素
        let once = 20;
        let page = Math.ceil(total / once); // 修正:计算页数
        let index = 0;
        function loop(curTotal) {
            if (curTotal <= 0) {
                return;
            }
            let pageCount = Math.min(curTotal, once);
            setTimeout(() => {
                for (let i = 0; i < pageCount; i++) {
                    let li = document.createElement('li'); // 修正:使用正确的标签名称 'li'
                    li.innerText = ~~(Math.random() * total);
                    ul.appendChild(li);
                }
                loop(curTotal - pageCount);
            }, 0); // 修正:指定延迟时间
        }
        loop(total);
    </script>
</body>

</html>
  1. 在这里,我们一次只加载20条数据,page就是我们要加载的次数
  2. 如果剩余数据少于20条,那么用pageCount = Math.min(curTotal, once);来计算
  3. 递归遍历,直到所有的数据全部渲染完成

这样,我们的十万条数据就成功渲染在了页面上,但是我发现了一个问题,当我向下滚动页面的时候,总是会存在短暂的白屏,不停地查找之下我发现:由于dom结构的渲染和页面的刷新率不同步造成闪烁

requestAnimationFrame + fragment(时间分片)

html 复制代码
<!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 now = Date.now();
        const total = 10000;
        let ul = document.getElementById('container'); // 修正:获取 #container 元素
        let once = 20;
        let page = Math.ceil(total / once); // 修正:计算页数
        let index = 0;
        function loop(curTotal) {
            if (curTotal <= 0) {
                return;
            }
            let pageCount = Math.min(curTotal, once);
            window.requestAnimationFrame(() => {
                let fragment
                for (let i = 0; i < pageCount; i++) {
                    fragment = document.createDocumentFragment();//创建一个虚拟的文档片段
                    let li = document.createElement('li'); // 修正:使用正确的标签名称 'li'
                    li.innerText = ~~(Math.random() * total);
                    fragment.appendChild(li);
                }
                ul.appendChild(fragment);
                loop(curTotal - pageCount);
            })
        }
        loop(total);
    </script>
</body>

</html>

在我们使用setTimeout方法来渲染数据的时候,因为每渲染一条数据都要操作DOM,所以性能不好,并且会造成闪烁,这里我们使用文档碎片和window.requestAnimationFrame()来解决这个问题...

  • 文档碎片

文档片段(DocumentFragment)是DOM中的一种节点类型,它允许将一组子节点添加到文档树中,而无需像普通操作一样频繁地更新DOM。文档片段可以看作是一个轻量级的虚拟容器,用于临时存储节点,然后一次性地将它们添加到文档中,这样可以减少DOM操作,提高性能。

  • window.requestAnimationFrame()

window.requestAnimationFrame() 是一个用于在浏览器重绘之前执行指定的函数的方法。它通常用于执行动画和其他需要在每一帧之间更新的任务,以确保动画的流畅性并提高性能。

主要特点包括:

  1. 与屏幕刷新同步requestAnimationFrame() 方法会在浏览器准备好重绘页面时执行指定的函数,通常是在下一帧绘制之前。这样可以确保动画和其他操作与屏幕刷新同步,避免了丢帧和动画不连贯的问题。
  2. 自动调节帧率:浏览器会根据当前设备的性能和显示器的刷新率来动态调节帧率,以确保最佳的用户体验。这意味着在性能较差的设备上,帧率会自动降低,而在性能较好的设备上,帧率会自动提高。
  3. 优化性能 :由于requestAnimationFrame() 方法会在浏览器重绘之前执行,因此可以更好地利用浏览器的重绘优化机制,从而提高性能并减少功耗。

我们将每一页的数据先放在文档碎片里,再将文档碎片作为子节点挂载到ul上

虚拟列表

掘友们想想,我们看不见的数据有必要渲染吗?联想图片懒加载,看不见的数据我直接不加载就完了...

我们只需要渲染可视区域的内容就好了,缓冲区域是为了让我们的滚动更丝滑,不会出现白屏闪烁的现象

  • 拿到可视区域
js 复制代码
const boxHeight = ref(0)//可视区域的高度
const itemHeight = ref(40)//列表项的高度
const scrollBox = ref(null)//可视区域容器
onMounted(() => {
    boxHeight.value = scrollBox.value.clientHeight
})
const itemNum = computed(() => {
   return ~~(boxHeight.value / itemHeight.value) + 2
 })
     const startIndex = ref(0)//可视区域第一项的下标
  • 滚动事件
js 复制代码
const doScroll = () => {
  const index = ~~(scrollBox.value.scrollTop / itemHeight.value)
    if (index === startIndex.value) return
    startIndex.value = index
    }
    const endIndex = computed(() => {//可视区域最后一项的索引
    let index = startIndex.value + itemNum.value * 2
    if (!allList.value[index]) {
        index = allList.value.length - 1
    }
    return index
})
  1. doScroll 函数:该函数是用来处理滚动事件的。它首先通过 scrollBox.value.scrollTop 获取滚动容器的滚动距离,并根据列表项的高度 itemHeight.value 计算出当前滚动到的列表项的索引。然后与 startIndex 变量进行比较,如果计算出的索引与 startIndex 相同,则表示可见区域内的列表项没有变化,直接返回,否则更新 startIndex 变量为新的索引值。
  2. endIndex 计算属性:该计算属性用于计算可视区域内最后一个可见列表项的索引。它首先根据 startIndexitemNum 计算出当前可见区域内的最后一个列表项的索引,并添加了一些额外的列表项数量,以确保在滚动过程中有足够的列表项可以填充整个可见区域。如果计算出的索引超出了列表的范围,则将其调整为列表的最后一个索引。
  • 拿到渲染数据
js 复制代码
const curList = computed(() => {
    let index = 0
    if (startIndex.value <= itemNum.value) {
        index = 0
    }else{
        index = startIndex.value - itemNum.value
        }
     return allList.value.slice(index, endIndex.value+1)
})
  1. 首先,通过 computed 属性创建了 curList 计算属性。
  2. 在计算 curList 的过程中,首先判断 startIndex 是否小于等于 itemNum,如果是,则将 index 设置为 0,表示从列表的第一项开始渲染。
  3. 如果 startIndex 大于 itemNum,则将 index 设置为 startIndex - itemNum,这样可以确保在滚动时,始终在当前可见区域的前面预留一些列表项,以提供更流畅的滚动体验。
  4. 然后,通过 allList.value.slice(index, endIndex.value+1)allList 数组进行切片操作,获取从 indexendIndex(包括 endIndex)范围内的列表项,作为当前需要渲染的列表项数组。

全代码:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Document</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .v-scroll {
            width: 300px;
            height: 400px;
            border: 1px solid #000;
            margin: 100px 0 0 100px;
            overflow-y: scroll;
        }

        li {
            list-style: none;
            padding-left: 20px;
            height: 40px;
            line-height: 40px;
            border-bottom: 1px solid #d5cece;
            box-sizing: border-box;
        }
    </style>
</head>

<body>
    <div id="app">
        <div class="v-scroll" ref="scrollBox" @scroll="doScroll">
            <ul>
                <li v-for="(item,index) in curList" :key="index">{{index+1}} ---- {{item}}</li>
            </ul>
        </div>
    </div>
    <script>
        const { createApp, ref, onMounted, computed } = Vue

        createApp({
            setup() {
                const allList = ref([])//所有的请求
                const getAllList = (count) => {//接口请求
                    for (let i = 0; i < count; i++) {
                        allList.value.push(`我是列表${i + 1}项`)
                    }
                }
                getAllList(300)

                //----------------------------------------------------------------------
                const boxHeight = ref(0)//可视区域的高度
                const itemHeight = ref(40)//列表项的高度
                const scrollBox = ref(null)//可视区域容器
                onMounted(() => {
                    boxHeight.value = scrollBox.value.clientHeight
                })
                const itemNum = computed(() => {
                    return ~~(boxHeight.value / itemHeight.value) + 2
                })
                const startIndex = ref(0)//可视区域第一项的下标

                // 页面滚动
                const doScroll = () => {
                    const index = ~~(scrollBox.value.scrollTop / itemHeight.value)
                    if (index === startIndex.value) return
                    startIndex.value = index
                }
                const endIndex = computed(() => {//可视区域最后一项的索引
                    let index = startIndex.value + itemNum.value * 2
                    if (!allList.value[index]) {
                        index = allList.value.length - 1
                    }
                    return index
                })

                // 拿到真正要被渲染的数据
                const curList = computed(() => {
                    let index = 0
                    if (startIndex.value <= itemNum.value) {
                        index = 0
                    }else{
                        index = startIndex.value - itemNum.value
                    }
                   return allList.value.slice(index, endIndex.value+1)
                })
                return {
                    curList,
                    allList,
                    boxHeight,
                    itemHeight,
                    scrollBox,
                    itemNum,
                    startIndex,
                    doScroll,
                    endIndex
                }
            }
        }).mount('#app')
    </script>
</body>

</html>

结尾

擦干眼泪,后端还是咱的好伙计...

相关推荐
小白学习日记几秒前
【复习】HTML常用标签<table>
前端·html
john_hjy4 分钟前
11. 异步编程
运维·服务器·javascript
风清扬_jd27 分钟前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java43 分钟前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele1 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
It'sMyGo1 小时前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
懒羊羊大王呀1 小时前
CSS——属性值计算
前端·css
睡觉然后上课1 小时前
c基础面试题
c语言·开发语言·c++·面试
xgq1 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
李是啥也不会1 小时前
数组的概念
javascript