前端渲染大量数据思路【虚拟列表】【异步机制】

当浏览器遇到性能瓶颈导致页面卡顿时,你会怎么处理?如何查找问题的原因?

浏览器本身自带性能检测工具,通常我们分析由脚本导致的页面卡顿会选择 性能(performance) 选项卡,在其中我们可以找到导致页面卡顿的函数,另外通过观察我们可以看到脚本的执行时间占比以及页面卡顿的程度。

此外还有 内存 选项卡,可以截取当前页面的内存快照,由于我们的页面通常不像服务器那样一直运行,所以平常我们也不会过于关注页面的内存使用问题。

1. 渲染大量数据

由于要渲染大量数据,我们刚开始学习前端时,如果经验不足就会简单地将所有数据全部一次性地渲染到页面上,这样势必会造成页面的卡顿。所以这种方法我这里也不会去介绍。

下面直接进入正轨,首先提出一个问题引发思考:如果让你去实现,你会怎么实现?

1.1. 采用异步

我首先是想到如果要渲染大量数据,我们可以将这一个巨大的任务拆分成一个个的小任务来执行,将这一个个的小任务加入到任务队列当中采用异步的方式来慢慢地执行。

不过这个会有个缺陷,由于这么多数据是按照顺序来依次执行的,所以当用户想查看比较靠后的数据时,用户会发现数据一直在加载中、一直在渲染,然后用户等啊等,最后点击关闭标签页。

下面是一个简单的使用异步实现的逻辑(只包含部分代码):

js 复制代码
const ul = document.querySelector('ul');

let total = 100_000_000;
let count = 0;

function loop() {
    const fragment = document.createDocumentFragment();
    
    for (let i = 0; i < 20 && count < total; i++, count++) {
        const li = document.createElement("li");
        li.textContent = count;
        fragment.appendChild(li);
    }
    
    ul.appendChild(fragment);
    window.requestAnimationFrame(loop);
}

loop();

观察上面的代码,其中使用到了 requestAnimationFrame 函数,它接收一个函数作为参数,这个函数会在浏览器每一帧渲染结束之后执行。

为什么不使用 setTimeout 定时器,因为 setTimeout 函数无法控制函数的执行时机,我们只知道函数会被加入到任务队列,但并不知道函数会在何时执行,而 requestAnimationFrame 函数则固定在每一帧渲染之后执行,这样就不会在视觉上让用户感觉页面卡顿。

当然如果 requestAnimationFrame 的函数执行时间过长,会推迟下一帧的渲染,所以尽量不要将耗时任务放在其中。

1.2. 虚拟列表

虚拟列表采用的思想类似于懒加载,都是只加载用户看得见的数据,懒加载在计算机上随处可见,比如单例模式中的饿汉式、图片的懒加载等,在我们常用的 QQ 中,像消息列表,联系人列表他也采用了类似于虚拟列表的形式进行渲染,以减少内存的使用量。

因为这里我们要处理大量数据的渲染,如果要求所有的数据必须全部渲染在可视区当中,那么懒加载就派不上用场了。

虚拟列表主要由可视区的数据构成,其他的位置都是空白。我们可以通过 padding 或者是 top/left 来制造空白。如下图所示(图片来自这篇文章 面试官:如何一次性渲染十万条数据 - 掘金 (juejin.cn)),可视区展示我们想看见的数据,缓存区就是我说的空白。

实现的代码如下:

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>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        .scroll-box {
            height: 100vh;
            overflow-y: scroll;
        }
    </style>
</head>
<body>
    <div class="scroll-box">
        <ul></ul>
    </div>
    
    <script src="./test.js"></script>
</body>
</html>

对应的 test.js:

js 复制代码
const ul = document.querySelector('ul');
const scrollBox = document.querySelector('.scroll-box');

let total = 100_000_000;
let count = 0;
let liHeight = 20;
// 要展示的数据量
let showCount = scrollBox.clientHeight / liHeight >> 0;

let totalHeight = total * liHeight;

function generateLi() {
    // 计算出来的处于可视区顶部的数据索引
    let index = (scrollBox.scrollTop / liHeight) >> 0;
    const fragment = document.createDocumentFragment();
    // 制造空白
    ul.style.paddingTop = `${index * liHeight}px`;
    ul.style.paddingBottom = `${(totalHeight - (index + 20) * liHeight)}px`;

    // 生成可视区数据
    for (let i = 0; i < showCount; i++) {
      const li = document.createElement("li");
      li.textContent = `${index + i}`;
      fragment.appendChild(li);
    }

    // 重新设置元素,这里元素并没有考虑复用
    ul.innerHTML = "";
    ul.appendChild(fragment);
}

generateLi();

scrollBox.addEventListener("scroll", generateLi);

多嘴一句,当前元素可以滚动才有 scrollTop 值,否则一直为 0。比如当前元素固定了高度,但是它的子元素的高度超出了它的高度,就会导致溢出,这是我们可以设置 overflow-y: auto,当前元素就会有滚动条。

而 scrollTop 就是当前元素的顶部与它的子元素的顶部的距离,没有负值。

2. 参考

参考文献:

相关推荐
abc80021170341 小时前
前端Bug 修复手册
前端·bug
Best_Liu~1 小时前
el-table实现固定列,及解决固定列导致部分滚动条无法拖动的问题
前端·javascript·vue.js
_斯洛伐克2 小时前
下降npm版本
前端·vue.js
苏十八3 小时前
前端进阶:Vue.js
前端·javascript·vue.js·前端框架·npm·node.js·ecmascript
st紫月4 小时前
用MySQL+node+vue做一个学生信息管理系统(四):制作增加、删除、修改的组件和对应的路由
前端·vue.js·mysql
乐容4 小时前
vue3使用pinia中的actions,需要调用接口的话
前端·javascript·vue.js
似水明俊德5 小时前
ASP.NET Core Blazor 5:Blazor表单和数据
java·前端·javascript·html·asp.net
至天6 小时前
UniApp 中 Web/H5 正确使用反向代理解决跨域问题
前端·uni-app·vue3·vue2·vite·反向代理
与墨学长6 小时前
Rust破界:前端革新与Vite重构的深度透视(中)
开发语言·前端·rust·前端框架·wasm
H-J-L6 小时前
Web基础与HTTP协议
前端·http·php