背景
众所周知,Vue.js 的核心思想是数据驱动 + 组件化,通常我们开发页面的过程就是在编写一些组件,并且通过修改数据的方式来驱动组件的重新渲染。在这个过程中,我们不需要去手动操作 DOM。

然而在有些场景下,我们还是避免不了要操作 DOM。由于 Vue.js 框架接管了 DOM 元素的创建和更新的过程,因此它可以在 DOM 元素的生命周期内注入用户的代码,于是 Vue.js 设计并提供了自定义指令,允许用户进行一些底层的 DOM 操作。

举个实际的例子——图片懒加载。图片懒加载是一种常见性能优化的方式,由于它只去加载可视区域图片,能减少很多不必要的请求,极大的提升用户体验。

而图片懒加载的实现原理也非常简单,在图片没进入可视区域的时候,我们只需要让 img 标签的 src 属性指向一张默认图片,在它进入可视区后,再替换它的 src 指向真实图片地址即可。

如果我们想在 Vue.js 的项目中实现图片懒加载,那么用自定义指令就再合适不过了,那么接下来就让我手把手带你用 Vue3 去实现一个图片懒加载的自定义指令 v-lazy。

插件
为了让这个指令方便地给多个项目使用,我们把它做成一个插件:

const lazyPlugin = {install (app, options) {app.directive('lazy', {// 指令对象})}
}export default lazyPlugin
复制代码

然后在项目中引用它:

import { createApp } from 'vue'
import App from './App.vue'
import lazyPlugin from 'vue3-lazy'createApp(App).use(lazyPlugin, {// 添加一些配置参数
})
复制代码

通常一个 Vue3 的插件会暴露 install 函数,当 app 实例 use 该插件时,就会执行该函数。在 install 函数内部,通过 app.directive 去注册一个全局指令,这样就可以在组件中使用它们了。

指令的实现
接下来我们要做的就是实现该指令对象,一个指令定义对象可以提供多个钩子函数,比如 mounted、updated、unmounted 等,我们可以在合适的钩子函数中编写相应的代码来实现需求。

在编写代码前,我们不妨思考一下实现图片懒加载的几个关键步骤。

图片的管理
管理图片的 DOM、真实的 src、预加载的 url、加载的状态以及图片的加载。

可视区的判断
判断图片是否进入可视区域。

关于图片的管理,我们设计了 ImageManager 类:

const State = {loading: 0,loaded: 1,error: 2
}export class ImageManager {constructor(options) {this.el = options.elthis.src = options.srcthis.state = State.loadingthis.loading = options.loadingthis.error = options.errorthis.render(this.loading)}render() {this.el.setAttribute('src', src)}load(next) {if (this.state > State.loading) {return}this.renderSrc(next)}renderSrc(next) {loadImage(this.src).then(() => {this.state = State.loadedthis.render(this.src)next && next()}).catch((e) => {this.state = State.errorthis.render(this.error)console.warn(`load failed with src image(${this.src}) and the error msg is ${e.message}`)next && next()})}
}export default function loadImage (src) {return new Promise((resolve, reject) => {const image = new Image()image.onload = function () {resolve()dispose()}image.onerror = function (e) {reject(e)dispose()}image.src = srcfunction dispose () {image.onload = image.onerror = null}})
}
复制代码

首先,对于图片而言,它有三种状态,加载中、加载完成和加载失败。

当 ImageManager 实例化的时候,除了初始化一些数据,还会把它对应的 img 标签的 src 执行加载中的图片 loading,这就相当于默认加载的图片。

当执行 ImageManager 对象的 load 方法时,就会判断图片的状态,如果仍然在加载中,则去加载它的真实 src,这里用到了 loadImage 图片预加载技术实现去请求 src 图片,成功后再替换 img 标签的 src,并修改状态,这样就完成了图片真实地址的加载。

有了图片管理器,接下来我们就需要实现可视区的判断以及对多个图片的管理器的管理,设计 Lazy 类:

const DEFAULT_URL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'export default class Lazy {constructor(options) {this.managerQueue = []this.initIntersectionObserver()this.loading = options.loading || DEFAULT_URLthis.error = options.error || DEFAULT_URL}add(el, binding) {const src = binding.valueconst manager = new ImageManager({el,src,loading: this.loading,error: this.error})this.managerQueue.push(manager)this.observer.observe(el)}initIntersectionObserver() {this.observer = new IntersectionObserver((entries) => {entries.forEach((entry) => {if (entry.isIntersecting) {const manager = this.managerQueue.find((manager) => {return manager.el === entry.target})if (manager) {if (manager.state === State.loaded) {this.removeManager(manager)return}manager.load()}}})}, {rootMargin: '0px',threshold: 0})}removeManager(manager) {const index = this.managerQueue.indexOf(manager)if (index > -1) {this.managerQueue.splice(index, 1)}if (this.observer) {this.observer.unobserve(manager.el)}}
}const lazyPlugin = {install (app, options) {const lazy = new Lazy(options)app.directive('lazy', {mounted: lazy.add.bind(lazy)})}
}
复制代码

这样每当图片元素绑定 v-lazy 指令,且在 mounted 钩子函数执行的时候,就会执行 Lazy 对象的 add 方法,其中第一个参数 el 对应的就是图片对应的 DOM 元素对象,第二个参数 binding 就是指令对象绑定的值,比如:

<img class="avatar" v-lazy="item.pic">
复制代码

其中 item.pic 对应的就是指令绑定的值,因此通过 binding.value 就可以获取到图片的真实地址。

有了图片的 DOM 元素对象以及真实图片地址后,就可以根据它们创建一个图片管理器对象,并添加到 managerQueue 中,同时对该图片 DOM 元素进行可视区的观察。

而对于图片进入可视区的判断,主要利用了 IntersectionObserver API,它对应的回调函数的参数 entries,是 IntersectionObserverEntry 对象数组。 当观测的元素可见比例超过指定阈值时,就会执行该回调函数,对 entries 进行遍历,拿到每一个 entry,然后判断 entry.isIntersecting 是否为 true,如果是则说明 entry 对象对应的 DOM 元素进入了可视区。

然后就根据 DOM 元素的比对从 managerQueue 中找到对应的 manager,并且判断它对应图片的加载状态。

如果图片是加载中的状态,则此时执行 manager.load 函数去完成真实图片的加载;如果是已加载状态,则直接从 managerQueue 中移除其对应的管理器,并且停止对图片 DOM 元素的观察。

目前,我们实现了图片元素挂载到页面后,延时加载的一系列处理。不过,当元素从页面卸载后,也需要执行一些清理的操作:

export default class Lazy {remove(el) {const manager = this.managerQueue.find((manager) => {return manager.el === el})if (manager) {this.removeManager(manager)}}
}const lazyPlugin = {install (app, options) {const lazy = new Lazy(options)app.directive('lazy', {mounted: lazy.add.bind(lazy),remove: lazy.remove.bind(lazy)})}
}
复制代码

当元素被卸载后,其对应的图片管理器也会从 managerQueue 中被移除,并且停止对图片 DOM 元素的观察。

此外,如果动态修改了 v-lazy 指令绑定的值,也就是真实图片的请求地址,那么指令内部也应该做对应的修改:

export default class ImageManager {update (src) {const currentSrc = this.srcif (src !== currentSrc) {this.src = srcthis.state = State.loading}}
}export default class Lazy {update (el, binding) {const src = binding.valueconst manager = this.managerQueue.find((manager) => {return manager.el === el})if (manager) {manager.update(src)}}
}const lazyPlugin = {install (app, options) {const lazy = new Lazy(options)app.directive('lazy', {mounted: lazy.add.bind(lazy),remove: lazy.remove.bind(lazy),update: lazy.update.bind(lazy)})}
}
复制代码

至此,我们已经实现了一个简单的图片懒加载指令,在这个基础上,还能做一些优化吗?

指令的优化
在实现图片的真实 url 的加载过程中,我们使用了 loadImage 做图片预加载,那么显然对于相同 url 的多张图片,预加载只需要做一次即可。

为了实现上述需求,我们可以在 Lazy 模块内部创建一个缓存 cache:

export default class Lazy {constructor(options) {// ...this.cache = new Set()}
}
复制代码

然后在创建 ImageManager 实例的时候,把该缓存传入:

const manager = new ImageManager({el,src,loading: this.loading,error: this.error,cache: this.cache
})
复制代码

然后对 ImageManager 做如下修改:

export default class ImageManager {load(next) {if (this.state > State.loading) {return}if (this.cache.has(this.src)) {this.state = State.loadedthis.render(this.src)return}this.renderSrc(next)}renderSrc(next) {loadImage(this.src).then(() => {this.state = State.loadedthis.render(this.src)next && next()}).catch((e) => {this.state = State.errorthis.cache.add(this.src)this.render(this.error)console.warn(`load failed with src image(${this.src}) and the error msg is ${e.message}`)next && next()})  }
}
复制代码

在每次执行 load 前从缓存中判断是否已存在,然后在执行 loadImage 预加载图片成功后更新缓存。

通过这种空间换时间的手段,就避免了一些重复的 url 请求,达到了优化性能的目的。

最后
如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163相互学习,我们会有专业的技术答疑解惑

如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star: https://gitee.com/ZhongBangKeJi/CRMEB不胜感激 !

手把手带你写一个 Vue3 的自定义指令相关推荐

  1. 手把手带你写一个JavaScript类型判断小工具

    业务写了很多,依然不是前端大神,我相信这是很多'入坑'前端开发同学的迷茫之处,个人觉得前端职业发展是有路径可寻的,前期写业务是一个积累过程,后期提炼总结,比如编程思想,父子类的原型继承,还是对象之间的 ...

  2. 【NLP】Pyhon+讯飞开放平台:​手把手带你写一个智能语音播报系统

    手把手带你写一个智能语音播报系统. 微信扫码登陆讯飞开放平台:https://www.xfyun.cn/ 完成个人认证. 在控制台创建应用,注意应用名称全库查重,很容易跟别人重复. 可查看到pytho ...

  3. 手把手带你写一个中断输入设备驱动

    今天群里有人问,要开始驱动开发的话从什么开始比较好. 我说,应该开始去摸索触摸屏驱动,现在我想了下,触摸屏驱动可能会难了些,但是从一个GPIO开始,我觉得一定是一件很容易的事情. 所以这篇文章就来了. ...

  4. 手把手带你写一个中断输入设备驱动~

    今天来分享一下以前写一个中断输入设备驱动案例,希望对有需要的朋友能有所帮助. 背景介绍 在一个项目中,有这样一个需求: 主控芯片采用ZYNQ,需要采集外部一个脉冲编码输入信号,这个信号是一个脉冲波形, ...

  5. 手把手教你写一个Vue3组件库但是乞丐版

    好久没写文章了,最近在研究一些组件库的实现方法,分享一下.在这我这篇文章之前其实已经有一篇文章讲了Vue如何打包组件库了(最底部),但是这篇文章一是没有源码二是Vue3和Vue2的组件库写法有点不一样 ...

  6. 手把手带你写一个中高级程序员必会的分布式RPC框架

    一.概述 什么是RPC? 远程服务调用 官方:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想 通俗一点:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调 ...

  7. 手把手教你写一个spring IOC容器

    本文分享自华为云社区<手把手教你写一个spring IOC容器>,原文作者:技术火炬手. spring框架的基础核心和起点毫无疑问就是IOC,IOC作为spring容器提供的核心技术,成功 ...

  8. 手把手教你写一个Matlab App(一)

    对于传统工科的学生用的最多的编程软件应该就是matlab,其集成度高,计算能力强,容易上手,颇受大众青睐.今天挖的这个新坑,主要是分享用matlab app designer设计GUI界面的一些方法和 ...

  9. 手把手带你撸一个校园APP(六):失物招领二手交易模块

    代码经过简单的整理,已经放到Github上了.https://github.com/zhengweichao/Hevttc 回首来看,代码质量一般,里面也有各种逻辑问题,还望各位看官海涵.接下来有时间 ...

最新文章

  1. Linux中/proc目录下文件详解
  2. 在 Scale Up 中使用 Health Check - 每天5分钟玩转 Docker 容器技术(145)
  3. HDU-1212 Big Number JAVA又出毛病了
  4. SAP Spartacus Organization Unit List三个按钮的技术实现
  5. sql loader 参数详解
  6. 哈夫曼树编码与译码(完整C/C++实现代码)
  7. HDU3017:Lucas定理及详解
  8. 【HTML——代码雨】(效果+代码)
  9. [答疑]什么是“消极需求“
  10. msp430是什么?
  11. w10计算机右键管理,电脑右键新建菜单管理,win10右键新建菜单管理-
  12. Maven的作用通俗介绍
  13. JQuery中$(document)、$(document).ready()是什么意思?
  14. 【硬件篇】计算机起源
  15. win10家庭版的常见问题1
  16. 【IOI2000】 邮局
  17. 如何将电脑设置为定时关机?
  18. 400. 第 N 位数字【我亦无他唯手熟尔】
  19. mac 下载软件无法将程序拖移至app 出现错误
  20. 什么是ITSM Master?

热门文章

  1. windows update更新返回错误码统计(WUSA.exe)
  2. LaTeX 使用:itemize,enumerate,description 用法
  3. 华硕ROG品牌机安装ubuntu双系统
  4. 华硕rog笔记本怎么设置u盘启动重装系统
  5. [bzoj 4976]宝石镶嵌
  6. 解决commander选项取值问题
  7. jquery 中加入html代码,jquery实现动态添加html代码
  8. C#与安捷伦34970a巡检仪通讯
  9. 用python编写蝴蝶曲线的动画
  10. 网络营销第一课:市场营销基础