注册
web

🔥 滚动监听写到手抽筋?IntersectionObserver让你躺平实现懒加载


🎯 学习目标:掌握IntersectionObserver API的核心用法,解决滚动监听性能问题,实现高效的懒加载和可见性检测


📊 难度等级:中级

🏷️ 技术标签#IntersectionObserver #懒加载 #性能优化 #滚动监听

⏱️ 阅读时间:约8分钟





🌟 引言


在日常的前端开发中,你是否遇到过这样的困扰:



  • 滚动监听卡顿:addEventListener('scroll')写多了页面卡得像PPT
  • 懒加载复杂:图片懒加载逻辑复杂,还要手动计算元素位置
  • 无限滚动性能差:数据越来越多,滚动越来越卡
  • 可见性检测麻烦:想知道元素是否进入视口,代码写得头疼

今天分享5个IntersectionObserver的实用场景,让你的滚动监听更加丝滑,告别性能焦虑!




💡 核心技巧详解


1. 图片懒加载:告别手动计算位置的痛苦


🔍 应用场景


当页面有大量图片时,一次性加载所有图片会严重影响页面性能,需要实现图片懒加载。


❌ 常见问题


传统的滚动监听方式性能差,需要频繁计算元素位置。


// ❌ 传统滚动监听写法
window.addEventListener('scroll', () => {
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
});
});

✅ 推荐方案


使用IntersectionObserver实现高性能的图片懒加载。


/**
* 创建图片懒加载观察器
* @description 使用IntersectionObserver实现高性能图片懒加载
* @param {string} selector - 图片选择器
* @param {Object} options - 观察器配置选项
* @returns {IntersectionObserver} 观察器实例
*/

const createImageLazyLoader = (selector = 'img[data-src]', options = {}) => {
// 推荐写法:使用IntersectionObserver
const defaultOptions = {
root: null, // 使用视口作为根元素
rootMargin: '50px', // 提前50px开始加载
threshold: 0.1 // 元素10%可见时触发
};

const config = { ...defaultOptions, ...options };

const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// 加载图片
img.src = img.dataset.src;
img.removeAttribute('data-src');
// 停止观察已加载的图片
observer.unobserve(img);
}
});
}, config);

// 观察所有待加载的图片
document.querySelectorAll(selector).forEach(img => {
observer.observe(img);
});

return observer;
};

💡 核心要点



  • rootMargin:提前加载,避免用户看到空白
  • threshold:设置合适的触发阈值
  • unobserve:加载完成后停止观察,释放资源

🎯 实际应用


在Vue3项目中的完整应用示例:


<template>
<div class="image-gallery">
<img
v-for="(image, index) in images"
:key="index"
:data-src="image.url"
:alt="image.alt"
class="lazy-image"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200'%3E%3Crect width='100%25' height='100%25' fill='%23f0f0f0'/%3E%3C/svg%3E"
/>
</div>
</template>

<script setup>
import { onMounted, onUnmounted } from 'vue';

let observer = null;

onMounted(() => {
observer = createImageLazyLoader('.lazy-image');
});

onUnmounted(() => {
observer?.disconnect();
});
</script>



2. 无限滚动:数据加载的性能优化


🔍 应用场景


实现无限滚动列表,当用户滚动到底部时自动加载更多数据。


❌ 常见问题


传统方式需要监听滚动事件并计算滚动位置,性能开销大。


// ❌ 传统无限滚动实现
window.addEventListener('scroll', () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
loadMoreData();
}
});

✅ 推荐方案


使用IntersectionObserver监听底部哨兵元素。


/**
* 创建无限滚动观察器
* @description 监听底部哨兵元素实现无限滚动
* @param {Function} loadMore - 加载更多数据的回调函数
* @param {Object} options - 观察器配置
* @returns {Object} 包含观察器和控制方法的对象
*/

const createInfiniteScroll = (loadMore, options = {}) => {
const defaultOptions = {
root: null,
rootMargin: '100px', // 提前100px触发加载
threshold: 0
};

const config = { ...defaultOptions, ...options };
let isLoading = false;

const observer = new IntersectionObserver(async (entries) => {
const [entry] = entries;

if (entry.isIntersecting && !isLoading) {
isLoading = true;
try {
await loadMore();
} catch (error) {
console.error('加载数据失败:', error);
} finally {
isLoading = false;
}
}
}, config);

return {
observer,
// 开始观察哨兵元素
observe: (element) => observer.observe(element),
// 停止观察
disconnect: () => observer.disconnect(),
// 获取加载状态
getLoadingState: () => isLoading
};
};

💡 核心要点



  • 哨兵元素:在列表底部放置一个不可见的元素作为触发器
  • 防重复加载:使用loading状态防止重复请求
  • 错误处理:加载失败时的异常处理

🎯 实际应用


Vue3组件中的使用示例:


<template>
<div class="infinite-list">
<div v-for="item in items" :key="item.id" class="list-item">
{{ item.title }}
</div>
<!-- 哨兵元素 -->
<div ref="sentinelRef" class="sentinel"></div>
<div v-if="loading" class="loading">加载中...</div>
</div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const items = ref([]);
const loading = ref(false);
const sentinelRef = ref(null);
let infiniteScroll = null;

// 加载更多数据
const loadMoreData = async () => {
loading.value = true;
// 模拟API请求
const newItems = await fetchData();
items.value.push(...newItems);
loading.value = false;
};

onMounted(() => {
infiniteScroll = createInfiniteScroll(loadMoreData);
infiniteScroll.observe(sentinelRef.value);
});

onUnmounted(() => {
infiniteScroll?.disconnect();
});
</script>



3. 元素可见性统计:精准的用户行为分析


🔍 应用场景


统计用户对页面内容的浏览情况,比如广告曝光、内容阅读时长等。


❌ 常见问题


手动计算元素可见性复杂且不准确。


// ❌ 手动计算可见性
const isElementVisible = (element) => {
const rect = element.getBoundingClientRect();
return rect.top >= 0 && rect.bottom <= window.innerHeight;
};

✅ 推荐方案


使用IntersectionObserver精准统计元素可见性。


/**
* 创建可见性统计观察器
* @description 统计元素的可见性和停留时间
* @param {Function} onVisibilityChange - 可见性变化回调
* @param {Object} options - 观察器配置
* @returns {IntersectionObserver} 观察器实例
*/

const createVisibilityTracker = (onVisibilityChange, options = {}) => {
const defaultOptions = {
root: null,
rootMargin: '0px',
threshold: [0, 0.25, 0.5, 0.75, 1.0] // 多个阈值,精确统计
};

const config = { ...defaultOptions, ...options };
const visibilityData = new Map();

const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const element = entry.target;
const elementId = element.dataset.trackId || element.id;

if (!visibilityData.has(elementId)) {
visibilityData.set(elementId, {
element,
startTime: null,
totalTime: 0,
maxVisibility: 0
});
}

const data = visibilityData.get(elementId);

if (entry.isIntersecting) {
// 元素进入视口
if (!data.startTime) {
data.startTime = Date.now();
}
data.maxVisibility = Math.max(data.maxVisibility, entry.intersectionRatio);
} else {
// 元素离开视口
if (data.startTime) {
data.totalTime += Date.now() - data.startTime;
data.startTime = null;
}
}

// 触发回调
onVisibilityChange({
elementId,
isVisible: entry.isIntersecting,
visibilityRatio: entry.intersectionRatio,
totalTime: data.totalTime,
maxVisibility: data.maxVisibility
});
});
}, config);

return observer;
};

💡 核心要点



  • 多阈值监听:使用多个threshold值精确统计可见比例
  • 时间统计:记录元素在视口中的停留时间
  • 数据持久化:将统计数据存储到Map中

🎯 实际应用


广告曝光统计的实际应用:


// 实际项目中的广告曝光统计
const trackAdExposure = () => {
const tracker = createVisibilityTracker((data) => {
const { elementId, isVisible, visibilityRatio, totalTime } = data;

// 曝光条件:可见比例超过50%且停留时间超过1秒
if (visibilityRatio >= 0.5 && totalTime >= 1000) {
// 发送曝光统计
sendExposureData({
adId: elementId,
exposureTime: totalTime,
visibilityRatio: visibilityRatio
});
}
});

// 观察所有广告元素
document.querySelectorAll('.ad-banner').forEach(ad => {
tracker.observe(ad);
});
};



4. 动画触发控制:精准的视觉效果


🔍 应用场景


当元素进入视口时触发CSS动画或JavaScript动画,提升用户体验。


❌ 常见问题


使用滚动监听触发动画,性能差且时机不准确。


// ❌ 传统动画触发方式
window.addEventListener('scroll', () => {
const elements = document.querySelectorAll('.animate-on-scroll');
elements.forEach(el => {
const rect = el.getBoundingClientRect();
if (rect.top < window.innerHeight * 0.8) {
el.classList.add('animate');
}
});
});

✅ 推荐方案


使用IntersectionObserver精准控制动画触发时机。


/**
* 创建动画触发观察器
* @description 当元素进入视口时触发动画
* @param {Object} options - 观察器和动画配置
* @returns {IntersectionObserver} 观察器实例
*/

const createAnimationTrigger = (options = {}) => {
const defaultOptions = {
root: null,
rootMargin: '-10% 0px', // 元素完全进入视口后触发
threshold: 0.3,
animationClass: 'animate-in',
once: true // 只触发一次
};

const config = { ...defaultOptions, ...options };
const triggeredElements = new Set();

const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const element = entry.target;

if (entry.isIntersecting) {
// 添加动画类
element.classList.add(config.animationClass);

if (config.once) {
// 只触发一次,停止观察
observer.unobserve(element);
triggeredElements.add(element);
}

// 触发自定义事件
element.dispatchEvent(new CustomEvent('elementVisible', {
detail: { intersectionRatio: entry.intersectionRatio }
}));
} else if (!config.once) {
// 允许重复触发时,移除动画类
element.classList.remove(config.animationClass);
}
});
}, {
root: config.root,
rootMargin: config.rootMargin,
threshold: config.threshold
});

return observer;
};

💡 核心要点



  • rootMargin负值:确保元素完全进入视口后才触发
  • once选项:控制动画是否只触发一次
  • 自定义事件:方便其他代码监听动画触发

🎯 实际应用


配合CSS动画的完整实现:


/* CSS动画定义 */
.fade-in-element {
opacity: 0;
transform: translateY(30px);
transition: all 0.6s ease-out;
}

.fade-in-element.animate-in {
opacity: 1;
transform: translateY(0);
}

// JavaScript动画控制
const initScrollAnimations = () => {
const animationTrigger = createAnimationTrigger({
animationClass: 'animate-in',
threshold: 0.2,
once: true
});

// 观察所有需要动画的元素
document.querySelectorAll('.fade-in-element').forEach(element => {
animationTrigger.observe(element);

// 监听动画触发事件
element.addEventListener('elementVisible', (e) => {
console.log(`元素动画触发,可见比例: ${e.detail.intersectionRatio}`);
});
});
};



5. 虚拟滚动优化:大数据列表的性能救星


🔍 应用场景


处理包含大量数据的列表,只渲染可见区域的元素,提升性能。


❌ 常见问题


渲染大量DOM元素导致页面卡顿,滚动性能差。


// ❌ 渲染所有数据
const renderAllItems = (items) => {
const container = document.getElementById('list');
items.forEach(item => {
const element = document.createElement('div');
element.textContent = item.title;
container.appendChild(element);
});
};

✅ 推荐方案


结合IntersectionObserver实现简化版虚拟滚动。


/**
* 创建虚拟滚动观察器
* @description 只渲染可见区域的列表项,优化大数据列表性能
* @param {Array} data - 数据数组
* @param {Function} renderItem - 渲染单个项目的函数
* @param {Object} options - 配置选项
* @returns {Object} 虚拟滚动控制器
*/

const createVirtualScroll = (data, renderItem, options = {}) => {
const defaultOptions = {
itemHeight: 60, // 每项高度
bufferSize: 5, // 缓冲区大小
container: null // 容器元素
};

const config = { ...defaultOptions, ...options };
const visibleItems = new Map();

// 创建占位元素
const createPlaceholder = (index) => {
const placeholder = document.createElement('div');
placeholder.style.height = `${config.itemHeight}px`;
placeholder.dataset.index = index;
placeholder.classList.add('virtual-item-placeholder');
return placeholder;
};

const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const placeholder = entry.target;
const index = parseInt(placeholder.dataset.index);

if (entry.isIntersecting) {
// 元素进入视口,渲染真实内容
if (!visibleItems.has(index)) {
const realElement = renderItem(data[index], index);
realElement.style.height = `${config.itemHeight}px`;
placeholder.replaceWith(realElement);
visibleItems.set(index, realElement);
}
} else {
// 元素离开视口,替换为占位符
const realElement = visibleItems.get(index);
if (realElement) {
const newPlaceholder = createPlaceholder(index);
realElement.replaceWith(newPlaceholder);
observer.observe(newPlaceholder);
visibleItems.delete(index);
}
}
});
}, {
root: config.container,
rootMargin: `${config.bufferSize * config.itemHeight}px`,
threshold: 0
});

// 初始化列表
const init = () => {
const container = config.container;
container.innerHTML = '';

data.forEach((_, index) => {
const placeholder = createPlaceholder(index);
container.appendChild(placeholder);
observer.observe(placeholder);
});
};

return {
init,
destroy: () => observer.disconnect(),
getVisibleCount: () => visibleItems.size
};
};

💡 核心要点



  • 占位符机制:使用固定高度的占位符保持滚动条正确
  • 缓冲区:通过rootMargin提前渲染即将可见的元素
  • 内存管理:及时清理不可见的元素,释放内存

🎯 实际应用


Vue3组件中的虚拟滚动实现:


<template>
<div ref="containerRef" class="virtual-scroll-container">
<!-- 虚拟滚动内容将在这里动态生成 -->
</div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const containerRef = ref(null);
let virtualScroll = null;

// 大量数据
const largeDataset = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
title: `列表项 ${i + 1}`,
content: `这是第 ${i + 1} 个列表项的内容`
})));

// 渲染单个列表项
const renderListItem = (item, index) => {
const element = document.createElement('div');
element.className = 'list-item';
element.innerHTML = `
<h3>${item.title}</h3>
<p>${item.content}</p>
`;
return element;
};

onMounted(() => {
virtualScroll = createVirtualScroll(
largeDataset.value,
renderListItem,
{
itemHeight: 80,
bufferSize: 3,
container: containerRef.value
}
);

virtualScroll.init();
});

onUnmounted(() => {
virtualScroll?.destroy();
});
</script>



📊 技巧对比总结


技巧使用场景优势注意事项
图片懒加载大量图片展示性能优秀,实现简单需要设置合适的rootMargin
无限滚动长列表数据加载避免频繁滚动监听防止重复加载,错误处理
可见性统计用户行为分析精确统计,多阈值监听数据存储和上报策略
动画触发页面交互效果时机精准,性能好动画只触发一次的控制
虚拟滚动大数据列表内存占用低,滚动流畅元素高度固定,复杂度较高



🎯 实战应用建议


最佳实践



  1. 合理设置rootMargin:根据实际需求提前或延迟触发观察
  2. 及时清理观察器:使用unobserve()和disconnect()释放资源
  3. 错误处理机制:为异步操作添加try-catch保护
  4. 性能监控:在开发环境中监控观察器的性能表现
  5. 渐进增强:为不支持IntersectionObserver的浏览器提供降级方案

性能考虑



  • 观察器数量控制:避免创建过多观察器实例
  • threshold设置:根据实际需求设置合适的阈值
  • 内存泄漏防护:组件销毁时及时清理观察器
  • 兼容性处理:使用polyfill支持旧版浏览器



💡 总结


这5个IntersectionObserver实用场景在日常开发中能显著提升页面性能,掌握它们能让你的滚动监听:



  1. 图片懒加载:告别手动位置计算,性能提升显著
  2. 无限滚动:避免频繁滚动监听,用户体验更佳
  3. 可见性统计:精准的用户行为分析,数据更准确
  4. 动画触发:完美的视觉效果时机控制
  5. 虚拟滚动:大数据列表的性能救星

希望这些技巧能帮助你在前端开发中告别滚动监听的性能焦虑,写出更丝滑的交互代码!




🔗 相关资源






💡 今日收获:掌握了5个IntersectionObserver实用场景,这些知识点在实际开发中非常实用。



如果这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题也欢迎在评论区讨论。 🚀


作者:Bug_Constructer
来源:juejin.cn/post/7549102542833631267

0 个评论

要回复文章请先登录注册