注册
web

1.6kB 搞定懒加载、无限滚动、精准曝光

上文提到有很多类库在用 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;

如上例,可以了解到以下几点知识




  1. new 一个 IntersectionObserver 对象,下称 io,需传入一个函数,下称 callbackcallback 的入参 entries 代表了正在被观察的元素数组,数组的每一项都拥有属性 intersectionRatio ,代表了被观察的元素与根元素可视区域的交叉比例,。




  2. 使用 ioobserve 方法来添加你想观察的元素,可以多次调用添加多个,




  3. 使用 iodisconnect 方法来销毁观测




使用上方的代码,可以完成对元素最基本的观察。如上方 gif 操作,在控制台可得到以下结果 ,




  • 进入页面时,callback 被调用了一次:intersectionRatio 为 0
  • 滚动到可视区,再次调用:intersectionRatio > 0
  • 滚动出可视区,再次调用:intersectionRatio 为 0
  • 滚动到可视区,再次调用:intersectionRatio > 0

而懒加载,无限滚动,精准曝光是如何基于这个 api 去实现的呢,如果直接去写,当然也能实现,但是会有些繁琐,下面引入本篇文章的主角:react-intersection-observer 类库,先看看这个类库的基本介绍吧。


2. react-intersection-observer 基础介绍


这个类库在全局维护了一个 IntersectionObserver 实例(如果只有一个根元素,那全局仅有一个实例,实际上代码中维护了一个实例的 Map,此处简单表述),并提供了一个名为 useInViewhooks 方便我们了解到被观测的元素的观测状态。与上面相同的例子,他的写法如下:


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,需要指定为该元素)




  • rootMarginrootmargin




    • 同 css 上右下左写法,需要带单位,可简写('200px 0px')




    • 正值代表观察区域增大,负值代表观察区域缩小






在图片懒加载中,因为通常不可能等到元素被滚动到了可视区域,才开始加载图片,所以需要调整 rootMargin ,可以写为,rootMargin: `0px 0px ${window.innerHeight}px 0px ,这样图片可以提前一屏进行加载。


同样懒加载不需要不可见的时候回收掉相应的 dom ,所以只需要触发一次,设置 triggerOncetrue 即可。


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 会被触发,通过对 inViewtrue 的判断后,加载后续的数据。同理,真正的无限滚动也需要提前加载(在观察内写异步请求等),也可以设置相应的 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,默认值 0
  • delay: 延迟通知元素露出(如果延迟后元素未达标,则不会触发onChange),取值单位毫秒,非必填。

使用上面两个属性,就可以轻松实现业务需求。


3.4. 官方示例


示例,官方示例中还有很多对属性的应用,比如 threshold 传入数组,skiptrack-visibility ,大家可自行体验。


总结


以上就是对 IntersectionObserver 以及 react-intersection-observer 的介绍了,希望能对大家有所帮助,文中录制的示例完整项目可以从此处获取。


作者:windyrain
来源:juejin.cn/post/7220309530910851130

0 个评论

要回复文章请先登录注册