滚动加载更多

当页面数据过多为了优化页面加载速度和避免页面的卡顿时使用滚动加载的方式实现数据的分页加载功能

加载更多方案

利用 scroll 事件实现

通过监听对应元素的滚动,不断计算元素相对窗口的位置判断元素是否滚动到底部,若滚动到底部就加载更多

缺点
  • scroll 事件不断触发需要做节流操作实现
  • 每次触发事件时都要计算元素的位置性能不好
优点
  • 兼容性好
  • 易于实现

利用 IntersectionObserver 实现

IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。

优点
  • IntersectionObserver 是浏览器内部实现的方法,性能较高
  • 可以自定义视窗重叠的阈值
缺点
  • 兼容性较差

使用 IntersectionObserver 的一般流程

第一步

初始化 IntersectionObserver,构建 IntersectionObserver 实例,确定视窗窗口和触发阈值及回调函数

1
const ob = new IntersectionObserver(callback, options)
说明
callback

当元素可见比例超过指定阈值后,会调用一个回调函数,此回调函数接受两个参数

  • entries: 一个 IntersectionObserverEntry 对象的数组,每个被触发的阈值,都或多或少与指定阈值有偏差。
  • observer: 被调用的 IntersectionObserver 实例。
options(可选)

一个可以用来配置 observer 实例的对象。如果 options 未指定,observer 实例默认使用文档视口作为 root,并且没有 margin,阈值为 0%(意味着即使一像素的改变都会触发回调函数)。你可以指定以下配置:

  • root: 监听元素的祖先元素 Element 对象,其边界盒将被视作视口。目标在根的可见区域的的任何不可见部分都会被视为不可见。
  • rootMargin: 一个在计算交叉值时添加至根的边界盒(bounding_box)中的一组偏移量,类型为字符串(string) ,可以有效的缩小或扩大根的判定范围从而满足计算需要。语法大致和 CSS 中的 margin 属性等同; 可以参考 The root element and root margin in Intersection Observer API 来深入了解 margin 的工作原理及其语法。默认值是”0px 0px 0px 0px”。
  • threshold: 规定了一个监听目标与边界盒交叉区域的比例值,可以是一个具体的数值或是一组 0.0 到 1.0 之间的数组。若指定值为 0.0,则意味着监听元素即使与根有 1 像素交叉,此元素也会被视为可见. 若指定值为 1.0,则意味着整个元素都是可见的。阈值的默认值为 0.0。

第二步

调用 observe 观测元素,targetElement 即为观测的元素

1
ob.observe(targetElement)

第三步

调用 unobserve 取消对元素的观测,target 即为取消观测的对象

1
ob.unobserve(target)

第四步

调用 disconnect 终止对所有目标元素可见性变化的观察

1
ob.disconnect()

React 中的实现

  • 初始化 IntersectionObserver,确定视窗窗口和触发阈值及回调函数
  • 通过 children 属性获取子组件中的最后一个元素
  • 监听列表中的最后一个组件,当最后的组件触发事件时加载更多
  • 引入 intersection-observer 组件做兼容性处理

完整代码参考

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import React from 'react'
import 'intersection-observer'

interface IProps extends React.DOMAttributes<HTMLDivElement> {
children: any
getRootElement?: (parent: Element) => Element
rootMargin?: string
observerCallback?: IntersectionObserverCallback
}

class LoadMore extends React.PureComponent<IProps> {
private ele: Element | null = null
private observer: IntersectionObserver | undefined
private observerElement: Element | null = null
private lastThreshold: number = 0

public componentDidMount() {
const { rootMargin } = this.props
this.observer = new IntersectionObserver(this.observerCallback, {
root: this.getRootElement(),
rootMargin,
threshold: [0, 0.01],
})
this.listenLastDom()
}

public componentDidUpdate(prevProps: IProps) {
const { children: oldChildren } = prevProps
const { children } = this.props
const len = React.Children.toArray(children).length
if (len !== React.Children.toArray(oldChildren).length && len) {
this.listenLastDom()
}
}

public componentWillUnmount() {
this.unobserve()
}

public render() {
const {
children,
getRootElement,
observerCallback,
rootMargin,
...otherProps
} = this.props
return (
<div ref={this.getElement} {...otherProps}>
{children}
</div>
)
}

private getRootElement = () => {
const { getRootElement } = this.props
const ele = this.ele as Element
let parent = null
if (getRootElement) {
parent = getRootElement(
ele && ele.parentElement
? ele.parentElement
: (document.body as Element),
)
}
if (!parent && ele && ele.parentElement) {
parent = ele.parentElement
}
return parent
}

private getElement = (ele: Element | null) => {
this.ele = ele
}

// 监听回调
private observerCallback = (
entries: IntersectionObserverEntry[],
observer: IntersectionObserver,
) => {
const [{ intersectionRatio = 0 }] = entries

const { observerCallback } = this.props
if (intersectionRatio > this.lastThreshold) {
if (observerCallback) {
observerCallback(entries, observer)
}
}
this.lastThreshold = intersectionRatio
}

// 停止监听元素
private unobserve = () => {
if (this.observerElement && this.observer) {
this.observer.unobserve(this.observerElement)
}
}

// 监听元素
private listenLastDom = () => {
if (this.ele && this.ele.lastChild) {
const dom: Element = this.ele.lastChild as Element
this.lastThreshold = 0
const observer = this.observer as IntersectionObserver
if (dom && observer) {
this.unobserve()
this.observerElement = dom
observer.observe(this.observerElement)
}
}
}
}

export default LoadMore

参考