导读

  • 在很早之前我们都是通过滚动距离来进行判断,是否在可视区,进而判断是否渲染,完成虚拟列表
  • 这个过程是有些繁琐的,当滚动回去的时候 还要重新计算逻辑
  • 但是js 的新对象 IntersectionObserver 就完美解决了这个问题
  • 可以通过这个对象的方法进行设计,直接可以根据 entry.isIntersecting 判断是否在可视区
  • true 就是可视区 false 就在屏幕之外
  • 逻辑结构简单明了
  • 通过 react Hooks 进行了封装
  • 用的时候只需要调用下就可以很简单的完成一个虚拟列表了

useVirtualList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 虚拟列表
import { RefObject, useEffect, useState } from 'react'
interface Args extends IntersectionObserverInit {
freezeOnceVisible?: boolean
}
/**
*
* @param {ReactRef} elementRef 传入虚拟列表父盒子的 ref
* @param {object}
* threshold 阀值 0-1 1表示完全出现在屏幕可视区域才触发
* root 表示指定根元素 默认为浏览器视口 用于检查目标可见性
* rootMargin root的外边距 '0 0 0 0'
* freezeOnceVisible 是否缓存 再次滑动不重新渲染
* @returns {Object} entry 这个对象具有当前可视区的信息
* 例如 entry.isIntersecting 是否在可视区范围
*/
function useVirtualList(
elementRef: RefObject<Element>,
{
threshold = 0,
root = null,
rootMargin = '0%',
freezeOnceVisible = true
}: Args
): IntersectionObserverEntry | undefined {

const [entry, setEntry] = useState<IntersectionObserverEntry>()
const frozen = entry?.isIntersecting && freezeOnceVisible
const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
setEntry(entry)
}
useEffect(() => {
const node = elementRef?.current
const hasIOSupport = !!window.IntersectionObserver
if (!hasIOSupport || frozen || !node) return
const observerParams = { threshold, root, rootMargin }
const observer = new IntersectionObserver(updateEntry, observerParams)
observer.observe(node)
return () => observer.disconnect()
}, [elementRef, JSON.stringify(threshold), root, rootMargin, frozen])

return entry
}
export default useVirtualList

index.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import useVirtualList from './useVirtualList'
import { useRef } from 'react'

const Section = (props: any) => {
const ref = useRef(null)
const entry = useVirtualList(ref, {})
const isVisible = !!entry?.isIntersecting
var imgUrlArr = [ // 所有需要加载的图片链接
"https://i.loli.net/2019/12/25/Fze9jchtnyJXMHN.jpg",
"https://i.loli.net/2019/12/25/ryLVePaqkYm4TEK.jpg",
"https://i.loli.net/2019/12/25/gEy5Zc1Ai6VuO4N.jpg",
"https://i.loli.net/2019/12/25/d6QHbytlSYO4FBG.jpg",
"https://i.loli.net/2019/12/25/6nepIJ1xTgufatZ.jpg",
"https://i.loli.net/2019/12/25/E7Jvr4eIPwUNmzq.jpg",
"https://i.loli.net/2019/12/25/mh19anwBSWIkGlH.jpg",
"https://i.loli.net/2019/12/25/2tu9JC8ewpBFagv.jpg",
];
console.log(`Render Section ${props.title}`, { isVisible })
return (
<div
ref={ref}
style={{
minHeight: '10vh',
display: 'flex',
border: '1px dashed #000',
fontSize: '2rem',
width: '100%'
}}
>
{
isVisible ? (
<div style={{ margin: 'auto', textAlign: 'center' }}>
<h1>This is the {props.title} girl</h1>
<img src={imgUrlArr[(0 | Math.random() * 7)]} style={{ width: '100%', height: '100%' }} />
</div>
) : ""
}
</div>
)
}
export default function IndexPage() {
return (
<>
{
Array.from({ length: 100 }).map((_, index) => (
<Section key={index + 1} title={`${index + 1}`} />
))
}
</>
);
}