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

相关推荐
清灵xmf6 小时前
Web 和 Native 是怎么“对话“的?JSBridge 解答
前端·webview·native·jsbridge·hybrid
jiayong237 小时前
前端面试题库 - ES6+新特性篇
前端·面试·es6
海兰7 小时前
【实用应用】React+TypeScript+Next.js博客项目
开发语言·javascript·elasticsearch
前端那点事7 小时前
Vue nextTick 超全解析|作用、使用场景、底层原理、Vue2/Vue3区别
前端·vue.js
前端那点事7 小时前
Vue面试高频:子组件能直接修改父组件数据吗?单向数据流原理+正确写法全覆盖
前端·vue.js
前端那点事7 小时前
为什么 Vue 的 template 标签不能用 v-show?底层机制+踩坑复盘+生产级解决方案
前端·vue.js
weelinking7 小时前
【claude】14_Claude作为技术文档助手
前端·人工智能·react.js·数据挖掘·前端框架
天问一7 小时前
router路由类型和使用方法
开发语言·javascript·ecmascript
jiayong237 小时前
前端面试题库 - JavaScript核心基础篇
前端·javascript·面试