React,自定义hook,Intersection Observer API实现图片的懒加载。
之前方法是通过element.getBoundingClientRect()来获取元素距离视口位置然后监听scroll事件(文章链接),判断元素是否进入视口从而来实现懒加载,因为用到了scroll的监听事件,因此频繁触发、调用可能会造成性能问题。这种方法的优点是兼容性较好,但检测方法却极其怪异且不优雅,因此官方就提出了Intersection Observer API,用来高效解决图片懒加载、内容无限滚动、触底加载、两个元素是否相交等场景。
什么是Intersection Observer API ?
IntersectionObserver接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法。其祖先元素或视口被称为根(root)。
当一个 IntersectionObserver对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦 IntersectionObserver被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。
以上出自MDN官方文档描述。
故我们需要观察的元素被称为目标元素(target),设备视口或我们指定的元素视口的边界框我们称它为根元素(root),简称为根 。
IntersectionObserver翻译过来叫做 “交叉观察器”,因为判断元素是否可见的本质就是看目标元素是否与根元素或视口产生了交叉。
构造函数:
IntersectionObserver();
创建一个IntersectionObserver.observe对象,当其监听到目标元素的可见部分(的比例)超过了一个或多个阈值(threshold)时,会执行指定的回调函数。
实例方法:
IntersectionObserver.observe(); 使IntersectionObserver开始监听一个目标元素。
IntersectionObserver.unobserve();使IntersectionObserver停止监听特定目标元素。
IntersectionObserver.disconnect(); 使IntersectionObserver对象停止监听目标。
接下我们进入实践,src目录下添加hook文件夹,创建一个自定义hook组件。
请求接口,拿到数据然后渲染视图,数据遍历时我们将图片url优先给我们<img>标签的自定属性datasrc,等后边判断可见时再赋值给src使其显示出来。
import React, { useEffect, useState } from 'react'
import { getImages } from '../../api'
export default function NewTestpage() {
const [images, setImages] = useState([]);
// 渲染数据接口
const getPageImage = () => {
getImages("query").then(res => {
if (res.status === 200) {
// console.log(res);
setImages(res.data);
}
})
}
useEffect(() => {
getPageImage();
}, [])
return (
<div>
<div className="imgBox" style={{ height: "200vh" }}>
{images.map(item => (
<div key={item.name}>
<img datasrc={item.url} className='imgEle' alt="" style={{ height: "500px", width: "400px", minHeight: "500px", minWidth: "400px" }} />
</div>
))}
</div>
</div>
)
}
然后用Intersection Observer API写我们的自定义hook并导出。
import { useEffect } from 'react'
// 声明一个变量 用来接收observer对象
let observer;
// ele为目标元素 watch依赖项
export default function VisibleImages(ele, watch) {
useEffect(() => {
const nodes = document.querySelectorAll(ele);
// 如果目标不为空
if (nodes && nodes.length) {
// 创建IntersectionObserver对象 entries为IntersectionObserverEntry对像数组
observer = new IntersectionObserver((entries) => {
// console.log(entries);
entries.forEach(item => {
// isIntersecting是一个Boolean值,判断目标元素当前是否可见
if (item.isIntersecting) {
// 获取到我们自定的img属性 赋值给src 然后停止该目标监听
const datasrc = item.target.getAttribute("datasrc");
item.target.src = datasrc;
observer.unobserve(item.target);
}
})
})
// 开始监听目标元素
nodes.forEach(item => {
observer.observe(item);
})
}
// 组件注销时 停止对象监听
return () => {
if (nodes && nodes.length && observer) {
observer.disconnect();
}
}
}, watch)
}
将hook引入到我们需要的组件中,然后传参执行,最终为:
import React, { useEffect, useState } from 'react'
import { getImages } from '../../api'
// 引入自定hook
import VisibleImages from '../../hook/VisibleImages';
export default function NewTestpage() {
const [images, setImages] = useState([]);
const getPageImage = () => {
getImages("query").then(res => {
if (res.status === 200) {
// console.log(res);
setImages(res.data);
}
})
}
// 将监听目标元素类名传入,依赖项为null
VisibleImages(".imgEle",null);
useEffect(() => {
getPageImage();
}, [])
return (
<div>
<div className="imgBox" style={{ height: "200vh" }}>
{images.map(item => (
<div key={item.name}>
<img datasrc={item.url} className='imgEle' alt="" style={{ height: "500px", width: "400px", minHeight: "500px", minWidth: "400px" }} />
</div>
))}
</div>
</div>
)
}
最后,大功告成!