所有废墟上的一切,也都将于结束之后重新开始。 —— 镜·神寂
1. 前言
作为在掘金摸🐟已经很久的一名前端er,今天终于迈出了写文章的步伐,主要用于记录平时工作中或者学习中遇到的一些问题以及笔记,方便以后查阅,当然也希望能够帮助更多可爱的前端er。其实从很早以前就有了这个想法,但是每一次都没有坚持下去,现在从头开始,希望为时未晚。
这篇文章的起源来自于最近的一个产品需求,相爱相杀的产品经理希望在移动端预览转码后的PDF、PPT等文件,一般而言转码后的PDF、PPT等都会生成大量的图片,这时就涉及到了图片懒加载
的实现。在原有的印象中,图片懒加载的实现都是通过监听scroll事件实现:
- 添加自定义属性:给img标签添加
data-src
,值为图片的url,同时不要设置src属性 - 判断目标元素与视口的交叉状态:通过获取元素的getBoundingClientRect属性的top值和页面的clientHeight进行对比,如果top值小于clientHeight,则说明元素出现在可视区域之内
- 设置真实的src:当元素出现在可视区域内时,将真实的图片地址赋值给目标元素的src属性
但是,今天在调研的过程中发现了IntersectionObserver
这个API完全贴合了我的需求(PS:是我太落后了🙈~)。下面开始进入今天的正题吧!
2. IntersectionObserver 简介
IntersectionObserver 接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗 (viewport) 交叉状态的方法。祖先元素与视窗 (viewport) 被称为**根 (root)。——引用自MDN
下面这些情况都需要用到相交检测:
- 图片懒加载——当图片滚动到可见时才进行加载
- 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉
- 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况
- 在用户看见某个区域时执行任务或播放动画
具体的属性以及方法就不赘述了,请直接参考MDN文档
3. 基础用法
// 回调函数
const callback =(entries, observer) => {
entries.forEach(entry => {
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
// 参数配置
const options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
};
// 1. 实例化一个IntersectionObserver对象,并传入相应参数和回调函数
const observer = new IntersectionObserver(callback, options);
// 2. 指定一个目标元素进行观察
const target = document.querySelector('#listItem')
observer.observe(target);
// 3.销毁时,一定要取消观察对象,否则会一直监听
observer.unobserve(target);
以上提到的entry
下每个属性的具体函数,请查阅MDN
4. 图片懒加载的实现
由于项目中我们统一的技术栈为Vue3 + Tsx,所以下面采用tsx的写法来编写。
import { defineComponent, ref, toRefs, PropType, onMounted, onUnmounted } from 'vue';
import styles from './index.module.scss';
import defaultImage from '@/assets/images/defaultImage.png';
export default defineComponent({
props: {
// 图片集合
imagesList: {
type: Array as PropType<string[]>,
required: true,
},
},
setup(props) {
const { imagesList } = toRefs(props);
const observer = ref<any>(null);
const root = ref<HTMLElement | null>(null);
// 监视器的回调函数
const handlerObserve = (entries: any) => {
entries.forEach(({ isIntersecting, target }: any) => {
if (isIntersecting) {
const targetImg = target.children[0];
targetImg.src = targetImg.dataset.src;
// 修改过src属性之后,即可移除data-src属性并且取消监视
targetImg.removeAttribute('data-src');
observer.value.unobserve(target);
}
});
};
// 针对图片容器添加监听器
const addObserve = () => {
const list = document.querySelectorAll('.image-item-observe') as NodeListOf<Element>;
list.forEach((item: Element) => {
observer.value.observe(item);
});
};
// 初始化监视器
const initObserve = () => {
observer.value = new IntersectionObserver(handlerObserve, {
root: root.value,
rootMargin: '0px 0px 500px 0px', // 监视区向下拓展500px
});
addObserve();
};
onMounted(async () => {
initObserve();
});
onUnmounted(() => {
// 关闭观察器
observer.value.disconnect();
});
return () => (
<div ref={root} class={styles['image-list-container']}>
{
imagesList.value.map((item: string, index: number) => (
<div class={`${styles['image-item-wrapper']} image-item-observe`}>
{/* 设置一个默认的缺省图,避免在加载过程中出现白屏的现象 */}
<img
class={styles['image-item']}
src={defaultImage}
data-src={item}
key={index}
width="100%"
/>
</div>
))
}
</div>
);
},
});
具体效果如下图所示(200张图的加载,整体看起来还是👌的):
5. 结语
第一次写文章,算是迈出了第一步,希望以后能够坚持下去。就我自身而言,写文章主要是为了记录在工作中遇到的问题,以便后期的回顾和查阅。不足之处,还请大家包涵,以后请多多指教。