前端-使用虚拟滚动和Web Workers加载大量list数据的方案


最新版本更新
https://code.jiangjiesheng.cn/article/349

1. 应用场景

可尝试用于数据量大时,列表渲染慢且卡顿的页面
另外是否可考虑预加载列表逻辑?

2. 代码

2.1 index.html

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<title>加载大量list数据-虚拟滚动和Web Workers的示例</title>
		<style>
			.div1 {
				height: 100px;
				width: 100%;
				background-color: yellow;
				margin: 5px;

				/* div中的文字居中方式1 */
				display: flex;
				justify-content: center;
				align-items: center;

				/* div中的文字居中方式2 */
				/* display: grid;
				place-items: center; */
			}

			.search {
				width: 100%;
				margin: 5px;
			}

			.div2 {
				width: 100%;
				background-color: orange;
				margin: 5px;
				word-break: break-all;
			}

			.list {
				height: 300px;
				overflow-y: scroll;
				border: 1px solid black;
				margin: 5px;
			}

			#progress {
				margin: 5px;
				height: 30px;
			}

			.row {
				height: 40px;
				line-height: 40px;
				border-bottom: 1px solid #ccc;
			}
		</style>
	</head>
	<body>
		<div class="div1">加载大量list数据-虚拟滚动和Web Workers的示例
		</div>

		<div class="search">
			<input placeholder="请输入" id="input" />
			<button id="search">搜索</button>
		</div>
		<div id="progress">当前进度:</div> <!-- 共多少条,当前正在加载弟多少条 -->

		<div class="list" id="list"></div>
		<pre class="div2">
	Web Workers 兼容性
	
	【如果实现担心兼容性的问题,就直接改造成js模拟分页吧,关键词 "js模拟分页和筛选"】
	
	Web Workers 是 HTML5 引入的一项技术,它允许在浏览器后台独立于主线程运行脚本,
	避免了长时间运行的脚本导致的页面冻结。Web Workers 的兼容性在现代浏览器中是非
	常广泛的,但仍然存在一些差异和限制。以下是主要浏览器对 Web Workers 的支持情况:

	Google Chrome: 自 Chrome 7(2008年12月)起支持 Web Workers。
	Mozilla Firefox: 自 Firefox 3.5(2009年6月)起支持 Web Workers。
	Apple Safari: 自 Safari 4(2009年6月)起支持 Web Workers。
	Microsoft Edge: 原始的 Edge 浏览器(基于 EdgeHTML)自 Edge 12(2015年7月)
	起支持 Web Workers。新版 Edge(基于 Chromium)自发布之初就支持 Web Workers。
	Internet Explorer: Internet Explorer 不支持 Web Workers。即使是最新版本的 
	IE11 也不支持这项技术。
	Opera: 自 Opera 10.5(2010年3月)起支持 Web Workers。
	Android Webview: 自 Android 4.4(2013年10月)起支持 Web Workers。
	iOS Safari: 自 iOS 3.2(2010年4月)起支持 Web Workers。
	
	兼容性注意事项:
	
	服务工作线程(Service Workers):这是一种特殊的 Web Worker,用于实现离线缓存
	和推送通知等功能。它的兼容性与普通 Web Workers 类似,但IE和部分旧版浏览器不支持。
	Shared Workers:允许多个脚本共享一个 Worker 实例,以节省资源。它的兼容性与普通
	 Web Workers 类似,但IE和其他一些较旧的浏览器不支持。
	Worker 全局作用域:self 关键字在 Worker 内部引用全局作用域,这与主线程中的 
	window 相似。不同之处在于 Worker 不提供 window 对象。
	Blob 和 File 对象:在 Web Workers 中,Blob 和 File 对象的兼容性可能受限,
	因为它们的实现可能与主线程不同。
	XMLHttpRequest 和 Fetch API:虽然 Web Workers 支持使用 XMLHttpRequest 和 
	Fetch API 进行网络请求,但它们的使用有一些限制,例如 CORS 需要正确的设置。
	
	兼容性检测:
	在代码中,你可以使用以下方式检测 Web Worker 是否可用:

	if (typeof Worker !== 'undefined') {
	  // Web Worker 支持
	} else {
	  // Web Worker 不支持
	}
	
	总的来说,Web Workers 在现代浏览器中是广泛支持的,但在开发时仍然需要考虑不支持
	 Web Workers 的场景,比如为 IE 用户提供回退方案。在生产环境中,使用类似 
	 Can I Use 或 MDN 的资源来检查兼容性是一个好习惯。2</pre>

		<script>
			
			let isShowAllOnce = true; //异步加载所有 【支持修改】
			let data = []; // Your data array here
			const filterCriteria = {}; // Your filter criteria object here

			const worker = new Worker('worker.js');
			const listElement = document.getElementById('list');
			const search = document.getElementById('search');
			const input = document.getElementById('input');
			const progress = document.getElementById('progress');
			const pageSize = 150;
			let currentStartRow = 0;
			let currentEndRow = pageSize; // Initial number of rows to load
			const rowHeight = 1; //40 有问题
			let isLoadingMore = false; //保持顺序和防止重复点击
			let lastClickSearchTimestamp = null; //保持顺序和防止重复点击

			for (var i = 0; i < 5000; i++) {
				const item = {
					"name": "第" + (i + 1) + "人",
					"orderNum": 1 + i
				};
				data.push(item);
			}

			console.log("data size =" + data.length)

			// Initialize the list
			loadMoreRows();

			// Add a scroll event listener to load more rows as needed
			listElement.addEventListener('scroll', function() {
				if (isScrolledToBottom()) {
					loadMoreRows();
					console.log("isScrolledToBottom true")
				} else {
					console.log("isScrolledToBottom false")
				}
			});

			// Function to check if the user has scrolled to the bottom
			function isScrolledToBottom() {
				const {
					scrollTop,
					scrollHeight,
					clientHeight
				} = listElement;
				let abs = Math.abs((scrollTop + clientHeight) - scrollHeight);
				// console.log("abs=" + abs)
				return abs < 10;
			}

			// Function to load more rows using the Web Worker
			function loadMoreRows() {
				// console.log("loadMoreRows")
				if (isLoadingMore) {
					return;
				}
				isLoadingMore = true;
				worker.postMessage({
					//key value相同写一个
					data,
					filterCriteria,
					startRow: currentStartRow,
					endRow: currentEndRow,
					lastClickSearchTimestamp
				});

			}

			// Listen for messages from the Web Worker
			worker.onmessage = function(event) {
				const dataObj = event.data;
				const newData = dataObj.slicedData;
				let lastClickSearchTimestampFromPost = dataObj.lastClickSearchTimestamp;

				if (!newData || newData.length == 0) {
					isLoadingMore = false;
					return;
				}
				if (lastClickSearchTimestampFromPost < lastClickSearchTimestamp) {
					console.log("快速切换搜索内容的情况下,不是最近的搜索数据,防止数据混乱,直接return ");
					isLoadingMore = false;
					return;
				}

				renderRows(newData);

				currentStartRow = currentEndRow;
				currentEndRow += pageSize; // Load 10 more rows each time

				// console.log("worker.onmessage currentStartRow =" + currentStartRow + "data.length=" + data.length);
				let pgs = "当前进度:";

				if (!!isShowAllOnce) {
					if (currentStartRow < data.length) {
						console.log("isShowAllOnce =", isShowAllOnce, "自动分页加载全部方式,当前pageSize:", pageSize, "currentStartRow:",
							currentStartRow,
							"总数:",
							data.length);
						isLoadingMore = false;
						loadMoreRows();
						pgs += "自动分页加载全部方式[支持改成 滚动加载方式],当前pageSize:";
					} else {
						console.log("isShowAllOnce =" + isShowAllOnce, currentStartRow, data.length, "自动分页加载全部方式,已加载全部");
						pgs += "自动分页加载全部方式[支持改成 滚动加载方式],已加载全部,当前pageSize:";
					}

				} else {
					console.log("isShowAllOnce =" + isShowAllOnce, currentStartRow, data.length, "滚动加载方式");
					pgs += "滚动加载方式[支持改成 自动分页加载全部方式],当前pageSize:";
				}
				pgs += pageSize;
				pgs += ",";
				pgs += "currentStartRow:";
				pgs += currentStartRow;
					pgs += ",";
				pgs += "总数:";
				pgs += data.length;

				isLoadingMore = false;

				progress.innerText = pgs;

			};

			// Render the rows to the DOM
			function renderRows(dataSlice) {
				if (!dataSlice || dataSlice.length == 0) {
					return;
				}

				dataSlice.forEach(item => {
					const row = document.createElement('div');
					row.className = 'row';
					row.style.transform = `translateY(${(currentStartRow++) * rowHeight}px)`;
					row.textContent = `${item.name} - ${item.orderNum}`;
					listElement.appendChild(row);
				});
			}

			search.addEventListener("click", function(e) {

				const inputValue = input.value.trim();
				filterCriteria.name = inputValue;
				console.log("inputValue =" + inputValue)
				currentStartRow = 0;
				currentEndRow = 10;
				listElement.innerHTML = '';
				//这里应该记录下本次的时间戳,作为全局对象,
				//worker.onmessage中只接收这个时间错之后的数据,防止不停的切换搜索关键词,会带出来非本次搜索的结果
				lastClickSearchTimestamp = new Date().getTime();
				loadMoreRows();
			})
		</script>
	</body>
</html>

2.2 worker.js

// worker.js
self.addEventListener('message', function(event) {
  //从event.data解析出data、filterCriteria、startRow、endRow、lastClickSearchTimestamp
  const {
    data,
    filterCriteria,
    startRow,
    endRow,
    lastClickSearchTimestamp
  } = event.data;

  // console.log("filterCriteria =" + JSON.stringify(filterCriteria))
  // console.log("data =" + JSON.stringify(data))
  // console.log("data size =" + data.length)

  const filteredData = data.filter(item => {
    return Object.keys(filterCriteria).every(key => {
      if (filterCriteria[key]) {
        return item[key].toString().toLowerCase().includes(filterCriteria[key]
                                                           .toString().toLowerCase());
      }
      return true;
    });
  });
  // Slice the data for the requested range
  const slicedData = filteredData.slice(startRow, endRow);

  //console.log("slicedData=" + startRow + ",endRow=" + endRow + ",filteredData" + JSON.stringify(slicedData))

  // Send the slice back to the main thread
  //key value相同写一个
  let slicedDataWithTime = {
    slicedData,
    lastClickSearchTimestamp
  };
  self.postMessage(slicedDataWithTime);
});

2. 在线演示

https://tech.jiangjiesheng.cn/dev/study/demo/loading-large-list-data-using-virtual-scrolling-and-Web-Workers/
​​
最新版本更新
https://code.jiangjiesheng.cn/article/349

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值