面试官:会用自定义指令来实现懒加载吗?

前言

懒加载相信大家都熟得不能再熟了,不熟看这 -> 一眼就能明白的懒加载实现原理! 详细易懂......

回归正传,刚刚给面试官讲完懒加载的实现原理,面试官就冷不丁地问我一句:会用自定义指令来实现懒加载吗?

生活不易,面试自闭,破防了......

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方法的导入

懒加载实现思路

  1. 确保所有图片元素的 src 属性为空,或者使用一个占位图。
  2. 将真实的图片地址保存在自定义属性(比如 data-src)中。
  3. 监听页面滚动事件,当用户滚动页面时,检查每个图片元素是否进入了可视区域。
  4. 如果图片进入了可视区域,则将保存在 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>

总结

生活不易,面试自闭。最后,跪求点赞+关注!!!

相关推荐
一颗花生米。2 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19952 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
勿语&3 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
正小安6 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
小飞猪Jay7 小时前
C++面试速通宝典——13
jvm·c++·面试
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光7 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js