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
三、语法与用法
创建与观察
代码示例
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">▼ 向下滚动</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">⚡</div>
<div class="feature-title">高性能</div>
<div class="feature-desc">浏览器内部优化</div>
</div>
<div class="feature-card scale-in delay-2">
<div class="feature-icon">👁</div>
<div class="feature-title">精确检测</div>
<div class="feature-desc">交叉比例可配置</div>
</div>
<div class="feature-card scale-in delay-3">
<div class="feature-icon">🎨</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>五、浏览器兼容性
六、注意事项与最佳实践
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 可以精确控制触发时机,适用于图片懒加载、无限滚动、曝光统计、滚动动画等场景。使用时注意及时取消不再需要的观察、合理设置阈值和边距,以及处理动态添加的元素。
本文涉及AI创作
内容由AI创作,请仔细甄别