虚拟列表是为了提高页面性能而出现的。我们知道,一个页面上的 DOM 树越复杂,节点越多性能越低,每次重排(reflow)的成本越高。于是,虚拟列表出现了。虚拟列表的原理是只渲染可视部分以及部分预渲染的节点,待滚动之后替换可视部分节点。余下的空间则用 padding-top
padding-bottom
撑开。
渲染 50 个 Node,实际只渲染了可见部分的 Node
本篇文章不讨论如何实现一个虚拟列表,此类文章网上有很多。但是有关于回退页面无法回到虚拟列表上一次的位置的文章却很少。默认情况下,在后退页面时,浏览器会自动回到上一次浏览的位置。(如果设置 history.scrollRestoration = 'auto'
,默认为 auto
)
但是如果用了虚拟列表,这里的虚拟列表跟随 document 根节点(document.documentElement)滚动,即使开启了 Restoration,回退页面后仍然无法回到上一次的位置。这是因为虚拟列表需要计算得出整个容器的高度,在计算之前容器没有高度,浏览器就不能回到之前的滚动高度了,因为高度不存在。
react-virtuoso
一种方式是,记录之前虚拟列表容器的高度,在回退回来之后先用之前记录的值去撑开整个容器高度,待虚拟列表加载后去除。这样有个问题是虚拟列表无法知道当前的位置原来是什么内容,因为虚拟列表都是按照单个 Node 高度去计算的,整体高度是一个预估值,不能知道当前位置具体是什么。
对于 react-virtuoso 这个库,没有直接暴露给我们每个 Node 计算后的高度,也没有一个自身的 State 想要缓存状态不太现实。一个不好的解决方案是用提供的接口在每次滚动后记录一个 Range,Range 是一个当前渲染内容的索引,在之后的渲染后可以用自身的 scrollTo
方法跳转。这样有个坏处是会出现跳动,原先在顶部直接跳动到了原先的位置,还是个预估值。既不准确也不符合 UX 逻辑。
之后,我又找到一个比较小众的库,
,不仅仅可以在 React 使用,他独立封装了一个 Core,可以单独在各个框架中使用,即使在 VanillaJS 中使用,小众的库功能肯定不会很多,但是基本的功能也都有,也可以 fork 一份出来进行修改和扩展。选择此库的原因是他暴露了自身的 State,可以缓存每个 State 在之后的渲染中使用。该库没有文档,没有 type definition,通过翻看源码我们可以知道,可以在 Router Change 之前获取到该组件的 Ref,记录下该组件的 State,在后面的渲染中注入 initialState。
jsx
1import Router from 'next/router'
2import { useEffect, useMemo, useRef } from 'react'
3import VirtualScroller from 'virtual-scroller/react'
4const cacheState = {}
5const cachePrevTop = {}
6if ('window' in globalThis) {
7 window.debug = {
8 cacheState,
9 cachePrevTop,
10 }
11}
12export default function Test() {
13 const cacheKey = useMemo(
14 () => ('window' in globalThis ? location.pathname : ''),
15 [],
16 )
17
18 const ref = useRef()
19 useEffect(() => {
20 // history.scrollRestoration = 'manual'
21 const $scroll = document.scrollingElement
22
23 requestAnimationFrame(() => {
24 $scroll.scrollTop = cachePrevTop[cacheKey]
25 // console.log('top')
26 })
27
28 const handler = () => {
29 cachePrevTop[cacheKey] = $scroll.scrollTop
30 if (ref.current) {
31 cacheState[cacheKey] = { ...ref.current.state }
32 }
33 }
34 Router.events.on('routeChangeStart', handler)
35 return () => {
36 Router.events.off('routeChangeStart', handler)
37 }
38 }, [])
39 return (
40 <div className="">
41 <VirtualScroller
42 ref={ref}
43 items={Array.from({ length: 50 }, (_, i) => i)}
44 itemComponent={Item}
45 initialState={cacheState[cacheKey]}
46 />
47 </div>
48 )
49}