上文提到有很多类库在用 IntersectionObserver
实现懒加载,但更精准的描述是,IntersectionObserver
提供了一种异步观察目标元素与根元素(窗口或指定父元素)的交叉状态的能力,这项能力不仅能用来做懒加载,还可以提供无限滚动,精准曝光的功能。
1. IntersectionObserver 基础介绍
不管我们使用哪个类库,都需要了解 IntersectionObserver 的基本原理,下面是一个简单的例子
import React, { useEffect } from "react";
import "./page.css";
const Page1 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
useEffect(() => {
const io = new IntersectionObserver((entries) => {
console.log(entries[0].intersectionRatio);
});
const footer = document.querySelector(".footer");
if (footer) {
io.observe(footer);
}
return () => {
io.disconnect();
};
}, []);
return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer">被观察的元素</div>
</div>
);
};
export default Page1;
如上例,可以了解到以下几点知识
new
一个IntersectionObserver
对象,下称io
,需传入一个函数,下称callback
,callback
的入参entries
代表了正在被观察的元素数组,数组的每一项都拥有属性intersectionRatio
,代表了被观察的元素与根元素可视区域的交叉比例,。
使用
io
的observe
方法来添加你想观察的元素,可以多次调用添加多个,
使用
io
的disconnect
方法来销毁观测
使用上方的代码,可以完成对元素最基本的观察。如上方 gif 操作,在控制台可得到以下结果 ,
- 进入页面时,
callback
被调用了一次:intersectionRatio
为 0 - 滚动到可视区,再次调用:
intersectionRatio
> 0 - 滚动出可视区,再次调用:
intersectionRatio
为 0 - 滚动到可视区,再次调用:
intersectionRatio
> 0
而懒加载,无限滚动,精准曝光是如何基于这个 api
去实现的呢,如果直接去写,当然也能实现,但是会有些繁琐,下面引入本篇文章的主角:react-intersection-observer
类库,先看看这个类库的基本介绍吧。
2. react-intersection-observer 基础介绍
这个类库在全局维护了一个 IntersectionObserver
实例(如果只有一个根元素,那全局仅有一个实例,实际上代码中维护了一个实例的 Map,此处简单表述),并提供了一个名为 useInView
的 hooks
方便我们了解到被观测的元素的观测状态。与上面相同的例子,他的写法如下:
import React, { useEffect } from "react";
import { useInView } from 'react-intersection-observer';
import "./page.css";
const Page2 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const { ref } = useInView({
onChange: (inView, entry) => {
console.log(entry.intersectionRatio);
}
});
return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer" ref={ref}>被观察的元素</div>
</div>
);
};
export default Page2;
如上例,使用更少的代码,就实现了相同的功能,而且带来了一些好处
- 不用自己维护
IntersectionObserver
实例,既不用关心创建,也不用关心销毁 - 不用控制被观察的元素到底是
entries
内的第几个,观察事件都会在相应绑定的onChange
中进行回调
以上仅为基本使用,实战中需求是更为复杂的,所以这个类库也提供了一系列属性,方便大家的使用:
利用上面这些配置项,我们可以实现以下功能
3. 实战用例
3.1. 懒加载
import React from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";
interface Props {
width: number;
height: number;
src: string;
}
const LazyImage = ({ width, height, src, ...rest }: Props) => {
const { ref, inView } = useInView({
triggerOnce: true,
root: document.querySelector('.scroll-container'),
rootMargin: `0px 0px ${window.innerHeight}px 0px`,
onChange: (inView, entry) => {
console.log('info', inView, entry.intersectionRatio);
}
});
return (
<div
ref={ref}
style={{
position: "relative",
paddingBottom: `${(height / width) * 100}%`,
background: "#2a4b7a",
}}
>
{inView ? (
<img
{...rest}
src={src}
width={width}
height={height}
style={{ position: "absolute", width: "100%", height: "100%", left: 0, top: 0 }}
/>
) : null}
</div>
);
};
const Page3 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<LazyImage width={750} height={200} src={"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e4acf97e7dc944bf8ad5719b2b42f026~tplv-k3u1fbpfcp-watermark.image?"} />
</div>
);
};
export default Page3;
懒加载中我们需要用到几个额外的属性:
triggerOnce
:只触发一次
root
:默认为文档视口(如果被观察的元素,父/祖元素中有 overflow:scroll
,需要指定为该元素)
rootMargin
:root
的margin
同 css 上右下左写法,需要带单位,可简写('200px 0px')
正值代表观察区域增大,负值代表观察区域缩小
在图片懒加载中,因为通常不可能等到元素被滚动到了可视区域,才开始加载图片,所以需要调整 rootMargin
,可以写为,rootMargin: `0px 0px ${window.innerHeight}px 0px
,这样图片可以提前一屏进行加载。
同样懒加载不需要不可见的时候回收掉相应的 dom
,所以只需要触发一次,设置 triggerOnce
为 true
即可。
3.2. 无限滚动
import React, { useState } from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";
const Page4 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const [datas, setDatas] = useState([1, 1, 1]);
const { ref } = useInView({
onChange: (inView, entry) => {
console.log("inView", inView);
if (inView) {
setDatas((prevDatas) => [...prevDatas, ...new Array(3).fill(1)]);
}
},
});
return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
{datas.map((item, index) => {
return (
<div key={index + 1} className="placeholder">
第{index + 1}个元素
</div>
);
})}
<div className="load-more" ref={ref}></div>
</div>
);
};
export default Page4;
无限滚动主要依赖在 onChange
中对 inView
进行判断,我们可以添加一个高度为0的元素,名为 load-more
,当页面滚动到最下方时,该元素的 onChange
会被触发,通过对 inView
为 true
的判断后,加载后续的数据。同理,真正的无限滚动也需要提前加载(在观察内写异步请求等),也可以设置相应的 rootMargin
,让无限滚动更丝滑。
3.3. 精准曝光
import React from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";
const Page5 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const { ref } = useInView({
threshold: 0.5,
delay: 500,
onChange: (inView, entry) => {
if (inView) {
console.log("元素需要上报曝光事件", entry.intersectionRatio);
}
},
});
return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer" ref={ref}>
需要精准曝光的元素
</div>
</div>
);
};
export default Page5;
精准曝光也是很常见的业务需求,通常此类需求会要求元素的露出比例和最小停留时长。
- 对露出比例要求的原因:因为有可能元素的有效信息并未展示,只是露出了一点点头,一般业务上会要求露出比例大于一半。
- 对停留时长要求的原因:有可能用户快速划过,比如小说看到了很啰嗦的章节快速滑动,直接看后面结果,如果不加停留时长,中间快速滑动的区域也会曝光,与实际想要的不符。
类库恰好提供了下面两个属性方便大家的使用
threshold
: 观察元素露出比例,取值范围 0~1,默认值 0delay
: 延迟通知元素露出(如果延迟后元素未达标,则不会触发onChange),取值单位毫秒,非必填。
使用上面两个属性,就可以轻松实现业务需求。
3.4. 官方示例
示例,官方示例中还有很多对属性的应用,比如 threshold
传入数组,skip
,track-visibility
,大家可自行体验。
总结
以上就是对 IntersectionObserver
以及 react-intersection-observer
的介绍了,希望能对大家有所帮助,文中录制的示例完整项目可以从此处获取。
来源:juejin.cn/post/7220309530910851130