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

浅聊一下

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

模拟十万条数据

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>

结尾

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

相关推荐
DogDaoDao3 小时前
leetcode 面试经典 150 题:有效的括号
c++·算法·leetcode·面试··stack·有效的括号
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
CodeClimb5 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
hunter2062066 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb6 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角6 小时前
CSS 颜色
前端·css
九酒6 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter