IntersectionObserver
作用: 观察一个元素是否在视口可见
用途:无限滚动、图片懒加载、控制动画/视频执行(性能优化)
使用场景:判断某个元素是否进入了视口,传统的实现方法是,监听scroll事件,调用目标元素的getBoundingClientRect()方法,得到对于视口左上角的位置,再去判断是否在视口之内,缺点是由于scroll事件频发,计算量很大,容易造成性能问题
IntersectionObserver浏览器提供的原生方法,可以自动观察元素是否可见
cloud.tencent.com/developer/a…
图片懒加载
原理:
-
图片是否进入可视区域 intersectionRatio
-
将图片的具体地址暂存到data-src属性
-
图片进入可视区后,将img标签的data-src属性赋值给src属性
// ref存储可变值
const observerRef = useRef(new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const { target, intersectionRatio } = entry;
// target 目标dom
// intersectionRatio 相交区域和目标元素的比例值,进入可视区域大于0 否则等于0
// isIntersecting 是否进入可视区域
if (intersectionRatio > 0) {
target.src = target.dataset.src; // 替换目标元素的src路径
target.onload = () => {
target.style.opacity = '1'
}
observerRef.current.unobserve(target); // 关闭监听目标元素
}
})
}))
useEffect(() => {
// // 创建观察者实例
// let observer = new IntersectionObserver((entries, observer) => {
// entries.forEach(entry => {
// let target = entry.target; // 目标元素
// // 当元素出现在视图中
// if (entry.intersectionRatio > 0) {
// target.src = target.dataset.src; // 替换元素的src路径
// observer.unobserve(target) // 关闭监听目标元素
// }
// })
// })
Array.from(document.querySelectorAll('img')).forEach(image => {
observerRef.current.observe(image); // 监听每一个元素
})
return () => {
observerRef.current.disconnect(); // 取消所有监听dom
}
}, []);
列表下拉加载
原理: loading是否进入可视区域: 如果进入可视区域 则加载下一页
实际上除了接口错误,和请求回来的数据长度小于设定的pageSize外,loading一直存在,只不过被请求回来的数据挤下去了
html结构:
<div className="resume-list-wrapper">
{renderList()}
{
hasMore ? (
<div className="loader" ref={loaderRef} style={{ height: hasLoadOneTime ? '100%' : 'calc(100vh - 100px)' }}>
<Icon type="loading" />数据加载中...
</div>
) : (
<div className="no-more-data">{resumeDataList.length ? '没有更多数据了~' : null}</div>
)
}
{ScrollTopIcon}
</div>
const loadMore = useCallback((pageNumber) => {
if (hasMore) {
run({ pageNo: pageNumber, pageSize: 50 }).then((res) => {
setHasLoadOneTime(true);
if (res && res.list) {
if (res.list.length < 50) setHasMore(false);
setResumeDataList((preList) => [...preList, ...res.list]);
}
});
}
}, [run, hasMore]);
useInfiniteScroll({ loaderRef, loadMore, pageNo: 1 });
自定义 useInfiniteScroll Hooks
import { useEffect, useRef } from 'react';
// 请务必确保首次加载能将首屏占满,并且每次加载数据后能将屏幕铺满,loader展示在隐藏处
// 用户需自己根据接口等状态,维护loaderRef的展示,内部会根据loaderRef判断是否需要去执行loadMore函数
interface ITypes {
loaderRef: { current: undefined | HTMLElement };
loadMore: (pageNo: number) => void;
pageNo: number;
}
export default function useInfiniteScroll({ loaderRef, loadMore, pageNo }: ITypes) {
// 这里使用pageNoRef的原因是缓存ref,这样不会频繁调用一下的useeffect函数,就不会重复创建IntersectionObserver了
const pageNoRef = useRef(pageNo);
useEffect(() => {
let ob: IntersectionObserver;
// 只有当loaderRef.current存在时,才有必要创建observer。如果loader不存在了,就没必要ob了
// 外部组件需要自己设置hasMore变量,自行控制loader组件的展示
if (loaderRef.current) {
ob = new IntersectionObserver((entries) => {
const entry = entries[0];
// isIntersecting 表示目标元素loaderRef 是否在可视区,在可视区 再去加载
if (entry.isIntersecting) {
loadMore(pageNoRef.current);
pageNoRef.current += 1;
}
});
ob.observe(loaderRef.current); // 监听元素
}
return () => {
ob && ob.disconnect(); // 组件卸载时 取消监听
};
}, [loadMore, loaderRef]);
}
自定义Hooks
在项目中使用自定义useScrollTop
当页面滚动的距离大于屏幕的高度时,出现圆形按钮,点击可滚动到页面最上方
import React, { useState, useEffect } from 'react';
import { Icon } from '@ss/mtd-react';
import debounce from 'lodash/debounce';
const useScrollTop = ({ className = 'float-icon' }) => {
const [iconVisible, setIconVisible] = useState(false);
useEffect(() => {
const callback = debounce((e) => {
// clientHeight 屏幕高度 scrollTop 距离顶部距离 scrollHeight 滚动视图整体高度
const { clientHeight = 0, scrollTop = 0 } = e?.target?.scrollingElement;
if (scrollTop > clientHeight) { setIconVisible(true); } else {
setIconVisible(false);
}
}, 200);
document.addEventListener('scroll', callback);
return () => {
document.removeEventListener('scroll', callback);
};
}, []);
if (iconVisible) {
return (
<div
className={className}
onClick={() => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
}}
><Icon type="top" />
</div>
);
}
return null;
};
export default useScrollTop;
const ScrollTopIcon = useScrollTop({});