长列表渲染优化
前言
长列表其实可以分为两种,一种是元素不长
但元素个数很多
,一种是元素个数不多
,但元素都很长
。
针对第一种,我们采用传统的虚拟列表去优化渲染,详情可看这篇文章:三种虚拟列表原理与实现
而第二种就是本文的主要内容了。
有“短”又长的列表
表现
这种列表之所以说短,是因为元素个数很少,而长,是因为元素高度很大,导致整个列表非常的长。我们来看一下这种列表的结构就明白了。
const List = () => (
<div className="list">
{new Array(10).fill(true).map((listItem, index) => (
<div className="list-item" key={index}>
{new Array(100).fill(true).map((item, i) => (
<div className="child" key={`${index}-${i}`}>{`${index}-${i}`}</div>
))}
</div>
))}
</div>
);
list中一共就只有10个元素,但每个元素都包含100个元素,且自身无滚动条,难以用传统的虚拟列表去解决。
毫无疑问,此时页面中已经有上千个dom了,如果每个元素再复杂点,页面难免会出现卡顿的现象。
分析
或许有人会说,这里一样可以用传统的虚拟列表去解决。但我们仔细想想,如果是传统的虚拟列表,可视区放一个,上下缓冲区放一个,那常态下也是有着至少300个dom,并不见得是好方案,并且,如果说我的这个List组件并不是如此纯净的列表呢?如果他是一个页面呢?
这种情况下我们计算元素的top值是否就变得困难了。
到此,我们可以沿用虚拟列表的思想,想想看有没一种办法可以使我们知道那个ListItem出现在了可视区,只渲染它就好了,这样常态下只有100个元素,交接处最多才200。
解决方案
要想知道哪个listItem在可视区内,我们需要了解一个api:IntersectionObserver
IntersectionObserver
IntersectionObserver是一个观察者,我们可以指定让他观察某个元素,可以监听到观察元素是否与指定父盒子发生了交接。
看个demo:
<body>
<div class="box">
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
</div>
</body>
<script>
const child = document.querySelector('.child');
const box = document.querySelector('.box');
// 第一个参数callback:当元素可见比例超过指定阈值后,会调用一个回调函数
// 第二个参数options:其中有一项配置就是指定监听的父元素
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
console.log(entry);
})
}, { root: box });
observer.observe(child);
</script>
以上demo的主要效果是,当第一个child进入或离开box的可视区内,都会触发callback。
我们主要关注触发callback是打印的entry,这个entry中有一个很关键的属性isIntersecting,它的意思是,如果child在box的可视区内就为true,不在就为false。
以上只是简单介绍IntersectionObserver,若想了解更多建议去MDN学习:IntersectionObserver
主要思路
通过上面的IntersectionObserver,我们可以轻松获取到当前出现在List可视区内的LIstItem,之后我们只需要将不在可视区内的ListItem用一个等高的空盒子替换掉元素内容,撑开滚动条,然后只渲染可视区内的ListItem就好了。
实现
import React from 'react';
const ListItem = ({ index }) => {
const [visible, setVisible] = React.useState(true);
const ListItemRef = React.useRef();
React.useEffect(() => {
const box = document.querySelector('#list');
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
setVisible(entry.isIntersecting);
})
}, { root: box });
observer.observe(ListItemRef.current);
// 组件被卸载一定要记得取消观察,页面中观察者太多也是很耗性能的
return () => {
observer.unobserve(ListItemRef.current);
};
}, []);
return (
<div className="list-item" ref={ListItemRef}>
<div style={{ height: 20 * 100, width: '100%', display: visible ? 'none' : 'block' }} />
<div style={{ display: visible ? 'block' : 'none' }}>
{new Array(100).fill(true).map((item, i) => (
<div
className="child"
key={`${index}-${i}`}
style={{ background: i % 2 ? 'lightblue' : 'lightcoral' }}
>
{`${index}-${i}`}
</div>
))}
</div>
</div>
);
};
const List = () => (
<div id="list" >
<header>页面的头部,这里包含各种标题,登录信息等</header>
<section>页面的简介部分等等</section>
{
new Array(10).fill(true).map((listItem, index) => (
<ListItem key={index} index={index} />
))
}
</div >
);
export default List;
可以看到只有第一个在可视区内,所以就只有第一个的包含100个元素的盒子是block状态,其他都是none。
进阶版
相信有不少读者意识到了一点,上面例子中的元素高度都被固定,且用于撑开高度的空白盒子也是写死了2000的高度。但凡任何一个盒子可能发生高度变化,上面的代码就会出现不合理的地方了。接下来我们完善下它。
ResizeObserver
在这个版本我们需要认识这个ResizeObserverapi,一样是观察者家族的成员,他能指定观察的元素,当元素大小变化时就触发callback。
import React from 'react';
const Demo = () => {
const [items, setItems] = React.useState([0, 1]);
const boxRef = React.useRef();
React.useEffect(() => {
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
console.log(entry);
})
})
observer.observe(boxRef.current);
return () => {
observer.unobserve(boxRef.current);
};
}, []);
return (
<div style={{ borderBottom: '1px solid lightgreen' }} ref={boxRef}>
<button onClick={() => setItems((pre) => [...pre, pre.length])}>增加元素</button>
{items.map(item => (
<div key={item}>{item}</div>
))}
</div>
);
};
export default Demo;
以上demo可以看出,当所观察的盒子发生大小变化时,就会触发callback,我们可以在entry.contentRect中拿到height。
以上只是简单介绍ResizeObserver,若想了解更多建议去MDN学习:ResizeObserver
主要思路
我们只要观察listItem高度变化,当高度出现变化时,我们就更新空盒子的高度。
实现
import React, { useState } from 'react';
const Item = ({ index, str }) => {
const [content, setContent] = useState([str]);
return (
<div
className="child"
key={index}
style={{ background: index % 2 ? 'lightblue' : 'lightcoral' }}
>
{content.map((item, i) => (<div key={i}>{item}</div>))}
<button onClick={() => setContent((pre) => [...pre, pre[0]])}>增加元素</button>
</div>
);
};
const ListItem = ({ index }) => {
const [height, setHeight] = React.useState(0);
const [visible, setVisible] = React.useState(true);
const ListItemRef = React.useRef();
const containerRef = React.useRef();
React.useEffect(() => {
const box = document.querySelector('#list');
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
setVisible(entry.isIntersecting);
})
}, { root: box });
observer.observe(ListItemRef.current);
return () => {
observer.unobserve(ListItemRef.current);
};
}, []);
React.useEffect(() => {
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
if (entry.contentRect.height) {
setHeight(entry.contentRect.height);
}
})
})
observer.observe(containerRef.current);
return () => {
observer.unobserve(containerRef.current);
};
}, []);
return (
<div className="list-item" ref={ListItemRef}>
<div style={{ height, width: '100%', display: visible ? 'none' : 'block' }} />
<div style={{ display: visible ? 'block' : 'none' }} ref={containerRef}>
{new Array(100).fill(true).map((item, i) => (
<Item key={`${index}-${i}`} str={`${index}-${i}`} index={i} />
))}
</div>
</div>
);
};
const List = () => (
<div id="list" >
<header>页面的头部,这里包含各种标题,登录信息等</header>
<section>页面的简介部分等等</section>
{
new Array(10).fill(true).map((listItem, index) => (
<ListItem key={index} index={index} />
))
}
</div >
);
export default List;
结尾
以上就是本文的所有内容了,看完本文和上一篇虚拟列表的文章,相信无论是何种长列表各位读者都能轻松拿捏。
上一篇::三种虚拟列表原理与实现
转载自:https://juejin.cn/post/7237856777588310073