前言
懒加载相信大家都熟得不能再熟了,不熟看这 -> 一眼就能明白的懒加载实现原理! 详细易懂......
回归正传,刚刚给面试官讲完懒加载的实现原理,面试官就冷不丁地问我一句:会用自定义指令来实现懒加载吗?
生活不易,面试自闭,破防了......
1. 自定义指令的创建
创建指令
一个自定义指令 由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。下面是一个自定义指令的例子,当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦。
注意: 在没有使用 <script setup>
的情况下,自定义指令需要通过 directives
选项注册:
js
//main.js
import { createApp } from 'vue'
const app = createApp({})
// 使 v-img-lazy 在所有组件中都可用
app.directive('img-lazy', {
/* ... */
})
关于自定义指令 和指令钩子 更详细的内容可以前往Vue.js文档阅读 -> Vue.js自定义指令
指令钩子
以下是一个指令的定义对象可以提供的几种钩子函数的截图
我们一般使用mounted(el, binding, vnode, prevVnode) {}
钩子,该钩子最主要的俩种参数为:
el
:指令绑定到的元素。这可以用于直接操作 DOM。例如:<img>
binding
:一个对象,其中的binding.value
指令可绑定表达式的值,例如图片的url。
js
// main.js
import { createApp } from 'vue'
const app = createApp(App)
// 定义全局指令
app.directive('img-lazy', {
mounted (el, binding) {
// el: 指令绑定的那个元素 img
// binding: binding.value 可绑定表达式的值,例如图片的url。
console.log(el, binding.value)
}
})
app.mount('#app')
验证自定义指令是否生效
我们用以下的代码可以来验证下该自定义指令v-img-lazy
是否有打印结果:
js
<template>
<!-- 图片容器 -->
<div class="container">
<div class="box" v-for="item in imageList">
<!-- 初始时使用空src,并在data-src中指定真实图片路径 -->
<img v-img-lazy:src="item" src="">
</div>
</div>
</template>
<script setup>
// 图片列表
const imageList = [
"https://yanxuan-item.nosdn.127.net/cac68a7880bec1c72dcfce112d10e955.png",
"https://yanxuan-item.nosdn.127.net/06a158d2888b20383a466227e39bbbc7.jpg",
"https://yanxuan.nosdn.127.net/8f8092d5bf6a133a8cb59ab7b9f790e9.png",
"https://yanxuan-item.nosdn.127.net/eac6c40fdb0f977fdf80048d7b181ffa.png",
"https://yanxuan-item.nosdn.127.net/f881cfe7de9a576aaeea6ee0d1d24823.jpg"
]
</script>
<style lang="css" scoped>
.container {
margin-top: 900px;
}
img {
width: 230px;
height: 160px;
margin-left: 10px;
background-color: #f2f2f2;
}
</style>
打印结果:
可以看到我们的自定义指令v-img-lazy
有打印结果说明已经绑定生效,接下来就是写业务逻辑了。
2. VueUse方法的导入
懒加载实现思路
- 确保所有图片元素的
src
属性为空,或者使用一个占位图。 - 将真实的图片地址保存在自定义属性(比如
data-src
)中。 - 监听页面滚动事件,当用户滚动页面时,检查每个图片元素是否进入了可视区域。
- 如果图片进入了可视区域,则将保存在
data-src
中的真实图片地址赋值给src
属性,从而触发图片的加载显示。
这个懒加载的实现思路相信大家都清楚,但是最关键的监听页面滚动事件在实际项目中实现起来就有点麻烦,那么有没有一种好用的方法来监听页面元素的滚动呢?
答案是肯定有的啦 -> VueUse 中的 useIntersectionObserver
js
<script setup>
import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core' // 引入方法
const el = ref(null)
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => { // isIntersecting 监听绑定的元素是否进入视口区域
// 编辑业务逻辑
},
)
</script>
<template>
<div ref="el"></div>
</template>
-
import { useIntersectionObserver } from '@vueuse/core'
:这里引入了@vueuse/core
库中的useIntersectionObserver
方法,用于实现元素的可见性检测。 -
const { stop } = useIntersectionObserver(...)
:调用了useIntersectionObserver
方法,该方法接受两个参数:第一个参数是需要进行可见性检测的元素引用(这里传入了之前定义的el
引用),第二个参数是一个回调函数,当可见性状态发生变化时会执行这个回调函数。在这个回调函数中,通过解构赋值获取到了一个对象,其中包含了isIntersecting
属性,表示绑定元素是否进入视口区域。
验证 useIntersectionObserver 方法
js
// main.js
import { createApp } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
const app = createApp(App)
// 定义全局指令
app.directive('img-lazy', {
mounted (el, binding) {
// el: 指令绑定的那个元素 img
// binding: binding.value 指令等于号后面绑定的表达式的值 图片url
console.log(el, binding.value)
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => { // isIntersecting 监听是否进入视口区域
console.log(isIntersecting)
},
)
}
})
app.mount('#app')
滑动前:
我们可以看到该方法打印出了5个fasle
,说明该绑定元素当前不在当前视口窗内。
滚动使图片出现后;
图片出现后useIntersectionObserver
方法打印出了true
说明当前元素已经进入了当前视口窗内,有了isIntersecting
这个监听属性我们就可以很方便地是是实现我们的效果。
3. 懒加载全局指令的实现
js
// main.js
import { createApp } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
const app = createApp(App)
// 定义全局指令
app.directive('img-lazy', {
mounted (el, binding) {
// el: 指令绑定的那个元素 img
// binding: binding.value 指令等于号后面绑定的表达式的值 图片url
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
// console.log(isIntersecting)
if (isIntersecting) {
// 进入视口区域
el.src = binding.value
// 要停止,否则浪费内存
stop() // 第一次加载完图片后就停止监听
}
},
)
}
})
app.mount('#app')
注: 通过在图片第一次成功加载后立即停止监听,实现了一次性监听图片加载的效果。这样可以确保只有第一次加载图片时进行监听,后续不再监听,避免重复操作和性能消耗。
效果对比前:
滑动效果后:
到这里我们的全局自定义懒加载v-img-lazy
指令就生效了,是不是非常简单?
4. 实现插件化(附完整代码)
懒加载指定的逻辑写到入口main.js文件这样看起来实在不太美观,且main.js通常只做一些初始化的事情,不应该包含太多的逻辑代码,可以通过插件的方法把懒加载指令封装为插件 ,main.js入口文件只需要负责注册插件即可。
js
// directives/main.js
// 定义懒加载插件
import { useIntersectionObserver } from '@vueuse/core'
export const lazyPlugin = {
install (app) {
// 懒加载指令逻辑
app.directive('img-lazy', {
mounted (el, binding) {
// el: 指令绑定的那个元素 img
// binding: binding.value 指令等于号后面绑定的表达式的值 图片url
// console.log(el, binding.value)
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
// console.log(isIntersecting)
if (isIntersecting) {
// 进入视口区域
el.src = binding.value
// 要停止,否则浪费内存
stop()
}
},
)
}
})
}
}
install
方法是 Vue 插件的一个约定方法,当你创建一个 Vue 插件时,需要在插件对象中定义 install
方法,最后我们抛出即可实现插件化。
js
// main.js
import { createApp } from 'vue'
import { lazyPlugin } from '@/directives/index.js' // 导入插件文件中的lazyPlugin组件
import App from './App.vue'
const app = createApp(App)
// 使用全局指令
app.use(lazyPlugin).mount('#app')
实例代码:
js
<template>
<!-- 图片容器 -->
<div class="container">
<div class="box" v-for="item in imageList">
<!-- 初始时使用空src,并在data-src中指定真实图片路径 -->
<img v-img-lazy:src="item" src="">
</div>
</div>
</template>
<script setup>
// 图片列表
const imageList = [
"https://yanxuan-item.nosdn.127.net/cac68a7880bec1c72dcfce112d10e955.png",
"https://yanxuan-item.nosdn.127.net/06a158d2888b20383a466227e39bbbc7.jpg",
"https://yanxuan.nosdn.127.net/8f8092d5bf6a133a8cb59ab7b9f790e9.png",
"https://yanxuan-item.nosdn.127.net/eac6c40fdb0f977fdf80048d7b181ffa.png",
"https://yanxuan-item.nosdn.127.net/f881cfe7de9a576aaeea6ee0d1d24823.jpg"
]
</script>
<style lang="css" scoped>
.container {
margin-top: 900px;
}
img {
width: 230px;
height: 160px;
margin-left: 10px;
background-color: #f2f2f2;
}
</style>
总结
生活不易,面试自闭。最后,跪求点赞+关注!!!