懒加载

47 阅读3分钟

懒加载(Lazy Loading)是一种延迟加载技术,指在需要使用某个对象或数据时才进行加载,而不是在系统启动或加载时就立即加载。懒加载可以在一定程度上提高系统的性能和资源利用率。简而言之,懒加载就是按需加载,或者延时加载。

1. 技术原理:

判断元素是否出现需要加载的条件,条件成立就加载,不成立就不加载。这样做能防止页面一次性向服务器请求大量数据,导致服务器响应慢,页面卡顿或崩溃等情况。

2. 应用场景

  • 数据懒加载:当页面滚动到列表底部时,才加载数据。
  • 图片懒加载:当图片不可见时,不加载图片。当图片可见时,才去加载图片。

3. 代码实践

3.1. 通用组件:长列表 infinite

3.1.1. 实现原理

通过监听到列表滚动到底部,再加载数据。

核心API:vueuse提供的 [useIntersectionObserver]

该接口可以检查到目标元素的可见性。

<script setup>
import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'

const target = ref(null)
const targetIsVisible = ref(false)

const { stop } = useIntersectionObserver(
  target,
  ([{ isIntersecting }], observerElement) => {
    targetIsVisible.value = isIntersecting
  },
)
</script>

<template>
  <div ref="target">
    <h1>Hello world</h1>
  </div>
</template>

3.1.2. 构建组件

  1. 期望组件参数:
<m-infinite-list
  v-model="" // 当前是否处于加载状态
  :isFinished="" // 数据是否全部加载完成
  @onLoad="" // 加载下一页数据的触发事件
>
	列表
</m-infinite-list>
  1. 定义组件接收参数并处理双向绑定逻辑
import { ref } from 'vue'
import { useVModel } from '@vueuse/core'

const props = defineProps({
  // 是否处于加载状态
  modelValue: {
    type: Boolean,
    required: true
  },
  // 数据是否全部加载完成
  isFinished: {
    type: Boolean,
    default: false
  }
})

const emits = defineEmits(['onLoad', 'update:modelValue'])

// 处理 loading 状态
const loading = useVModel(props)
  1. 构建对应视图与插槽逻辑
<template>
  <div>
    <!-- 内容 -->
    <slot />
    <div ref="loadingTarget" class="h-6 py-4">
      <!-- 加载更多 -->
      <m-svg-icon
        v-show="loading"
        class="h-4 w-4 mx-auto animate-spin"
        name="infinite-load"
        ></m-svg-icon>
      <!-- 没有更多数据 -->
      <p v-if="isFinished" class="text-center text-base text-zinc-400">已经没有更多数据了~</p>
    </div>
  </div>
</template>
  1. 利用 useIntersectionObserver 方法,监听元素可见行为,用于判断列表滚动到底部:
// 滚动的元素
const loadingTarget = ref(null)
// 记录当前是否在底部
const targetIsIntersecting = ref(false)
useIntersectionObserver(loadingTarget, ([{ isIntersecting }]) => {
  // 获取当前交叉状态
  targetIsIntersecting.value = isIntersecting
  emitLoad()
})

const emitLoad = () => {
  // 当 加载更多 视图可见时,加载更多数据
  if (targetIsIntersecting.value && !loading.value && !props.isFinished) {
    // 修改加载视图标记
    loading.value = true
    // 触发更多加载行为
    emits('onLoad')
  }
}
/**
 * 监听 loading 的变化,解决数据加载完成后,首屏未铺满的问题
 */
watch(loading, (val) => {
  // 触发 load,延迟处理,等待 渲染和 useIntersectionObserver 的再次触发
  setTimeout(() => {
    emitLoad()
  }, 100)
})

3.2. 通用指令:图片懒加载

图片懒加载:在用户无法看到图片时,不加载图片,在用户可以看到图片后加载图片

如何判断用户是否看到了图片: useIntersectionObserver

如何做到不加载图片(网络):img 标签渲染图片,指的是 img 的 src 属性,src 属性是网络地址时,则会从网络中获取该图片资源。那么如果 img 标签不是网络地址呢?把该网络地址默认替换为非网络地址,然后当用户可见时,在替换成网络地址。

  1. 创建自定义指令文件
import { useIntersectionObserver } from '@vueuse/core'

export default {
  mounted(el) {
    // 1. 拿到当前 img 标签的 src
    const imgSrc = el.src
    // 2. 把 img 标签的 src 替换为本地地址,也可以替换为空或者一个透明的图片
    el.src = ''
    const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
      if (isIntersecting) {
        el.src = imgSrc
        stop()
      }
    })
  }
}
  1. 完成注册:
import lazy from './modules/lazy'

/**
 * 全局注册指令
 */
export default {
  install(app) {
    app.directive('lazy', lazy)
  }
}
  1. main.js 中触发 use
// 导入全局指令
import mDirectives from '@/directives/index.js'

app.use(mDirectives)
  1. 全局替换所有的 img 标签为 <img v-lazy />

3.3. 深入 vite:指令的自动注册

当我们在src/directives/index.js注册指令时,如果指令过多,那么一个一个注册太麻烦了,最好的方式是实现指令的自动注册

利用 Glob导入(import.meta.globEager)Object.entries 功能:

export default {
  install(app) {
    // 1. 获取当前路径下的所有文件
    const directives = import.meta.glob('./modules/*.js', { eager: true })
    // 2. 遍历获取到的自定义指令
    for (const [key, value] of Object.entries(directives)) {
      // 拼接指令注册的 name
      const dirArr = key.replace('.js', '').split('/')
      const dirName = dirArr[dirArr.length - 1]
      // 完成注册
      app.directive(dirName, value.default)
    }
  }
}