1

我在Cordova应用程序中使用 JS 构建了一个虚拟滚动列表,我想在每次滚动后保存位于视口顶部的确切节点。虚拟滚动的复杂之处在于使用scrollTop不可靠,因为节点正在从列表的顶部和底部删除,这会改变滚动位置。在 Javascript 中是否有可靠且高效的方法来执行此操作?用例是这样的:当用户关闭并重新打开应用程序时,我想将用户放置在相同的滚动位置,并确保该位置的元素是用户在关闭应用程序之前最后一次看到的元素。我正在考虑这个策略:

  • 获取每个滚动列表顶部的元素,如下所示:
scrollableList.addEventListener('scroll', e => {
  const topMostElement = document.elementFromPoint(150, 150); // <-- 150px due to some positioning in the app
});
  • 保存该元素的标识符,以便我可以在应用程序初始化时预加载列表,以便该元素始终呈现在该位置

我想知道是否有其他策略可以在基于 JS 的虚拟滚动器中处理此问题。我已经看过用 Javascript 构建虚拟滚动条的教程,但还没有看到提供有关如何解决这个问题的实现细节的教程。提前致谢!

更新 1

虚拟滚动列表有很多节点,可以有100多个,每个都有自己的子节点。这会导致其自身的一系列问题,因此重要的是(在可能的情况下)该解决方案不需要遍历节点列表或在每个节点上执行昂贵的操作,以避免阻塞主线程。

4

2 回答 2

0

我的猜测是您可能会保存行索引,而不是位置。这似乎更符合语义。比如说,您的数据源具有从 MIN 到 MAX 索引的项目。通过持久化 START 索引,您可以初始化视图以及初始位置的最简单计算,如下所示:

await renderVirtualScrollUI();
viewportElement.scrollTop = (START - MIN) * ITEM_SIZE;

您可以从 API、本地存储、查询字符串等中读取 START 值。现在如何确定。作为一种可能的方法,我将遍历视口的内部元素并使用getBoundingClientRect查找第一个可见的元素:

const getTopRowIndex = () => {
  const rows = viewportElement.children; // depends on the template
  const viewportTop = viewportElement.getBoundingClientRect().top;
  for (let i = 0; i < rows.length; i++) {
    const row = rows[i];
    const rowTop = row.getBoundingClientRect().top;
    if (rowTop > viewportTop) {
      // calculate topmost visible index based on position
      const index = Math.floor(viewportElement.scrollTop / ITEM_SIZE) + MIN;
      persistTopIndex(index);
      return;
    }
  }
}

getTopRowIndex()当你想保存值时运行方法。例如,响应 500 毫秒限制的滚动事件(通过lodash.throttle):

viewportElement.addEventListener('scroll', _.throttle(getTopRowIndex, 500));
于 2020-07-30T14:20:55.347 回答
0
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style >
        .scroll-item {
            height: 400px;
            background: red;
        }

        .scroll-item:nth-of-type(2n) {
            background: #000;
        }
    </style>
</head>

<body>
    <div id="scrollable-list">
        <div id="item-1" class="scroll-item"></div>
        <div id="item-2" class="scroll-item"></div>
        <div id="item-3" class="scroll-item"></div>
        <div id="item-4" class="scroll-item"></div>
        <div id="item-5" class="scroll-item"></div>
    </div>

    <script>
        const lastItem = localStorage.getItem('lastItem');
        if (lastItem) {
            const itemElement = document.getElementById(lastItem);
            if (itemElement) {
                itemElement.scrollIntoView();
            }
        }
        let options = {
            rootMargin: '0px',
            threshold: 1.0
        }
        const target = document.querySelectorAll('.scroll-item');
        const observer = new IntersectionObserver((entries, observer) => {
            entries.forEach(entry => {
                if (!entry.isIntersecting) return;
                console.log(entry.target);
                localStorage.setItem('lastItem', entry.target.id);
            })
        }, options);

        target.forEach(item => {
            observer.observe(item);
        });
    </script>
</body>

</html>

您可以使用 IntesectionObserver 并将当前元素保存在本地存储中

示例: https ://jsfiddle.net/6bu0ynkp/

  • 首先,我们将选择所有“滚动项”项
  • 然后我们将创建一个新的 IntersectionObserver 并遍历条目以检查项目是否相交
  • 如果视口内的项目我们将其 id 保存到 localStorage
  • 在下一页重新加载我们正在检查我们是否有“lastItem”并将项目滚动到视图中
于 2020-07-30T13:59:32.863 回答