100行代码实现React虚拟卷帘virtual-list

虚拟卷帘,大列表滚动 react-virtual-list,支持动态高度

借助罗永浩的口头禅:”少废话,先看东西“。

virtual-list

在使用微博的时候,当H5网页版微博数超过了一定数量,比如有 1000 条微博时,这时候刷微博不管是点击弹出或者滚动都会特别的卡,很多时候会导致微博APP闪退。

当然这不是微博服务器或者手机的问题,而是前端开发里面比较常见的长列表渲染问题。实际上并没有必要一次性把 1000 条微博都渲染出来,只需要渲染可视区域内的元素就行,这样明显数量要少得多。

之前在做微博电影的时候研究写过Vue版本的virtual-list,现在研究下React版本的如何实现!

鉴于React那简陋的API,没有Vue的computed和watch[最核心],实现起来有点麻烦【代码啰嗦】!

Vue 社区通常使用开源项目 vue-virtual-scroll-list vue-virtual-scroller 来优化这种无限列表的场景。

React 社区这边比较知名的就是 react-virtualized react-window ,于是撸一个简单够用的版本virtual-list。

下面简单说下简陋版 react-virtual-list 的实现,首先看下这个组件接受的属性:

1
2
3
4
5
6
7
8
9
10
11
export interface RowRendererParams {
index: number;
style: React.CSSProperties;
}

export interface ScrollListProps {
height: number;
rowHeight: number;
total: number;
rowRenderer: (params: RowRendererParams) => any;
}

height 是可视区域的高度,rowHeight 是每一行的高度,total 是总的有多少行,rowRenderer 是每一行要怎样渲染。看下 render 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
render() {
const { height, total, rowHeight } = this.props;
return (
<div
style={{
overflowX: "hidden",
overflowY: "auto",
height
}}
onScroll={this.onScroll}
ref={container => (this.scrollingContainer = container)}
>
<div
style={{
height: total * rowHeight,
position: "relative"
}}
>
{this.renderDisplayContent()}
</div>
</div>
);
}

最外层的 div 为可视区域,并且设置超过的时候显示滚动条。里面的 div 设置完整的高度,也就是每一行的高度乘以总的行数。然后监听最外层的滚动事件:

1
2
3
4
5
6
7
8
9
onScroll = (e: React.UIEvent<any>) => {
if (e.target === this.scrollingContainer) {
const { scrollTop } = e.target as any;

this.setState({
scrollTop
});
}
};

每次滚动的时候,更新一下 scrollTop,触发 render。这里可以对 onScroll 做下节流:

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
get limit() {
const { rowHeight, height } = this.props;
return Math.ceil(height / rowHeight);
}

renderDisplayContent = () => {
const { scrollTop } = this.state;
const { rowHeight, rowRenderer, total } = this.props;

const startIndex = Math.floor(scrollTop / rowHeight);
const endIndex = Math.min(startIndex + this.limit, total - 1);

let content = [];
for (let i = startIndex; i <= endIndex; i++) {
content.push(
rowRenderer({
index: i,
style: {
height: rowHeight,
left: 0,
right: 0,
position: "absolute",
top: i * rowHeight
}
})
);
}

return content;
};

最后是最核心的方法,renderDisplayContent,决定着可视区域里面显示哪些子元素。通过 scrollTop 我们可以知道应该从第几个子元素开始渲染, 然后通过 height 和 rowHeight 算出可视化区域可以显示的子元素个数,最终就可以知道结束的索引。然后调用 rowRenderer 方法,获得完整的显示列表。

完整代码

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
import * as React from 'react';

export interface RowRendererParams {
index: number;
style: React.CSSProperties;
}

export interface ScrollListProps {
height: number;
rowHeight: number;
total: number;
rowRenderer: (params: RowRendererParams) => any;
}

export interface ScrollListState {
scrollTop: number;
}

export class ScrollList extends React.PureComponent<ScrollListProps, ScrollListState> {
scrollingContainer: any;
constructor(props: any) {
super(props);

this.state = {
scrollTop: 0
};
}

get limit() {
const { rowHeight, height } = this.props;
return Math.ceil(height / rowHeight);
}

onScroll = (e: React.UIEvent<any>) => {
if (e.target === this.scrollingContainer) {
const { scrollTop } = e.target as any;

this.setState({
scrollTop
});
}
}

renderDisplayContent = () => {

// Math.round() 四舍五入的取整
// Math.ceil() 向上取整,如Math.cell(0.3) = 1 、又如Math.ceil(Math.random() * 10) 返回1~10
// Math.floor() 向下取整,如Math.floor(0.3) = 0、又如Math.floor(Math.random() * 10)返回0~9

const { scrollTop } = this.state;
const { rowHeight, rowRenderer, total } = this.props;

const startIndex = Math.floor(scrollTop / rowHeight);
const endIndex = Math.min(startIndex + this.limit, total - 1);

const content = [];
for (let i = startIndex; i <= endIndex; i++) {
content.push(
rowRenderer({
index: i,
style: {
height: rowHeight + 'px',
lineHeight: rowHeight + 'px',
left: 0,
right: 0,
position: 'absolute',
top: i * rowHeight
}
})
);
}
return content;
}

render() {
const { height, total, rowHeight } = this.props;
return (
<div
style={{
overflowX: 'hidden',
overflowY: 'auto',
height
}}
onScroll={this.onScroll}
ref={container => (this.scrollingContainer = container)}
>
<div
style={{
height: total * rowHeight,
position: 'relative'
}}
>
{this.renderDisplayContent()}
</div>
</div>
);
}
}
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
import React from 'react';
import { ScrollList } from './vscroll-list';
interface Props {

}
interface State {
list: Array<number>
}
export default class LargeList extends React.Component<Props, State> {
constructor(props: any) {
super(props);
this.state = {
list: [0, 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]
};
}

rowRenderer = ({ index, style }: any) => {
const item = this.state.list[index];
return (
<li key={item} style={style}>
{item}
</li>
);
}

render() {
return (
<ul>
<ScrollList
height={736}
rowHeight={40}
total={this.state.list.length}
rowRenderer={({ index, style }) => {
const item = this.state.list[index];
return (
<li key={item} style={style}>
{item}
</li>
);
}}
/>
</ul>
);
}
}