pin_drop当前位置:知识文库 ❯ 图文

HTML5 API:HTML5 Intersection Observ - 完整教程与代码示例

一、教程简介

Intersection Observer API 提供了异步检测元素与视口(或指定根元素)交叉状态的能力,可以高效地判断元素是否进入或离开可视区域。相比传统的滚动事件监听+getBoundingClientRect 方案,Intersection Observer 性能更优(由浏览器内部优化)、API 更简洁,广泛应用于图片懒加载、无限滚动、曝光统计、动画触发等场景。


二、核心概念

交叉比例(Intersection Ratio)

代码示例

元素完全不可见 → intersectionRatio = 0
元素部分可见 → intersectionRatio = 0 ~ 1
元素完全可见 → intersectionRatio = 1

阈值(Threshold)

指定交叉比例达到何值时触发回调:

代码示例

threshold: 0        // 任何部分进入/离开即触发
threshold: 0.5      // 50% 可见时触发
threshold: 1.0      // 完全可见时触发
threshold: [0, 0.25, 0.5, 0.75, 1.0]  // 多个阈值

根边距(Root Margin)

类似于 CSS margin,用于扩展或收缩根元素的判定区域:

代码示例

rootMargin: '0px'           // 默认,无扩展
rootMargin: '100px'         // 四边各扩展 100px
rootMargin: '50px 0px'      // 上下扩展 50px
rootMargin: '0px 0px 200px 0px'  // 底部扩展 200px(提前触发)

IntersectionObserverEntry

属性 说明
target 被观察的目标元素
isIntersecting 是否正在交叉(进入视口)
intersectionRatio 交叉比例(0~1)
intersectionRect 交叉区域的矩形信息
boundingClientRect 目标元素的边界矩形
rootBounds 根元素的边界矩形
time 变化发生的时间戳

三、语法与用法

创建与观察

代码示例

const observer = new IntersectionObserver(callback, options);

observer.observe(element);     // 开始观察
observer.unobserve(element);   // 停止观察
observer.disconnect();         // 停止所有观察
observer.takeRecords();        // 获取未处理的记录

回调函数

代码示例

const callback = function(entries, observer) {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            console.log('元素进入视口:', entry.target);
            console.log('交叉比例:', entry.intersectionRatio);
        } else {
            console.log('元素离开视口:', entry.target);
        }
    });
};

四、代码示例

示例一:图片懒加载

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Intersection Observer - 图片懒加载</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #f5f7fa;
            min-height: 100vh;
        }
        .container { max-width: 640px; margin: 0 auto; padding: 20px; }
        h1 { font-size: 24px; color: #2d3436; margin-bottom: 8px; }
        .desc { color: #636e72; font-size: 14px; margin-bottom: 24px; }
        .stats {
            position: sticky;
            top: 0;
            background: #fff;
            padding: 12px 16px;
            border-radius: 10px;
            margin-bottom: 16px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.08);
            display: flex;
            gap: 20px;
            font-size: 13px;
            z-index: 10;
        }
        .stat-item span { font-weight: 700; color: #0984e3; }
        .image-card {
            background: #fff;
            border-radius: 12px;
            margin-bottom: 16px;
            overflow: hidden;
            box-shadow: 0 2px 8px rgba(0,0,0,0.06);
        }
        .image-placeholder {
            height: 200px;
            background: linear-gradient(135deg, #dfe6e9, #b2bec3);
            display: flex;
            align-items: center;
            justify-content: center;
            color: #636e72;
            font-size: 14px;
            transition: opacity 0.5s;
        }
        .image-placeholder.loaded {
            background: none;
            height: auto;
        }
        .image-placeholder img {
            width: 100%;
            display: block;
        }
        .image-info {
            padding: 12px 16px;
        }
        .image-title { font-size: 14px; font-weight: 600; color: #2d3436; }
        .image-meta { font-size: 12px; color: #636e72; margin-top: 4px; }
        .load-indicator {
            text-align: center;
            padding: 20px;
            color: #636e72;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>图片懒加载</h1>
        <p class="desc">滚动页面,图片进入视口时才加载。观察加载状态统计。</p>

        <div class="stats">
            <div class="stat-item">总图片: <span id="totalCount">0</span></div>
            <div class="stat-item">已加载: <span id="loadedCount">0</span></div>
            <div class="stat-item">待加载: <span id="pendingCount">0</span></div>
        </div>

        <div id="imageList"></div>
        <div class="load-indicator" id="loadMore">向下滚动加载更多...</div>
    </div>

    <script>
        const imageData = [
            { id: 1, title: '城市天际线', colors: ['#667eea', '#764ba2'], desc: '繁华都市' },
            { id: 2, title: '海洋日落', colors: ['#f093fb', '#f5576c'], desc: '金色余晖' },
            { id: 3, title: '森林小径', colors: ['#43e97b', '#38f9d7'], desc: '绿意盎然' },
            { id: 4, title: '雪山之巅', colors: ['#a1c4fd', '#c2e9fb'], desc: '银装素裹' },
            { id: 5, title: '沙漠星空', colors: ['#f6d365', '#fda085'], desc: '大漠孤烟' },
            { id: 6, title: '极光之夜', colors: ['#4facfe', '#00f2fe'], desc: '北极光' },
            { id: 7, title: '樱花盛开', colors: ['#ffecd2', '#fcb69f'], desc: '春日物语' },
            { id: 8, title: '深蓝海洋', colors: ['#667eea', '#764ba2'], desc: '碧波万顷' },
            { id: 9, title: '秋叶飘零', colors: ['#f5576c', '#ff9a9e'], desc: '金秋时节' },
            { id: 10, title: '雨后彩虹', colors: ['#43e97b', '#38f9d7'], desc: '七彩斑斓' },
            { id: 11, title: '古城夜景', colors: ['#f6d365', '#fda085'], desc: '灯火辉煌' },
            { id: 12, title: '溪流瀑布', colors: ['#4facfe', '#00f2fe'], desc: '飞流直下' },
        ];

        let loadedCount = 0;
        let totalCount = imageData.length;

        function renderImages() {
            const list = document.getElementById('imageList');
            list.innerHTML = imageData.map(function (img) {
                return '<div class="image-card"><div class="image-placeholder" data-src="' + img.id + '" id="img-' + img.id + '"><span>图片加载中...</span></div><div class="image-info"><div class="image-title">' + img.title + '</div><div class="image-meta">' + img.desc + '</div></div></div>';
            }).join('');

            document.getElementById('totalCount').textContent = totalCount;
            document.getElementById('pendingCount').textContent = totalCount;

            const observer = new IntersectionObserver(function (entries) {
                entries.forEach(function (entry) {
                    if (entry.isIntersecting) {
                        const placeholder = entry.target;
                        const imgId = placeholder.dataset.src;
                        loadImages(placeholder, imgId);
                        observer.unobserve(placeholder);
                    }
                });
            }, {
                rootMargin: '100px'
            });

            document.querySelectorAll('.image-placeholder').forEach(function (el) {
                observer.observe(el);
            });
        }

        function loadImages(placeholder, imgId) {
            const img = imageData.find(function (i) { return i.id == imgId; });
            if (!img) return;

            setTimeout(function () {
                placeholder.innerHTML = '';
                placeholder.style.background = 'linear-gradient(135deg, ' + img.colors[0] + ', ' + img.colors[1] + ')';
                placeholder.classList.add('loaded');

                const badge = document.createElement('div');
                badge.style.cssText = 'position:absolute;top:8px;right:8px;background:rgba(0,184,148,0.9);color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;';
                badge.textContent = '已加载';
                placeholder.style.position = 'relative';
                placeholder.appendChild(badge);

                loadedCount++;
                document.getElementById('loadedCount').textContent = loadedCount;
                document.getElementById('pendingCount').textContent = totalCount - loadedCount;
            }, 300 + Math.random() * 700);
        }

        renderImages();
    </script>
</body>
</html>

示例二:滚动动画触发

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Intersection Observer - 滚动动画</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #0a0a23;
            color: #e0e0e0;
        }
        .hero {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            text-align: center;
            padding: 40px 20px;
        }
        .hero h1 { font-size: 48px; color: #fff; margin-bottom: 16px; }
        .hero p { font-size: 18px; color: #7a8ba0; max-width: 500px; }
        .scroll-hint { margin-top: 40px; animation: bounce 2s infinite; color: #00cec9; }
        @keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(10px); } }
        .section {
            min-height: 80vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 60px 20px;
        }
        .section-content {
            max-width: 600px;
            text-align: center;
        }
        .section-content h2 {
            font-size: 32px;
            color: #fff;
            margin-bottom: 16px;
        }
        .section-content p {
            font-size: 16px;
            color: #7a8ba0;
            line-height: 1.8;
        }
        .fade-up {
            opacity: 0;
            transform: translateY(60px);
            transition: opacity 0.8s ease, transform 0.8s ease;
        }
        .fade-up.visible {
            opacity: 1;
            transform: translateY(0);
        }
        .fade-left {
            opacity: 0;
            transform: translateX(-60px);
            transition: opacity 0.8s ease, transform 0.8s ease;
        }
        .fade-left.visible {
            opacity: 1;
            transform: translateX(0);
        }
        .fade-right {
            opacity: 0;
            transform: translateX(60px);
            transition: opacity 0.8s ease, transform 0.8s ease;
        }
        .fade-right.visible {
            opacity: 1;
            transform: translateX(0);
        }
        .scale-in {
            opacity: 0;
            transform: scale(0.8);
            transition: opacity 0.6s ease, transform 0.6s ease;
        }
        .scale-in.visible {
            opacity: 1;
            transform: scale(1);
        }
        .delay-1 { transition-delay: 0.1s; }
        .delay-2 { transition-delay: 0.2s; }
        .delay-3 { transition-delay: 0.3s; }
        .features {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 20px;
            margin-top: 32px;
        }
        .feature-card {
            background: #16213e;
            border-radius: 12px;
            padding: 24px;
            border: 1px solid #1a3a5c;
        }
        .feature-icon { font-size: 32px; margin-bottom: 12px; }
        .feature-title { font-size: 16px; color: #fff; margin-bottom: 8px; }
        .feature-desc { font-size: 13px; color: #7a8ba0; }
    </style>
</head>
<body>
    <div class="hero">
        <h1 class="scale-in">滚动动画演示</h1>
        <p class="fade-up delay-1">向下滚动,元素进入视口时触发动画效果</p>
        <div class="scroll-hint fade-up delay-2">&#x25BC; 向下滚动</div>
    </div>

    <div class="section">
        <div class="section-content">
            <h2 class="fade-up">淡入上移动画</h2>
            <p class="fade-up delay-1">当元素进入视口时,从下方淡入显示。这是最常见的滚动动画效果,适用于标题、段落等内容。</p>
        </div>
    </div>

    <div class="section">
        <div class="section-content">
            <h2 class="fade-left">从左滑入</h2>
            <p class="fade-left delay-1">元素从左侧滑入视口,适合图文混排的左侧内容。</p>
        </div>
    </div>

    <div class="section">
        <div class="section-content">
            <h2 class="fade-right">从右滑入</h2>
            <p class="fade-right delay-1">元素从右侧滑入视口,与从左滑入配合使用可创造对称效果。</p>
        </div>
    </div>

    <div class="section">
        <div class="section-content">
            <h2 class="scale-in">缩放进入</h2>
            <div class="features">
                <div class="feature-card scale-in delay-1">
                    <div class="feature-icon">&#9889;</div>
                    <div class="feature-title">高性能</div>
                    <div class="feature-desc">浏览器内部优化</div>
                </div>
                <div class="feature-card scale-in delay-2">
                    <div class="feature-icon">&#128065;</div>
                    <div class="feature-title">精确检测</div>
                    <div class="feature-desc">交叉比例可配置</div>
                </div>
                <div class="feature-card scale-in delay-3">
                    <div class="feature-icon">&#127912;</div>
                    <div class="feature-title">灵活配置</div>
                    <div class="feature-desc">阈值和边距可调</div>
                </div>
            </div>
        </div>
    </div>

    <script>
        const observer = new IntersectionObserver(function (entries) {
            entries.forEach(function (entry) {
                if (entry.isIntersecting) {
                    entry.target.classList.add('visible');
                }
            });
        }, {
            threshold: 0.15
        });

        document.querySelectorAll('.fade-up, .fade-left, .fade-right, .scale-in').forEach(function (el) {
            observer.observe(el);
        });
    </script>
</body>
</html>

五、浏览器兼容性

浏览器 支持版本
Chrome 51+
Firefox 55+
Safari 12.1+
Edge 15+
Opera 38+
IE 不支持
iOS Safari 12.2+

六、注意事项与最佳实践

1. 及时取消观察

代码示例

// 元素进入视口后不再需要观察
observer.observe(element);
// 在回调中
if (entry.isIntersecting) {
    // 处理逻辑
    observer.unobserve(entry.target); // 只需触发一次
}

2. 合理设置阈值

代码示例

// 懒加载:提前触发
{ rootMargin: '200px 0px', threshold: 0 }

// 曝光统计:精确触发
{ threshold: 0.5 }  // 50% 可见才算曝光

// 动画触发
{ threshold: 0.1 }  // 10% 可见即触发

3. 性能优化

代码示例

// 避免观察过多元素
// 不推荐:观察 10000 个元素
// 推荐:虚拟列表 + 只观察可视区域附近的元素

七、代码规范示例

代码示例

class LazyLoader {
    constructor(selector, options = {}) {
        this.selector = selector;
        this.options = {
            rootMargin: options.rootMargin || '100px',
            threshold: options.threshold || 0,
            onLoad: options.onLoad || function() {},
            once: options.once !== false
        };
        this.observer = null;
    }

    start() {
        this.observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    this.options.onLoad(entry.target);
                    if (this.options.once) {
                        this.observer.unobserve(entry.target);
                    }
                }
            });
        }, {
            rootMargin: this.options.rootMargin,
            threshold: this.options.threshold
        });

        document.querySelectorAll(this.selector).forEach(el => {
            this.observer.observe(el);
        });
    }

    stop() {
        this.observer?.disconnect();
    }

    refresh() {
        this.stop();
        this.start();
    }
}

// 使用
const loader = new LazyLoader('.lazy-image', {
    rootMargin: '200px',
    onLoad: (el) => {
        el.src = el.dataset.src;
        el.classList.add('loaded');
    }
});
loader.start();

八、常见问题与解决方案

常见问题

动态添加的元素不被观察?

添加新元素后重新调用 observer.observe(newElement),或使用 Mutation Observer 配合。

iOS 上 rootMargin 不生效?

iOS Safari 早期版本对 rootMargin 支持有限,升级到 12.2+ 或使用较小的 margin 值。

如何实现无限滚动?

使用哨兵元素配合 Intersection Observer,当哨兵进入视口时加载更多内容:const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) loadMoreData(); }); observer.observe(sentinel);


九、总结

Intersection Observer 是检测元素可见性的高效方案,相比滚动事件+getBoundingClientRect 性能更优、代码更简洁。通过配置 threshold 和 rootMargin 可以精确控制触发时机,适用于图片懒加载、无限滚动、曝光统计、滚动动画等场景。使用时注意及时取消不再需要的观察、合理设置阈值和边距,以及处理动态添加的元素。

标签: 可见性检测 IntersectionObserver 图片懒加载 滚动动画 HTML5 API

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: HTML5 API:HTML5 Mutation Observer - 完整教程与代码示例 下一篇: HTML5 API:HTML5 Resize Observer - 完整教程与代码示例

poll相关推荐