Vue3 自定义指令实现图片懒加载:从原理到实践

108 阅读3分钟

1. 为什么要实现懒加载?

在现代 Web 应用中,图片是流量消耗大户。如果页面包含大量图片(如相册、商品列表),首屏加载时间会显著增加。懒加载的核心思想是:

  • 延迟加载:图片进入可视区域时才开始加载
  • 提升性能:减少首屏请求数,节省用户流量
  • 优化体验:快速响应,避免页面卡顿

2. 核心原理

2.1 技术栈选择

  • Vue3 自定义指令v-lazy,直接操作 DOM 元素
  • Intersection Observer API:现代浏览器原生 API,监听元素是否进入视口
  • 图片预加载:创建 Image 对象提前加载,避免直接修改 src 导致闪烁

2.2 工作流程

1. 元素初始化 → 设置默认 loading 图片
2. 开启 IntersectionObserver 监听
3. 元素进入视口 (isIntersecting = true) → 触发加载
4. 预加载图片 → onload → 设置真实 src
5. 预加载失败 → onerror → 设置错误图
6. 关闭监听,防止重复加载

3. 完整代码实现

// src/directives/lazy.js

// 默认图片(建议使用 Base64 或本地路径)
const defaultLoading = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
const defaultError = 'https://cdn.example.com/images/error.png'

const imgLazy = {
  mounted(el, binding) {
    let observer = null

    // 1. 解析指令参数(支持对象和字符串两种形式)
    const { src, loading, error } =
      typeof binding.value === 'object'
        ? binding.value
        : { src: binding.value }

    // 2. 初始化:设置加载中的占位图
    el.src = loading || defaultLoading

    // 3. 图片预加载函数(核心逻辑)
    const imgLoad = () => {
      const img = new Image() // 创建虚拟 Image 对象预加载
      img.src = src

      // 加载成功
      img.onload = () => {
        el.src = src
        // 可选:添加淡入动画
        el.style.transition = 'opacity 0.3s'
        el.style.opacity = '1'
      }

      // 加载失败
      img.onerror = () => {
        el.src = error || defaultError
      }
    }

    // 4. 创建 IntersectionObserver 监听
    observer = new IntersectionObserver((entries) => {
      const entry = entries[0]

      if (entry.isIntersecting) {
        imgLoad() // 进入视口,开始加载
        observer.unobserve(el) // 关闭监听,避免重复加载
      }
    }, {
      rootMargin: '100px', // 提前 100px 开始加载,提升体验
      threshold: 0.01 // 只要有一点点可见就触发
    })

    // 5. 开启监听,并挂载到元素上(方便卸载时清理)
    observer.observe(el)
    el._imgObserver = observer
  },

  unmounted(el) {
    // 6. 清理监听器,防止内存泄漏
    el._imgObserver?.unobserve(el)
    el._imgObserver = null
  }
}

export default imgLazy

4. 在 Vue3 中使用

4.1 全局注册

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import imgLazy from './directives/lazy'

const app = createApp(App)
app.directive('lazy', imgLazy)
app.mount('#app')

4.2 组件内使用

<template>
  <div class="gallery">
    <!-- 基础用法:直接传 URL -->
    <img v-lazy="item.url" v-for="item in list" :key="item.id" />

    <!-- 进阶用法:配置 loading 和 error -->
    <img
      v-lazy="{
        src: item.url,
        loading: '/images/loading.gif',
        error: '/images/error.png'
      }"
      v-for="item in list"
      :key="item.id"
    />
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: [
        { id: 1, url: 'https://example.com/img1.jpg' },
        { id: 2, url: 'https://example.com/img2.jpg' },
        // ... 更多图片
      ]
    }
  }
}
</script>

<style>
/* 可选:添加加载动画 */
img {
  opacity: 0;
  transition: opacity 0.3s;
}
img[src] {
  opacity: 1;
}
</style>

5. 进阶优化建议

5.1 性能优化

// 优化的 observer 配置
const observer = new IntersectionObserver(callback, {
  rootMargin: '200px', // 更大的预加载区域
  threshold: 0.1,      // 10% 可见时触发
  root: null           // 相对于视口
})

5.2 支持 WebP 和懒加载降级

const imgLoad = () => {
  const img = new Image()

  // 检测浏览器是否支持 WebP
  const supportsWebP = document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') === 0

  img.src = supportsWebP ? src.replace('.jpg', '.webp') : src

  img.onload = () => { el.src = img.src }
  img.onerror = () => { el.src = error || defaultError }
}

5.3 添加加载完成回调

// 扩展指令支持回调
mounted(el, binding) {
  const { src, loading, error, onLoaded, onError } = binding.value

  const imgLoad = () => {
    const img = new Image()
    img.src = src
    img.onload = () => {
      el.src = src
      onLoaded?.(el) // 加载完成回调
    }
    img.onerror = () => {
      el.src = error || defaultError
      onError?.(el) // 加载失败回调
    }
  }
  // ... 其余代码
}

使用:

<img v-lazy="{
  src: item.url,
  onLoaded: (el) => console.log('图片加载完成', el),
  onError: (el) => console.log('图片加载失败', el)
}" />

6. 注意事项与最佳实践

6.1 内存泄漏防范

  • 必须在 unmounted 时调用 unobserve
  • ✅ 避免在组件卸载后继续操作 DOM
  • ✅ 使用 ?. 可选链操作符防止报错

6.2 图片格式建议

  • loading 图片:使用 1x1 像素的透明 GIF 或 Base64,避免额外请求
  • error 图片:使用稳定的 CDN 地址或本地资源
  • 真实图片:优先使用 WebP 格式,体积更小