IntersectionObserver 详解:封装 Vue 指令实现图片懒加载

一、什么是 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 核心思路

  1. 自定义指令 v-lazy,绑定图片真实地址
  2. 指令内部使用 IntersectionObserver 监听图片元素
  3. 图片进入视口时,将真实地址赋给 src 属性
  4. 加载完成后停止观察

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-lazyv-infinite-scrollv-exposure

一句话总结:

将 IntersectionObserver 封装成 Vue 指令,图片懒加载只需要写 <img v-lazy="url" /> 一行代码,简洁复用,底层自动处理进入视口监听和资源加载。

相关推荐
Uso_Magic14 小时前
VOL_实现APP多文件上传_前端多文件显示!
前端
问心无愧051314 小时前
ctf sow web入门112
android·前端·笔记
库拉大叔14 小时前
工具调用效率对比实测:GPT-5.5与Gemini 3.5 Flash性能评估
java·前端·人工智能
艾伦野鸽ggg14 小时前
CSS容器查询和悬浮间隙问题
前端·css
就改了14 小时前
微服务接口性能优化:CompletableFuture 并行聚合实践
java·微服务·性能优化
云水一下15 小时前
Vue.js从零到精通系列(一):初识Vue——背景、环境与第一个应用
前端·javascript·vue.js
大大杰哥15 小时前
Vue2学习(1)--了解基本方法与概念
javascript·学习·vue
云水一下15 小时前
Vue.js从零到精通系列(二):响应式核心——ref、reactive、computed与watch
前端·javascript·vue.js
放下华子我只抽RuiKe515 小时前
FastAPI 全栈后端(二):路由与数据模型
前端·人工智能·react.js·前端框架·html·fastapi
lichenyang45315 小时前
ArkTS 严格类型系统:我答错 2 道题后才真正搞懂的几条规则
前端