懒加载(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. 构建组件
- 期望组件参数:
<m-infinite-list
v-model="" // 当前是否处于加载状态
:isFinished="" // 数据是否全部加载完成
@onLoad="" // 加载下一页数据的触发事件
>
列表
</m-infinite-list>
- 定义组件接收参数并处理双向绑定逻辑
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)
- 构建对应视图与插槽逻辑
<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>
- 利用 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 标签不是网络地址呢?把该网络地址默认替换为非网络地址,然后当用户可见时,在替换成网络地址。
- 创建自定义指令文件
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()
}
})
}
}
- 完成注册:
import lazy from './modules/lazy'
/**
* 全局注册指令
*/
export default {
install(app) {
app.directive('lazy', lazy)
}
}
- 在
main.js
中触发use
// 导入全局指令
import mDirectives from '@/directives/index.js'
app.use(mDirectives)
- 全局替换所有的
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)
}
}
}