原创

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

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);
});

3. 在线演示

https://tech.jiangjiesheng.cn/dev/study/demo/loading-large-list-data-using-virtual-scrolling-and-Web-Workers/

正文到此结束
本文目录