IntersectionObserver终极懒加载指南

421 阅读3分钟

使用 IntersectionObserver API 结合 Vue 的 Hooks 实现图片懒加载

在 Web 应用中,图片懒加载是优化性能的关键技术之一,它能延迟加载非视口内的图片,减少初始页面加载时间。本文将介绍如何利用 Intersection Observer APIVue 3 的组合式 API(Hooks) 实现这一功能。


一、技术背景

  1. Intersection Observer API
    该 API 可以高效监听目标元素与视口的交叉状态,当元素进入或离开视口时触发回调,避免了传统滚动监听的高性能消耗。

  2. Vue 组合式 API
    Vue 3 的组合式 API(如 ref, onMounted)允许将逻辑封装成可复用的自定义 Hook,提升代码组织性和复用性。


二、实现思路

  1. 创建自定义 Hook
    封装一个 useIntersectionObserver Hook,用于初始化 IntersectionObserver 并管理图片加载逻辑。

  2. 监听元素可见性
    当图片进入视口时,将 data-src 属性替换为真实的 src,触发图片加载。

  3. 优化资源管理
    在组件卸载时,断开 Observer 的连接以避免内存泄漏。


三、认识IntersectionObserver API

1. 构造函数

通过 new IntersectionObserver(callback, options) 创建观察器:

  • callback:交叉状态变化时触发的回调函数,接收参数 entries(所有被观察元素的交叉状态数组)。

  • options(可选):

    • root:观察的父元素,默认为视口(null 表示视口)。
    • rootMargin:扩展或缩小观察区域的边距(类似 CSS margin 语法,如 "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)。

image.png

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>

screenshots.gif

四、 创建自定义 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 兼容。

参考:www.ruanyifeng.com/blog/2016/1…