一、什么是 IntersectionObserver?
IntersectionObserver 是浏览器提供的一个 API,用于异步监听目标元素与其祖先元素或顶级视口的交叉状态。
大白话:它可以告诉你,某个元素是否出现在了屏幕上。
传统 scroll 监听 vs IntersectionObserver
| 对比项 | scroll 监听 | IntersectionObserver |
|---|---|---|
| 触发频率 | 每次滚动都触发 | 只在交叉状态变化时触发 |
| 性能 | 需要手动节流 | 浏览器原生优化 |
| 代码复杂度 | 需要自己计算距离 | API 简洁 |
二、基础用法
2.1 基本语法
javascript
// 1. 创建观察器
const observer = new IntersectionObserver(callback, options)
// 2. 开始观察
observer.observe(element)
// 3. 停止观察
observer.unobserve(element)
// 4. 停止所有
observer.disconnect()
2.2 回调参数
javascript
const callback = (entries) => {
entries.forEach(entry => {
console.log(entry.target) // 被观察的元素
console.log(entry.isIntersecting) // 是否进入视口(true/false)
console.log(entry.intersectionRatio)// 可见比例(0-1)
})
}
2.3 配置选项
javascript
const options = {
root: null, // 滚动容器,null 表示视口
rootMargin: '0px', // 扩大交叉区域,如 '100px' 提前触发
threshold: 0 // 触发阈值,0.5 表示露出 50% 时触发
}
三、实战:封装 Vue 指令实现图片懒加载
3.1 核心思路
- 自定义指令
v-lazy,绑定图片真实地址 - 指令内部使用 IntersectionObserver 监听图片元素
- 图片进入视口时,将真实地址赋给
src属性 - 加载完成后停止观察
3.2 指令封装代码
javascript
// directives/lazy.js
// 默认占位图
const defaultPlaceholder = 'https://via.placeholder.com/400x300?text=加载中...'
// 定义指令
export default {
mounted(el, binding) {
// el: 指令绑定的 DOM 元素(img 标签)
// binding.value: 图片的真实地址
const realSrc = binding.value
const placeholder = binding.arg || defaultPlaceholder
// 先显示占位图
el.src = placeholder
// 创建 IntersectionObserver
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
// 当图片进入视口时
if (entry.isIntersecting) {
// 替换真实地址
el.src = realSrc
// 加载完成后停止观察
observer.unobserve(el)
// 可选:加载失败时的处理
el.onerror = () => {
el.src = placeholder
}
}
})
}, {
rootMargin: '100px', // 提前 100px 开始加载
threshold: 0.01
})
// 开始观察
observer.observe(el)
// 将 observer 存储到元素上,方便后续解绑
el._lazyObserver = observer
},
unmounted(el) {
// 组件销毁时,停止观察
if (el._lazyObserver) {
el._lazyObserver.disconnect()
delete el._lazyObserver
}
}
}
3.3 注册指令
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import lazyDirective from './directives/lazy'
const app = createApp(App)
// 全局注册指令
app.directive('lazy', lazyDirective)
app.mount('#app')
3.4 在组件中使用
vue
<template>
<div class="image-list">
<!-- 使用 v-lazy 指令,传入真实图片地址 -->
<img
v-for="img in imageList"
:key="img.id"
v-lazy="img.url"
class="lazy-image"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 模拟后端返回的图片数据
const imageList = ref([])
const fetchImages = async () => {
// 模拟请求
const res = await fetch('/api/images')
imageList.value = await res.json()
// 返回格式: [{ id: 1, url: 'xxx.jpg' }, ...]
}
onMounted(() => {
fetchImages()
})
</script>
3.5 带占位图参数的用法
vue
<template>
<!-- 通过指令参数传入自定义占位图 -->
<img
v-lazy:customPlaceholder="img.url"
v-lazy="img.url"
/>
</template>
<!-- 或使用对象语法 -->
<script setup>
// 在指令中支持更灵活的配置
const lazyConfig = {
src: 'https://real-image.jpg',
placeholder: 'https://loading.gif',
error: 'https://error.jpg'
}
</script>
<template>
<img v-lazy="lazyConfig" />
</template>
3.6 增强版指令(支持更多配置)
javascript
// directives/lazy.js - 增强版
export default {
mounted(el, binding) {
// 支持字符串或对象两种传参
let realSrc, placeholder, errorSrc
if (typeof binding.value === 'string') {
realSrc = binding.value
placeholder = binding.arg || 'https://via.placeholder.com/400x300?text=加载中'
errorSrc = placeholder
} else {
realSrc = binding.value.src
placeholder = binding.value.placeholder || 'https://via.placeholder.com/400x300?text=加载中'
errorSrc = binding.value.error || placeholder
}
// 添加加载中样式
el.classList.add('lazy-loading')
el.src = placeholder
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = new Image()
img.src = realSrc
img.onload = () => {
el.src = realSrc
el.classList.remove('lazy-loading')
el.classList.add('lazy-loaded')
observer.unobserve(el)
}
img.onerror = () => {
el.src = errorSrc
el.classList.remove('lazy-loading')
el.classList.add('lazy-error')
observer.unobserve(el)
}
}
})
}, {
rootMargin: binding.value.rootMargin || '100px',
threshold: binding.value.threshold || 0.01
})
observer.observe(el)
el._lazyObserver = observer
},
unmounted(el) {
if (el._lazyObserver) {
el._lazyObserver.disconnect()
delete el._lazyObserver
}
}
}
四、其他典型应用(指令形式)
4.1 无限滚动指令
javascript
// directives/infinite-scroll.js
export default {
mounted(el, binding) {
const callback = binding.value
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
callback()
}
}, {
rootMargin: '200px'
})
observer.observe(el)
el._infiniteObserver = observer
},
unmounted(el) {
if (el._infiniteObserver) {
el._infiniteObserver.disconnect()
}
}
}
vue
<template>
<div class="list">
<div v-for="item in list" :key="item.id">{{ item.name }}</div>
<div v-infinite-scroll="loadMore" class="trigger">加载更多...</div>
</div>
</template>
4.2 曝光埋点指令
javascript
// directives/exposure.js
export default {
mounted(el, binding) {
const reportFn = binding.value
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !el.dataset.reported) {
el.dataset.reported = 'true'
reportFn(el)
observer.unobserve(el)
}
})
}, {
threshold: 0.5
})
observer.observe(el)
el._exposureObserver = observer
},
unmounted(el) {
if (el._exposureObserver) {
el._exposureObserver.disconnect()
}
}
}
vue
<template>
<div
v-for="ad in adList"
:key="ad.id"
v-exposure="() => reportAdExposure(ad.id)"
>
{{ ad.title }}
</div>
</template>
五、总结
| 要点 | 内容 |
|---|---|
| 核心 API | new IntersectionObserver(callback, options) |
| 指令生命周期 | mounted 中注册观察,unmounted 中清理 |
| 指令参数 | 支持字符串(真实地址)或对象(src、placeholder、error) |
| 关键配置 | rootMargin 提前加载,threshold 控制触发比例 |
| 典型应用指令 | v-lazy、v-infinite-scroll、v-exposure |
一句话总结:
将 IntersectionObserver 封装成 Vue 指令,图片懒加载只需要写
<img v-lazy="url" />一行代码,简洁复用,底层自动处理进入视口监听和资源加载。