使用 IntersectionObserver API 结合 Vue 的 Hooks 实现图片懒加载
在 Web 应用中,图片懒加载是优化性能的关键技术之一,它能延迟加载非视口内的图片,减少初始页面加载时间。本文将介绍如何利用 Intersection Observer API 和 Vue 3 的组合式 API(Hooks) 实现这一功能。
一、技术背景
-
Intersection Observer API
该 API 可以高效监听目标元素与视口的交叉状态,当元素进入或离开视口时触发回调,避免了传统滚动监听的高性能消耗。 -
Vue 组合式 API
Vue 3 的组合式 API(如ref,onMounted)允许将逻辑封装成可复用的自定义 Hook,提升代码组织性和复用性。
二、实现思路
-
创建自定义 Hook
封装一个useIntersectionObserverHook,用于初始化IntersectionObserver并管理图片加载逻辑。 -
监听元素可见性
当图片进入视口时,将data-src属性替换为真实的src,触发图片加载。 -
优化资源管理
在组件卸载时,断开 Observer 的连接以避免内存泄漏。
三、认识IntersectionObserver API
1. 构造函数
通过 new IntersectionObserver(callback, options) 创建观察器:
-
callback:交叉状态变化时触发的回调函数,接收参数entries(所有被观察元素的交叉状态数组)。 -
options(可选):root:观察的父元素,默认为视口(null表示视口)。rootMargin:扩展或缩小观察区域的边距(类似 CSSmargin语法,如"0px 0px -100px 0px"表示底部向内收缩 100px)。threshold:触发回调的交叉比例阈值(如[0, 0.5, 1]表示元素 0%、50%、100% 可见时触发)。
2. 观察目标元素
通过 observer.observe(element) 开始观察指定元素。
observe的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。
io.observe(elementA);
io.observe(elementB);
3. 停止观察
observer.unobserve(element):停止观察单个元素。observer.disconnect():停止所有观察。
4. 回调函数的参数 entries
每个 entry 对象包含以下关键属性:
entry.target:被观察的 DOM 元素。entry.isIntersecting:元素是否与观察区域交叉(布尔值)。entry.intersectionRatio:元素的可见比例(0~1)。entry.boundingClientRect:元素的位置信息(类似getBoundingClientRect)。
5.小案例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, maximum-scale=1, initial-scale=1, user-scalable=no">
<title>Document</title>
</head>
<style>
body {
margin: 0;
padding: 0;
}
.f {
width: 700px;
height: 500px;
margin: 0 ,auto;
margin-top: 15%;
background-color: brown;
overflow: auto;
}
.child {
width: 200px;
height: 200px;
margin: 0 ,auto;
margin-top: 15%;
background-color: aqua;
}
</style>
<body>
<div class="f">
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
</div>
<script>
// callback函数的参数(entries)是一个数组,每个成员都是一个IntersectionObserverEntry对象
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if(entry.intersectionRatio <= 0) return
console.log(entry.target);
//
observer.unobserve(entry.target);
})
})
let arr = document.querySelectorAll('.f .child');
[...arr].forEach(el => {
observer.observe(el);
});
</script>
</body>
</html>
四、 创建自定义 Hook:useIntersectionObserver
//var io = new IntersectionObserver(callback, option);
function loadImage(target: HTMLImageElement) {
target.src = target.dataset.src ?? '';
}
function callback(entries : IntersectionObserverEntry[], observer: IntersectionObserver) {
// 遍历每个被观察的元素
entries.forEach(entry => {
if (entry.intersectionRatio <= 0) {
return
}
loadImage(entry.target as HTMLImageElement);
// console.log('load image', entry.target);
observer.unobserve(entry.target);//// 加载后取消观察
})
}
const options = {
root: null, // 默认为视窗
rootMargin: '0px', // 在计算交叉度时,扩大或缩小root的边界
threshold: 0 // 当目标元素0%的部分进入视窗时触发回调
}
function createIntersectionObserver() {
// 创建一个 IntersectionObserver 实例,传入回调函数和选项
const observer = new IntersectionObserver(callback, options);
return observer;
}
const observer = createIntersectionObserver();
export function useIntersectionObserver() {
return {
observer
};
}
封装成指令
// directives/lazy.js
import type { DirectiveBinding } from 'vue';
import { useIntersectionObserver } from '@/hooks/useIntersectionObserver';
export default {
mounted(el: HTMLElement, binding: DirectiveBinding) {
// 确保绑定的值是字符串类型,这里假设它是一个图片的URL
if (typeof binding.value === 'string') {
el.dataset.src = binding.value;
}
const { observer }: any = useIntersectionObserver();
observer.observe(el);
},
unmounted(el: any) {
// 当元素卸载时,取消观察
const { observer }: any = useIntersectionObserver();
if (observer) {
observer.unobserve(el);
}
}
};
全局注册
app.directive('lazy', lazyLoadDirective);
五、浏览器兼容性
- 支持所有现代浏览器(Chrome 51+、Firefox 55+、Safari 12.1+、Edge 15+)。
- 不支持 IE,可通过 Polyfill 兼容。