虚拟滚动之原理及其封装

2023-11-06

本文分享自微信公众号 - 一Li小麦(gh_c88159ec1309),作者:一li小麦
目前GitHub上只放出demo的版本,将在 https://github.com/dangjingtao/vList.git 持续更新。

前端的业务开发中会遇到一些不分页且数据条数超过1000加载的列表(长列表),不分页的需求在一般前端程序员看来是不可思议的。正常的思考逻辑是,当数据量20w+时,后端报文可去到30+M,查询时间可能去到几十秒。但是前端如果尝试渲染这些数据,花费的时间必定是以分钟计算。通常是3分钟以上。相比之下,由前端优化这个问题更为迫切,责任更为突出(锅更大)。

对于作为业务程序员的笔者来说,长列表性能优化是工作中反复需要面临的问题之一。

  1. 否定
    上来先说结论,完整渲染的长列表是不可能满足业务上的需求的。

先做一个小测试:

const createElements = count => {    const start = new Date();    for (let i = 0; i < count; i++) {        const element = document.createElement('div');        element.appendChild(document.createTextNode('' + i));        document.body.appendChild(element);    }
    setTimeout(function () {        console.log('渲染耗时:',new Date() - start);    }, 0);};
document.querySelector('#btn').addEventListener('click', e => createElements(10000))

这段代码意思是:点击按钮,依次生成10000个带文字的节点

为什么要setTimeout?

你可能注意到了上面的测试代码中的时间计算过程中并没有直接在调用完 API 之后直接计算时间,而是使用了一个 setTimeout,按照正常逻辑似乎、完全、可以这么写:

const start = Date.now();// bala bala...console.log(Date.now() - start);

你若直接看console的测试结果,只有40多毫秒,看来挺快嘛。

实际上对于 DOM 的性能测试这么做是不科学的,因为 DOM 的操作会引起浏览器的回流(reflow)。如果浏览器的 reflow 执行的时间远大于代码执行时间,会造成你时间计算完成之后,浏览器仍然在卡顿。统计的时间应该是从 开始创建元素 到 可以进行响应 的时间,所以一个合理的做法是把计算放 setTimeout(function() {}, 0) 中。setTimeout() 中的 callback 会被推迟到浏览器主线程 reflow 结束后才执行,这个时间和 Chrome Devtools 下的 Profile 的时间基本吻合,可以信任这个时间作为渲染时间。

如图,在一个空白的html上生成10000个dom,需要耗费约870ms。
在这里插入图片描述

打印结果为856ms,基本与测试相符。

好了。根据测试结果计算。在笔者的电脑上,创建 10000 个带文本节点就需要 800ms+,笔者实际业务中的列表每个条数据都需要 20个左右的节点。那么,实际单纯渲染10000条数据,理论上最快得17s。

  1. 斟酌
    非完整渲染的长列表一般有两种方式:

•懒渲染:这个就是常见的无限滚动的,每次只渲染一部分(比如 10 条),等剩余部分滚动到可见区域,就再渲染另一部分。•可视区域渲染:只渲染可见部分,不可见部分不渲染。

先说懒渲染,经常跟移动端打交道的程序员对于懒加载应该并不陌生。二者其实可以认为是一个东西。但这里懒渲染更加侧重于从列表优化的角度说明问题。这是一种前后端共同优化的方式,后端一次加载比较少的数据,就不用查询等几十秒,前端首次渲染更少的数据速度当然会更快。看起来很好。

遗憾的是有三点重大缺陷:

•边滚边加载的模式,会导致页面越发卡顿。(实际上是把锅丢到了后面)•无法实现动态反映选中状态•滚动条无法正确反映操作者当前浏览的信息在全部列表中的位置。而且我百万级数据加载,你一次给我加载十几条,滚到底太慢了,是想愚弄用户吗!

三条理由都很有道理。所以懒渲染被摈弃了。

于是方案来到了可视区域渲染。

可视区渲染有个更出名的名字,叫做虚拟滚动——指的是只渲染可视区域的列表项,非可见区域的完全不渲染,在滚动条滚动时动态更新列表项。

[注]:实际上考虑页面流畅性,不可能完全不渲染视区之外的内容,建议是预留2-3屏。

有两个重要的基本概念:

•可滚动区域:假设有 1000 条数据,每个列表项的高度是 30,那么可滚动的区域的高度就是 1000 * 30。当用户改变列表的滚动条的当前滚动值的时候,会造成可见区域的内容的变更。•可见区域:比如列表的高度是 300,右侧有纵向滚动条可以滚动,那么视觉可见的区域就是可见区域。

相比较于懒渲染,虚拟滚动要求一次性全部拿到数据,但是滚动条能够完全正确地反映当前页面在全部数据的位置。滚动无非是对几十个dom进行操作,可以达到极高的后续渲染性能。而且一旦实现,可以把页面慢的锅完全丢给后端了。

  1. 理想
    最低目标当然是满足需求。react方面的封装有

https://bvaughn.github.io/react-virtualized/#/components/List

参考它的实现,是时候考虑封装了。就定名VList吧。封装之前,应根据易用性设计options api。
在这里插入图片描述

预期的使用方法是:

const vlist = new Vlist({    itemHeight: 65,    container: document.querySelector("#list"),    containerContent:document.querySelector('.ul'),    maxHeight: document.documentElement.clientHeight,    isDebounce:true,    initData: data,    render: function (itemData, index) {        return `<div class="item">                    <div data-index="${index}" class="left">                    </div>                    <div class="right">                        <div class="title">${itemData.id}</div>                        <div class="price">${itemData.name}</div>                        <div class="price">${itemData.address}</div>                    </div>                </div>`    },    itemEventHandlers:[        {            eventTargetClass:'left',            eventType:'click',            handler:function(e){                console.log(e.target)            }        }    ]});
  1. 实现
    实现虚拟滚动就是处理滚动条滚动后的可见区域的变更,其中具体步骤如下:

1.计算当前可见区域起始数据的 startIndex2.计算当前可见区域结束数据的 endIndex3.计算当前可见区域的数据,并渲染到页面中4.计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上

vList对象基本过程:

初始化(mixin) -> 添加数据(addData) -> 绑定滚动事件(scrollEvent)

该逻辑可直接写在构造函数里:

constructor(opts) {    // mixin    this.mixin(opts);
    const {        initData,        isDebounce    } = opts;
    //初始化数据    if (initData) {        this.addData(initData);    }
    this.scrollEventBind = this.scrollEvent.bind(this);
    // 绑定滚动事件    if (isDebounce) {        this.container.addEventListener("scroll", (e) => {            this.debounce(this.scrollEventBind, 40);        });    } else {        this.container.addEventListener("scroll", this.scrollEventBind, false);    }
}

4.1 初始化
那么在构造函数获取得配置后,马上通过mixin初始化自身的配置,方便其它函数共享:

/** * 配置mixin * @param {*} opts  */mixin(opts) {    let {        itemHeight,        container,        containerContent,        maxHeight,        render,        initData,        itemEventHandlers,        isDebounce    } = opts;
    // 计算最大高度,否则取设备高度     maxHeight = maxHeight ? maxHeight : document.documentElement.clientHeight;
    const _this = {        itemHeight, // 每项高度        container,  // 滚动容器        containerContent, // 滚动内容        maxHeight, // 出现滚动条的高度        showItemCount: Math.ceil(maxHeight / itemHeight) + 1, // 视图区域显示item的个数        items: [], // 可见列表项        startIndex: 0, // 第一个item索引        render, // 渲染每一项的函数        data: [], // 列表数据        itemEventHandlers, //事件处理        isDebounce // 性能优化点:防抖    }
    Object.keys(_this).forEach(key => {        this[key] = _this[key];    })}

4.2 添加数据
添加数据的主要内容,做两件事:

1.是判断数据能否被撑起来,并做一些样式设置。2.计算需要展示的内容,展示。

/** * 添加数据 * @param {*} data 所需添加的数据,在原基础上加 */addData(data) {    let isInit = this.data.length == 0;    this.data = this.data.concat(data);    const realHeight = parseInt(this.data.length * this.itemHeight);    if (realHeight > this.maxHeight) {        // 出现滚动条        this.showItemCount = Math.ceil(this.maxHeight / this.itemHeight) * 3;//视图区域显示item的个数        this.container.style.height = this.maxHeight + 'px';    } else {        // 支撑起内容高度,不触发滚动条        this.container.style.height = realHeight + 'px';        this.showItemCount = this.data.length + 1;//视图区域显示item的个数    }
    this.containerContent.style.height = realHeight + 'px';
    if (isInit) {        this.initList();    }}

对应initList 方法异常简单,根据展示区数据量做循环,逐个生成数据。存在this.items中其中,生成数据时,可在此绑定eventHandlers的事件。

/*** 初始化列表* 只根据startIndex渲染可视区范围的数据*/initList() {    const count = this.data.length < this.showItemCount ? this.data.length : this.showItemCount;    for (let i = 0; i < count; i++) {        const item = this.renderItem({            index: i        });        this.containerContent.appendChild(item.dom);        this.items.push(item);    }}
/** * 渲染单行容器样式 * @param {Object} dom内容   * 最好是以template模板的形式,并加上事件 */renderItem(item) {    const index = item.index;
    // 此处应该配置    const itemDom = item.dom ? item.dom : document.createElement("DIV");    const itemData = this.data[index];    // 填充    itemDom.innerHTML = this.render(itemData, index);
    // 绑定事件,目前只支持item内class选择器    this.itemEventHandlers.forEach((x, i) => {        const targets = itemDom.querySelectorAll(`.${x.eventTargetClass}`);        for (let j = 0; j < targets.length; j++) {            targets[j].addEventListener(x.eventType, e => x.handler(e));        }    });
    // 设置高度    itemDom.style.position = "absolute";    itemDom.style.top = (index * this.itemHeight) + "px";    itemDom.style.height = this.itemHeight + "px";    itemDom.style.width = "100%";    itemDom.style.overflow = "hidden";    item.dom = itemDom;    item.dom.setAttribute("index", index);    item.top = index * this.itemHeight;    return item;}

更新视区的方式很多,比如,通过transfer2D,或是改变上面的padding,而vList采用定位的方法来做。

4.3 绑定滚动
计算渲染边界,确定是否渲染。

/*** 滚动事件*/scrollEvent() {    const containerScrollTop = this.container.scrollTop;
    const { itemHeight, startIndex, maxHeight } = this;
    // 滚动触发计算    const fakeStartIndex = Math.floor(containerScrollTop / itemHeight) - Math.ceil(maxHeight / itemHeight) - 1;    let startIndexNew = fakeStartIndex >= 0 ? fakeStartIndex : 0;    const maxStartIndex = this.data.length - this.showItemCount + 1;    startIndexNew = startIndexNew > maxStartIndex ? maxStartIndex : startIndexNew;
    if (containerScrollTop < 0) return; // ios兼容    if (startIndexNew === startIndex) return; // android兼容
    const scrollOver = startIndexNew + this.showItemCount - 1 >= this.data.length;    const renderOver = startIndexNew - startIndex === 1;
    // 如果到底没有渲染完就再渲染一次    if (scrollOver && renderOver === false) {        startIndexNew--;    }
    this.diffRender(startIndex, startIndexNew);    this.startIndex = startIndexNew;}

而以下代码侧重渲染

/** * startIndex比较渲染 * @param {*} startIndex  * @param {*} startIndexNew  */diffRender(startIndex, startIndexNew) {    const showItemCount = this.showItemCount;
    const items = this.items;    const moveCount = Math.abs(startIndex - startIndexNew);
    if (moveCount >= showItemCount) {        // 全部渲染        items.forEach((item, idx) => {            item.index = startIndexNew + idx;            this.renderItem(item);        })    } else {        // 部分渲染        if (startIndex - startIndexNew > 0) {            // 往上滚            for (let i = 1; i <= moveCount; i++) {                let item = items[showItemCount - i];                item.index = item.index - showItemCount;                this.renderItem(item);            }            this.items = items.splice(showItemCount - moveCount, moveCount).concat(items);        } else {            for (let i = 0; i < moveCount; i++) {                const item = items[i];                item.index = item.index + showItemCount;                this.renderItem(item);            }            this.items = items.concat(items.splice(0, moveCount));        }    }}

那么主要功能就实现了。

  1. 小结
    在虚拟dom成为主流的今日,如果不亲自去调查了解,你发现不了这么一个事实:习惯于从视图层取数据的前端原来还大有人在。

视图层依赖dom,而dom成为一种负担不得控制的时候,你会发现很多人技穷了。这时应该大胆地把数据处理的某些逻辑放到js内存来做。

认识到这点,不是一种优越感。这本来就是各有优劣的两个方面,相反当你要亲自去纠正,会深感自身才学不足,考虑的东西需要更多。

当前代码优化点仍然存在。往后将以续作形式更新。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

虚拟滚动之原理及其封装 的相关文章

  • 将 OoXml 插入单词抛出错误:未知

    我一直在尝试通过office js将OOXML插入到word文档的正文内容中insertOoXML 方法 我什至尝试过最简单的实现 认为我在尝试替换 XML 本身中的 fieldCodes 时做了一些不正确的事情 所有结果都是这样Error
  • 如何在同一页面上使用AJAX处理多个表单

    我有一个表单 当我单击 提交 时 它就被提交了 然后该表单隐藏 操作页面的结果显示在 div 中 classname dig 它工作正常 但是当我添加另一个表单时 它停止正常工作并且所有表单同时提交 我如何更改我的代码 done click
  • Android 设备上的 PhoneGap 蓝牙插件

    我一直在尝试让 PhoneGap 工作的蓝牙插件 但我似乎不知道哪里出了问题 首先 我的测试设备是 Galaxy S3 GT 19305T 应用程序是使用PhoneGap CLI http docs phonegap com en 3 0
  • 尝试将布尔 C# 变量传递给 javascript 变量并将其设置为 true

    在我的 aspx 页面中 我将布尔变量 C 传递给需要布尔类型的 javascript 函数 但遇到了问题 但是 C 变量返回 True 而 javascript 不喜欢大写 myjavascript 如果我将 c 变量转换为字符串 那么我
  • Web 串行 API - 未捕获(承诺中)DOMException:无法打开串行端口/所需成员 baudRate 未定义

    下面的代码可以在我的 Xubuntu 机器上运行 但现在我在 Kubuntu 上 它不再工作了 它不会打开端口 Arduino IDE 工作正常 可以向开发板写入代码 并且我可以在 Chrome 中选择设备 Arduino Uno 但当我尝
  • 在 Vue.js 中从父组件执行子方法

    目前 我有一个 Vue js 组件 其中包含其他组件的列表 我知道使用 vue 的常见方式是将数据传递给孩子 并从孩子向父母发出事件 但是 在这种情况下 我想在子组件中的按钮出现时执行子组件中的方法 parent被点击 哪种方法最好 一种建
  • 如何重定向到 instagram://user?username={username}

    我的 html 页面上有这个链接 可以在特定用户上打开 Instagram 应用程序 a href Link to Instagram Profile a 我一直在寻找自动运行 url instagram user username USE
  • 如何重置使用 JavaScript 更改的 CSS 属性?

    我的导航按钮的宽度从 100px 增加到 150px 当鼠标悬停在 nav li hover width 150px 但是使用 javascript 我已经做到了 无论选择哪个选项 宽度都将继续为 150px 当选择每个选项时 它会使其他选
  • Node.js:如何在检索数据(块)时关闭响应/请求

    我正在用 node js 构建一个应用程序 它加载多个页面并分析内容 因为 node js 发送块 所以我可以分析这些块 如果一个块包含例如索引 nofollow 我想关闭该连接并继续其余部分 var host example com to
  • 为什么是 javascript:history.go(-1);无法在移动设备上工作?

    首先 一些背景 我有一个向用户呈现搜索页面 html 表单 的应用程序 填写标准并单击 搜索 按钮后 结果将显示在标准部分下方 在结果列表中 您可以通过单击将您带到新页面的链接来查看单个结果的详细信息 在详细信息页面中 我添加了一个 返回结
  • 将div设置为隐藏,延时后可见

    我试图在 X 时间后 也许甚至在随机时间之后 但现在我们只做固定时间 在黑色背景上出现一个黄色方块 function initialSetup if document getElementById yellow null document
  • Firefox 书签探索未超过 Javascript 的第一级

    我已经编写了一些代码来探索我的 Firefox 书签 但我只获得了第一级书签 即我没有获得文件夹中的链接 e g 搜索引擎 雅虎网站 谷歌网站 在此示例中 我只能访问 Search engines 和 google com 不能访问 yah
  • 提交表单并重定向页面

    我在 SO 上看到了很多与此相关的其他问题 但没有一个对我有用 我正在尝试提交POST表单 然后将用户重定向到另一个页面 但我无法同时实现这两种情况 我可以获取重定向或帖子 但不能同时获取两者 这是我现在所拥有的
  • Grails 在 javascript 内的 GSP 站点中使用 grails var

    我有一个在 GSP 文件中的 javascript 代码中使用 grails 变量值的问题 例如 我有一个会话值session getAttribute selectedValue 我想在 javascript 代码部分使用这个值 我现在的
  • 如何使用tampermonkey模拟react应用程序中的点击?

    我正在尝试使用 Tampermonkey 脚本模拟对 React 元素的点击 不幸的是 由于 React 有自己的影子 DOM 所以天真的方法使用document querySelector 不工作 我遇到了一些需要修改 React 组件本
  • FireFox 中的自动滚动

    我的应用程序是实时聊天 我有一个 Div 来包装消息 每条消息都是一个 div 所以 在几条消息之后 我的 DOM 看起来像这样 div div Message number two div div div div
  • 如何在类似控制台的环境中运行 JavaScript?

    我正在尝试遵循这里的示例 http eloquentjavascript net chapter2 html http eloquentjavascript net chapter2 html and print blah 在浏览器中运行时
  • JQuery 图像上传不适用于未来的活动

    我希望我的用户可以通过帖子上传图像 因此 每个回复表单都有一个上传表单 用户可以通过单击上传按钮上传图像 然后单击提交来提交帖子 现在我的上传表单可以上传第一个回复的图像 但第二个回复的上传不起作用 我的提交过程 Ajax 在 php 提交
  • 如何仅在最后一个
  • 处给出透明六边形角度?
  • 我必须制作这样的菜单 替代文本 http shup com Shup 330421 1104422739 My Desktop png http shup com Shup 330421 1104422739 My Desktop png
  • 如何获取浏览器视口中当前显示的内容

    如何获取当前正在显示长文档的哪一部分的指示 例如 如果我的 html 包含 1 000 行 1 2 3 9991000 并且用户位于显示第 500 行的中间附近 那么我想得到 500 n501 n502 或类似的内容 显然 大多数场景都会比

随机推荐

  • 怎样才能从Java初级程序员成长为一名合格的架构师?

    怎样学习才能从一名Java初级程序员成长为一名合格的架构师 或者说一名合格的架构师应该有怎样的技术知识体系 这不仅仅是一个刚刚踏入职场的初级程序员也是工作三五年之后开始迷茫的老程序员经常会问到的问题 下面 我来详细回答这些问题 希望这篇文章
  • 基于Python和mysql开发的智慧校园答题考试系统(源码+数据库+程序配置说明书+程序使用说明书)

    一 项目简介 本项目是一套基于Python和mysql开发的智慧校园答题考试系统 主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Python学习者 包含 项目源码 项目文档 数据库脚本等 该项目附带全部源码可作为毕设使用 项目
  • 搭建自己的searx搜索引擎

    Searx是一个隐私尊重的免费元搜索引擎 它具有以下功能特性 自我托管 默认不使用 Cookie 不跟踪 不分析用户 不收集用户数据 不与第三方共享用户数据 提供安全的加密连接 HTTPS SSL 等 如果您更在意隐私而不是效率 这会是一个
  • PPTP L2TP IPsec

    点对点隧道协议 PPTP PPTP是一个第2层的协议 将PPP数据桢封装在IP数据报内通过IP网络 如Internet传送 PPTP还可用于专用局域网络之间的连接 RFC草案 点对点隧道协议 对PPTP协议进行了说明和介绍 该草案由PPTP
  • springboot+cache缓存下(原理篇)

    原理 1 自动配置类 CacheAutoConfiguration 在idea中使用快捷键ctrl shift n 查找出CacheAutoConfiguration类 看到import中有导入CacheConfigurationImpor
  • S7-1200 PLC的数据类型

    S7 1200 PLC的数据类型 除了基本数据类型之外 还支持一些复杂的数据类型 包括结构数据类型Struct PLC数据类型UDT 数组Array 系统数据类型SDT 硬件数据类型DB ANY 参数数据类型Variant String和C
  • C++11静态断言static_assert

    C 11静态断言static assert 一 运行时断言 二 静态断言的需求 三 静态断言 四 单参数版本的静态断言 一 运行时断言 断言 assertion 是一种编程中常用的手段 在通常情况下 断言就是将一个返回值总是需要为真的判别式
  • vue3 组合式api中 ref 和$parent 的使用

    ref 的使用 vue3中 在 组件中添加一个 component ref xxx 就可以在父组件中得到 子组件的 dom 对象 以及 虚拟的 dom 对象 有了虚拟 dom 我们就可以在父组件中控制子组件的显示了 ref 的使用方法 vu
  • vue中的富文本框的使用(vue-quill-editor)

    一 安装 vue quill editor npm install vue quill editor 二 在main js中引入 import VueQuillEditor from vue quill editor import quil
  • Android Widget:DrawerLayout配合Toolbar的使用及常见问题

    前言 最近在仿写网易云音乐安卓端界面 记录下所遇到的问题及解决方案 DrawerLayout的基础使用 DrawerLayout经常配合NavigationView及Toolbar使用 编写侧滑栏 1 首先添加支持 implementati
  • MapReduce处理csv

    MapReduce处理csv csv是由逗号 来分割的文件 在编写Mapper类的时候需要以 分割成一个个的数据 查看一下csv数据 以上是为了测试做的数据 要处理的结果就是经过mapreduce再原封不动的出来 因为是测试 所以内容不做任
  • 搭建虚拟专网络服务器

    搭建虚拟专网络服务器 准备环境 一台windows server2008R2 服务器 没有安装ad域服务的 IP地址为 192 168 4 92 一台物理机windows 10 家庭版 1 首先安装 网络策略和访问服务 第二步 勾选上远程访
  • 为什么普通人赚钱这么难?普通人的赚钱之路在哪里

    前几天听一个老家的朋友说 辛辛苦苦一整年 发现并没有赚到什么钱 付出与收入不成正比 首先要知道勤奋 努力并不一定就能够赚到钱 像送外卖的 工地上班的 厂里上班的哪个不勤奋 但他们即使非常努力工作一个月 扣除基本开支 也存不了多少钱 那普通人
  • GNS3-GREvpn

    GREvpn 发一下这些日忙的东西 实验拓扑 以R2为界限的左半边运行ospf 各个链路已经ping通 用R7模拟PC1 PC1和R2的f0 0来回链路没问题 右半边同左半边做相应的配置也成功ping通 在R2和R3之间建立vpn隧道 网段
  • 利用Matlab绘制图像中的某一行或者某一列的灰度曲线

    filename C Users Administrator Desktop 透视变换 包含裂缝的整个图片 123 jpg imgData imread filename imshow imgData 该函数可以用来显示已经读入的数据 A
  • 解决AD中pcb原件移动提示绿色报错问题

    有可能以下三个原因之一所导致的 1 不符合DRC规则 比如原件之间距离过近 就是报错 2 右下角ROOM没有删除 右键清楚就可以啦 3 如果以上两个确证都是正常的 还是报错的话 终极解决方案 step1 在pcb界面下 点击design n
  • 主线3.1DeepFM模型论文阅读:DeepFM: A Factorization-Machine based Neural Network for CTR Prediction

    文章目录 一 摘要 二 模型演变和各模型间的对比 1 CTR的任务要求 2 DeepFM模型的引入 3 各模型间的对比 4 DeepFM优势 三 DeepFM模型介绍 1 FM部分 2 Deep部分 一 摘要 对于一个基于CTR预估的推荐系
  • selenium小项目实践

    1 斗鱼爬虫 爬取斗鱼直播平台的所有房间信息 游戏直播 全部游戏直播 斗鱼直播 1 1 思路分析 数据的位置 每个房间的数据都在id live list contentbox的ul中 实现翻页和控制程序结束 selenium中通过点击就可以
  • 基于麻雀搜索算法(SSA)优化长短期记忆神经网络参数SSA-LSTM冷、热、电负荷预测(Python代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 2 1 冷负荷 2 2 热负荷 2 3 电负荷 3 参考文献 4 Python代码 数据 1
  • 虚拟滚动之原理及其封装

    本文分享自微信公众号 一Li小麦 gh c88159ec1309 作者 一li小麦 目前GitHub上只放出demo的版本 将在 https github com dangjingtao vList git 持续更新 前端的业务开发中会遇到