SolidJs尝鲜与Web Component实践造虚拟滚动的轮子

简介: 「造轮子」虚拟滚动 + soild + Web Component

「造轮子」虚拟滚动 + soild + Web Component


前端时间参与一个项目,一个下拉选择列表中可能出现最多 8000 条数据。上一位同事使用BetterScroll做滚动,但由于数据量庞大,要渲染的 dom 节点太多,导致加载时间很长甚至手机卡顿。


当我接手时,需要解决卡顿的问题。对于大量数据的加载和渲染,无疑虚拟滚动是最好的选择。


react-virtualized


我去 react 社区寻找已有的虚拟滚动实现react-virtualized,但其 2.27MB 的大小让我放弃了,我们的列表需求仅需要一个简单虚拟滚动即可,最多加上一个滚动加载,杀鸡何须用牛刀。于是便产生了自己造轮子的想法。


Web Component


我的们项目是基于 react 开发,但还有一个 vue 的复刻版。因此使用原生 js 可以实现两边兼容。封装为Web Component,可以更好的使用,且 react 和 vue 对此都有良好的支持。


在 React 中使用 Web Components


Vue 与 Web Components


solid


最近在社区逛了逛,发现有许多关于无 vdom 的响应式框架的讨论,比如sveltesolid。看完一部分我心中一惊,为什么大家知道,而我听都没听过。



从执行时和编译速度来看,这些框架确实非常快,对于饱受低速热重载折磨的我来说,简直是福音。



Vanilla 是不使用任何框架的纯粹的原生 JavaScript,通常作为一个性能比较的基准


话不多说,上手干。按照 solid 官网教程建立项目,构建工具竟然 vite,我喜欢。


设计思路


相比于普通的滚动,虚拟滚动仅渲染要显示的 dom。



我们可以让可视的 dom 随着滚动距离移动,根据滚动的距离计算当前滚动到哪一段数据,然后更新到可视的 dom 中。



实现过程


组件封装


按照上述设计思路,实现并封装虚拟列表组件VirtualList

import { createSignal } from'solid-js';
import { VirtualListProps } from'./interface';
importstylesfrom'./VirtualList.module.css';
constVirtualList= ({ rowRenderer }: VirtualListProps) => {
// item高度constITEM_HEIGHT=50;
// item数量constITEM_NUMBER=12000;
// view视图高度constVIEW_HEIGHT=400;
// 实际渲染的item数量constRENDER_NUMBER=10;
// 最大滚动距离constMAX_SCROLL_DISTANCE=ITEM_HEIGHT*ITEM_NUMBER-VIEW_HEIGHT;
// 可视区列表constviewArr=newArray(RENDER_NUMBER).fill(0).map((d, i) =>d+i);
// 可视区向上偏移的距离const [getOffset, setOffset] =createSignal(0);
// 可视区向上偏移的item数量const [getOffsetNum, setOffsetNum] =createSignal(0);
letref: any;
consthandleScroll= (ev: UIEvent) => {
const { scrollTop=0 } =ref?? {};
// 滚动item数量constoffsetNum=Math.floor(scrollTop/ITEM_HEIGHT);
// 滚动到底部if (MAX_SCROLL_DISTANCE<=scrollTop) {
setOffsetNum(ITEM_NUMBER-RENDER_NUMBER);
setOffset(MAX_SCROLL_DISTANCE);
return;
    }
setOffsetNum(offsetNum);
setOffset(scrollTop);
  };
return (
<divclass={styles['view-area']}
style={{ height: `${VIEW_HEIGHT}px` }}
ref={ref}
onScroll={handleScroll}
><divstyle={{ height: `${ITEM_NUMBER*ITEM_HEIGHT}px` }}><divclass={styles['virtual-inner']}
style={{ transform: `translateY(${getOffset()}px)` }}
>          {viewArr.map((d) => {
constindex=d+getOffsetNum();
returnrowRenderer({ index, domIndex: d });
          })}
</div></div></div>  );
};
exportdefaultVirtualList;

App.tsx

importtype { Component } from'solid-js';
importListItemfrom'example/components/ListItem';
importVirtualListfrom'@/VirtualList';
constApp: Component= () => {
return (
<VirtualListrowRenderer={({ index }) =><ListItemvalue={index} />} />  );
};
exportdefaultApp;

运行效果


滚动优化


可以看到数据滚动正常了,但是仅仅是数据内容在变化,而列表项看起来并没有动,这与真实的滚动差别还是挺大的。


原因在于滚动容器(virtual container)的偏移量(offset)与滚动高度(scrollTop)是相等的,那么滚动时,滚动容器与页面相对静止,因此看起来像没动也一样。


真实的滚动情况如下:



真实情况下offsetscrollTop并非时刻相等,当滚动到如上图所示的位置,边缘的元素只显示一部分时,offset应当是scrollTop减去最上面元素被遮挡高度remainHeight),如下图:



修改源代码

import { createSignal } from'solid-js';
import { VirtualListProps } from'./interface';
importstylesfrom'./VirtualList.module.css';
constVirtualList= ({ rowRenderer }: VirtualListProps) => {
// item高度constITEM_HEIGHT=50;
// item数量constITEM_NUMBER=12000;
// view视图高度constVIEW_HEIGHT=400;
// 实际渲染的item数量constRENDER_NUMBER=10;
// 最大滚动距离constMAX_SCROLL_DISTANCE=ITEM_HEIGHT*ITEM_NUMBER-VIEW_HEIGHT;
// 可视区列表constviewArr=newArray(RENDER_NUMBER).fill(0).map((d, i) =>d+i);
// 可视区向上偏移的距离const [getOffset, setOffset] =createSignal(0);
// 可视区向上偏移的item数量const [getOffsetNum, setOffsetNum] =createSignal(0);
letref: any;
consthandleScroll= (ev: UIEvent) => {
const { scrollTop=0 } =ref?? {};
// 滚动到底部if (MAX_SCROLL_DISTANCE<=scrollTop) {
setOffsetNum(ITEM_NUMBER-RENDER_NUMBER);
setOffset(MAX_SCROLL_DISTANCE);
return;
    }
// 滚动item数量constoffsetNum=Math.floor(scrollTop/ITEM_HEIGHT);
// 多余不足一个item高度的距离constremainHeight=scrollTop%ITEM_HEIGHT;
setOffsetNum(offsetNum);
setOffset(scrollTop-remainHeight);
  };
return (
<divclass={styles['view-area']}
style={{ height: `${VIEW_HEIGHT}px` }}
ref={ref}
onScroll={handleScroll}
><divstyle={{ height: `${ITEM_NUMBER*ITEM_HEIGHT}px` }}><divclass={styles['virtual-inner']}
style={{ transform: `translateY(${getOffset()}px)` }}
>          {viewArr.map((d) => {
constindex=d+getOffsetNum();
returnrowRenderer({ index, domIndex: d });
          })}
</div></div></div>  );
};
exportdefaultVirtualList;

看看修改后的效果:



这次的滚动看起来就比较真实了。


问题与优化


滚动结束的判定


onScroll获取的scrollTop并非是连续的,那么滚动到最后临近底部时,可能出现scrollTop突然超出计算出来的最大滚动距离MAX_SCROLL_DISTANCE,导致多出一个元素。解决这个问题的处理比较粗暴:

// 滚动到底部if (MAX_SCROLL_DISTANCE<=scrollTop) {
setOffsetNum(ITEM_NUMBER-RENDER_NUMBER);
setOffset(MAX_SCROLL_DISTANCE);
return;
}


scrollTop超过MAX_SCROLL_DISTANCE 时,直接设置滚动偏移量offset和滚动元素数目offsetNum为预期最终结果,强制结束滚动。


这样处理在逻辑上没有问题,但因滚动并非自然结束,那么在最末尾的一段数据也会出现非自然的渲染,如下图,体现出这个细节,我将滚动条目数量ITEM_NUMBER改为1200, 然后缓慢的一次一次向下滚动,仔细看最后一次滚动,1190明显渲染了2次。



元素高度固定


按照这个设计思路做虚拟滚动时,滚动元素的高度必须是已知且固定的,不适用于列表元素高度自适应的情况,而这在移动端的列表很常见。后续有时间,我会思考如何处理自适应元素高度的虚拟滚动。


编译打包


项目中使用vite打包,通过solid可把代码转换为纯js:



将这段js代码再封装,改为web Component组件:

classVirtualextendsHTMLElement {
constructor() {
super();
consttemplate=VirtualList({
rowRenderer: ({ index }) => {
consttemplateItem=document.getElementById('virtual-item');
constcontent=templateItem.content.cloneNode(true);
constdom=content.querySelector('.item');
dom.innerHTML=index;
returndom;
            },
        });
this.appendChild(template);
    }
}
window.customElements.define('virtual-list', Virtual);


这样就可以在html中直接使用virtual-list



参考资料


相关文章
|
5月前
|
关系型数据库 MySQL 调度
DataX教程(05)- DataX Web项目实践
DataX教程(05)- DataX Web项目实践
654 0
|
2天前
|
开发框架 前端开发 数据库
Python从入门到精通:3.3.2 深入学习Python库和框架:Web开发框架的探索与实践
Python从入门到精通:3.3.2 深入学习Python库和框架:Web开发框架的探索与实践
|
1月前
|
机器学习/深度学习 前端开发 算法
利用机器学习优化Web前端性能的探索与实践
本文将介绍如何利用机器学习技术来优化Web前端性能,探讨机器学习在前端开发中的应用,以及通过实际案例展示机器学习算法对前端性能优化的效果。通过结合前端技术和机器学习,提升Web应用的用户体验和性能表现。
|
1月前
|
监控 前端开发 JavaScript
构建高性能Web应用:前端性能优化的关键策略与实践
本文将深入探讨前端性能优化的关键策略与实践,从资源加载、渲染优化、代码压缩等多个方面提供实用的优化建议。通过对前端性能优化的深入剖析,帮助开发者全面提升Web应用的用户体验和性能表现。
|
1月前
|
弹性计算 算法 应用服务中间件
倚天使用|Nginx性能高27%,性价比1.5倍,基于阿里云倚天ECS的Web server实践
倚天710构建的ECS产品,基于云原生独立物理核、大cache,结合CIPU新架构,倚天ECS在Nginx场景下,具备强大的性能优势。相对典型x86,Http长连接场景性能收益27%,开启gzip压缩时性能收益达到74%。 同时阿里云G8y实例售价比G7实例低23%,是Web Server最佳选择。
|
1月前
|
安全 中间件 Go
Go语言Web服务性能优化与安全实践
【2月更文挑战第21天】本文将深入探讨Go语言在Web服务性能优化与安全实践方面的应用。通过介绍性能优化策略、并发编程模型以及安全加固措施,帮助读者理解并提升Go语言Web服务的性能表现与安全防护能力。
|
1月前
|
前端开发 JavaScript 开发者
构建响应式Web界面:现代前端开发实践
【2月更文挑战第17天】 随着移动设备用户群体的激增,为各种屏幕尺寸和分辨率构建兼容且优雅的Web界面变得至关重要。本文将深入探讨响应式设计的核心概念,并通过实际案例分析展示如何利用HTML5、CSS3以及JavaScript框架创建流畅的用户体验。我们将着重讨论媒体查询、弹性布局和网格系统等技术的应用,并分享优化响应式网站性能的最佳实践。通过阅读本文,开发者将获得设计和实现适应不同设备的前端项目所需的知识和技能。
|
1月前
|
JSON API 数据格式
构建高效Python Web应用:Flask框架与RESTful API设计实践
【2月更文挑战第17天】在现代Web开发中,轻量级框架与RESTful API设计成为了提升应用性能和可维护性的关键。本文将深入探讨如何使用Python的Flask框架来构建高效的Web服务,并通过具体实例分析RESTful API的设计原则及其实现过程。我们将从基本的应用架构出发,逐步介绍如何利用Flask的灵活性进行模块化开发,并结合请求处理、数据验证以及安全性考虑,打造出一个既符合标准又易于扩展的Web应用。
651 4
|
2月前
|
IDE Java 应用服务中间件
Java Web开发入门指南:从基础到实践
Java Web开发入门指南:从基础到实践
|
2月前
|
数据库 开发者 Python
Python在Web开发中的应用:Flask与Django框架介绍与实践
Python在Web开发中的应用:Flask与Django框架介绍与实践