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



参考资料


相关文章
|
8天前
|
弹性计算 Java 关系型数据库
Web应用上云经典架构实践教学
Web应用上云经典架构实践教学
Web应用上云经典架构实践教学
|
1月前
|
前端开发 JavaScript 开发工具
从框架到现代Web开发实践
从框架到现代Web开发实践
46 1
|
1月前
|
前端开发 开发者 UED
移动优先:响应式设计在现代Web开发中的实践策略
【10月更文挑战第29天】在现代Web开发中,响应式设计已成为不可或缺的实践策略,使网站能适应各种设备和屏幕尺寸。本文介绍了移动优先的设计理念,对比了移动优先与桌面优先的策略,探讨了流式布局与固定布局的区别,详细讲解了CSS媒体查询的使用方法,并强调了触摸和手势支持及性能优化的重要性。
40 1
|
2月前
|
开发框架 自然语言处理 PHP
PHP在Web开发中的持久魅力与创新实践###
【10月更文挑战第17天】 本文探讨了PHP作为一门老牌却充满活力的编程语言,在现代Web开发中的独特优势和未来趋势。通过分析其简洁性、灵活性、强大生态系统及不断创新的特性,本文旨在揭示PHP为何能持续吸引开发者,并在技术快速迭代的时代保持竞争力。同时,文章也展望了PHP在未来Web开发领域的发展潜力,强调其在技术创新和社区支持下,依然能够引领Web开发的新潮流。 ###
45 9
|
1月前
|
SQL 安全 Go
PHP在Web开发中的安全实践与防范措施###
【10月更文挑战第22天】 本文深入探讨了PHP在Web开发中面临的主要安全挑战,包括SQL注入、XSS攻击、CSRF攻击及文件包含漏洞等,并详细阐述了针对这些风险的有效防范策略。通过具体案例分析,揭示了安全编码的重要性,以及如何结合PHP特性与最佳实践来加固Web应用的安全性。全文旨在为开发者提供实用的安全指南,帮助构建更加安全可靠的PHP Web应用。 ###
46 1
|
2月前
|
存储 安全 数据库
后端技术在现代Web开发中的实践与创新
【10月更文挑战第13天】 本文将深入探讨后端技术在现代Web开发中的重要性,通过实际案例分析展示如何利用先进的后端技术提升用户体验和系统性能。我们将从基础架构设计、数据库优化、安全性保障等方面展开讨论,为读者提供清晰的指导和实用的技巧。无论是新手开发者还是经验丰富的技术人员,都能从中获得启发和帮助。
55 2
|
2月前
|
自然语言处理 Cloud Native 数据安全/隐私保护
后端技术在现代Web开发中的实践与创新
本文探讨了后端技术在现代Web开发中的重要性及其应用。通过分析当前流行的后端框架和开发模式,揭示了如何利用这些技术来构建高效、可扩展的Web应用程序。同时,文章也讨论了未来后端技术的发展趋势,为开发者提供了一些启示。
|
2月前
|
前端开发 JavaScript API
前端开发趋势与实践:拥抱Web Components
前端开发趋势与实践:拥抱Web Components
52 4
|
2月前
|
前端开发 JavaScript 安全
前端开发趋势与实践:构建现代Web应用的探索
【10月更文挑战第1天】前端开发趋势与实践:构建现代Web应用的探索
40 2
|
1月前
|
关系型数据库 API PHP
PHP在Web开发中的优势与实践###
【10月更文挑战第24天】 PHP是一种流行的服务器端脚本语言,特别适合Web开发。其简单易学、灵活性高和广泛应用的特点,使其成为众多开发者的首选。本文将探讨PHP在Web开发中的主要优势及其实际应用,通过实例展示如何使用PHP构建高效、可靠的Web应用。无论你是初学者还是有经验的开发者,这篇文章都将提供有价值的见解和实用技巧。 ###
46 0