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" /> 一行代码,简洁复用,底层自动处理进入视口监听和资源加载。

相关推荐
寅时码33 分钟前
React 之死·终章:一个 useRef,把闭包陷阱、依赖数组、漫天 rerender 全送走
前端·react.js·ai编程
不好听61337 分钟前
HTML 事件监听机制:从 DOM Level 0 到 React 合成事件
前端·react.js·html
wordbaby1 小时前
为什么刷新页面就 404?聊聊 SPA 路由与 Nginx 的那点事
前端
daols881 小时前
vxe-table 渲染器教程一:实现金额输入控件
javascript·vue.js·vxe-table
不好听6131 小时前
React 核心概念:JSX、组件与数据驱动
前端·react.js
触底反弹1 小时前
🔥 React 零基础入门(中):500 行屎山代码到组件化的蜕变
前端·javascript·react.js
不好听6131 小时前
React vs Vue:两大前端框架技术选型深度对比
前端·vue.js·react.js
GuWenyue1 小时前
Cursor黑盒拆解!1套LangChain.js手写Mini编程Agent,自动生成React项目,效率提升60%
前端·数据库·人工智能
GuWenyue1 小时前
传统Agent工具两大痛点!300行代码落地MCP跨语言工具,彻底解耦LLM与工具
前端·人工智能·算法
小林ixn2 小时前
从 onclick 到 React 合成事件,再到完整应用逻辑:一次前端架构的深度剖析
前端·react.js·前端框架