如何写一个 Vue3 的自定义指令

目录
  • 背景
  • 插件
  • 指令的实现

前端巅峰 
以下文章来源于微信公众号前端巅峰

背景

众所周知,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 去注册一个全局指令,这样就可以在组件中使用它们了。

指令的实现

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

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

图片的管理:

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

可视区的判断:

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

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

const State = {
  loading: 0,
  loaded: 1,
  error: 2
}

export class ImageManager {
  constructor(options) {
    this.el = options.el
    this.src = options.src
    this.state = State.loading
    this.loading = options.loading
    this.error = options.error
    
    this.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.loaded
      this.render(this.src)
      next && next()
    }).catch((e) => {
      this.state = State.error
      this.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 = src

    function 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_URL
    this.error = options.error || DEFAULT_URL
  }
  add(el, binding) {
    const src = binding.value
    
    const 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.src
    if (src !== currentSrc) {
      this.src = src
      this.state = State.loading
    }
  }  
}

export default class Lazy {
  update (el, binding) {
    const src = binding.value
    const 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.loaded
      this.render(this.src)
      return
    }
    this.renderSrc(next)
  }
  renderSrc(next) {
    loadImage(this.src).then(() => {
      this.state = State.loaded
      this.render(this.src)
      next && next()
    }).catch((e) => {
      this.state = State.error
      this.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 请求,达到了优化性能的目的。

总结:

懒加载图片指令完整的指令实现,可以在 vue3-lazy 中查看, 在我的课程《Vue3 开发高质量音乐 Web app》中也有应用。

懒加载图片指令的核心是应用了 IntersectionObserver API 来判断图片是否进入可视区,该特性在现代浏览器中都支持,但 IE 浏览器不支持,此时可以通过监听图片可滚动父元素的一些事件如 scroll、resize 等,然后通过一些 DOM 计算来判断图片元素是否进入可视区。不过 Vue3 已经明确不再支持 IE,那么仅仅使用 IntersectionObserver API 就足够了。

除了懒加载图片自定义指令中用到的钩子函数,Vue3 的自定义指令还提供了一些其它的钩子函数,你未来在开发自定义指令时,可以去查阅它的文档,在适合的钩子函数去编写相应的代码逻辑。

相关链接:

[1] IntersectionObserver: https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver

[2] vue3-lazy: https://github.com/ustbhuangyi/vue3-lazy

[3] 《Vue3 开发高质量音乐 Web app》:https://coding.imooc.com/class/503.html

[4] Vue3 自定义指令文档: https://v3.cn.vuejs.org/guide/custom-directive.html

(0)

相关推荐

  • vue3.0自定义指令(drectives)知识点总结

    在大多数情况下,你都可以操作数据来修改视图,或者反之.但是还是避免不了偶尔要操作原生 DOM,这时候,你就能用到自定义指令. 举个例子,你想让页面的文本框自动聚焦,在没有学习自定义指令的时候,我们可能会这么做. const app = Vue.createApp({ mounted(){ this.$refs.input.focus(); }, template: `<input type="text" ref="input" />`, }); 在mou

  • vue3 自定义指令详情

    目录 一.注册自定义指令 1.1.全局自定义指令 1.2.局部自定义指令 二.自定义指令中的生命周期钩子函数 三.自定义指令钩子函数的参数 四.自定义指令参数 一.注册自定义指令 以下实例都是实现一个输入框自动获取焦点的自定义指令. 1.1.全局自定义指令 在vue2中,全局自定义指令通过 directive 挂载到 Vue 对象上,使用 Vue.directive('name',opt). 实例1:Vue2 全局自定义指令 Vue.directive('focus',{ inserted:(e

  • 理解Vue2.x和Vue3.x自定义指令用法及钩子函数原理

    目录 Vue2.x用法 全局注册 局部注册 使用 钩子函数 钩子函数的参数 Vue3.x用法 全局注册 局部注册 使用 钩子函数 较 Vue2.x 相比, 钩子函数有变化 Vue2.x用法 全局注册 Vue.directive( 指令名, { 自定义指令生命周期 } ) 局部注册 directives: { 指令名, { 自定义指令生命周期 } } 使用 v-指令名: 属性名.修饰符="value值" 钩子函数 bind - 自定义指令绑定到 DOM 后调用. 只调用一次, 注意: 只

  • Vue3.0写自定义指令的简单步骤记录

    前言 vue中提供了丰富的内置指令,如v-if,v-bind,v-on......,除此之外我们还可以通过Vue.directive({})或者directives:{}来定义指令 在开始学习之前我们应该理解,自定义指令的应用场景,任何功能的开发都是为了解决具体的问题的, 通过自定义指令,我们可以对DOM进行更多的底层操作,这样不仅可以在某些场景下为我们提供快速解决问题的思路,而且让我们对vue的底层有了进一步的了解 第一步 在main.js 在src下简历对应的文件夹 import Direc

  • 如何写一个 Vue3 的自定义指令

    目录 背景 插件 指令的实现 前端巅峰 以下文章来源于微信公众号前端巅峰 背景 众所周知,Vue.js 的核心思想是数据驱动 + 组件化,通常我们开发页面的过程就是在编写一些组件,并且通过修改数据的方式来驱动组件的重新渲染.在这个过程中,我们不需要去手动操作 DOM. 然而在有些场景下,我们还是避免不了要操作 DOM.由于 Vue.js 框架接管了 DOM 元素的创建和更新的过程,因此它可以在 DOM 元素的生命周期内注入用户的代码,于是 Vue.js 设计并提供了自定义指令,允许用户进行一些底

  • vue3使用自定义指令实现el dialog拖拽功能示例详解

    目录 实现el-dialog的拖拽功能 通过自定义指令实现拖拽功能 实现拖拽功能 使用方式 实现el-dialog的拖拽功能 这里指的是 element-plus 的el-dialog组件,一开始该组件并没有实现拖拽的功能,当然现在可以通过设置属性的方式实现拖拽. 自带的拖拽功能非常严谨,拖拽时判断是否拖拽出窗口,如果出去了会阻止拖拽. 如果自带的拖拽功能可以满足需求的话,可以跳过本文. 通过自定义指令实现拖拽功能 因为要自己操作dom(设置事件),所以感觉还是使用自定义指令更直接一些,而且对原

  • vue3的自定义指令directives实现

    目录 一.什么是自定义指令 二.指令的分类 三.指令的作用 四.指令的钩子 五.钩子参数 六.指令的使用 指令的参数和修饰符 一.什么是自定义指令 我们已经熟悉Vue内置的一系列指令 ,比如 v-model, v-show, v-if, v-for等等,自定义指令从命名上看主要区别于Vue自带的内置指令,我们可以创建自己想要的指令,使用必须以v-为前缀 二.指令的分类 局部指令:组件中通过directives选项实现,只能在当前组件中使用 全局指令:应用实例的directive方法,可以在任意组

  • Vue3 编写自定义指令插件的示例代码

    编写自定义插件 // src/plugins/directive.ts import type { App } from 'vue' // 插件选项的类型 interface Options { // 文本高亮选项 highlight?: { // 默认背景色 backgroundColor: string } } /** * 自定义指令 * @description 保证插件单一职责,当前插件只用于添加自定义指令 */ export default { install: (app: App,

  • 如何在Vue3中实现自定义指令(超详细!)

    目录 前言 生命周期 钩子的参数 简化形式 对象字面量 在组件上使用指令 几个实用的自定义指令 自动聚焦v-focus 防抖v-debounce 节流v-throttle 弹窗隐藏v-hide 总结 在开发Vue项目时,大多数人都会使用到Vue内置的一些指令,例如v-model.v-if等,在使用的时候不知道有没有想过自己也来实现一个指令呢.本文就以Vue3项目为基础,从原理.方法到实际案例.注意事项,尽可能细致的讲解如何自定义指令. 前言 我们需要明白为什么需要自定义一个指令,其实就是想更加简

  • vue3 自定义指令控制按钮权限的操作代码

    经过1个周的摸索和查阅资料,终于搞定VUE3中自定义指令,实现按钮级别的权限控制.当然,只是简单的对按钮进行隐藏和删除的dom操作比较容易,一直纠结的是当按钮无权限时,不是直接删除当前dom元素(button按钮),这样用户体验不好,让人感觉没有这个功能.为了提高用户体验,当该按钮无权使用时,使用el-tooltip功能进行提醒.以下是个人的做法,是否有弊端和不足,或者各位高手有更优的方案,欢迎指导和赐教! 1.总体效果如下: 2.permissionlist组件中的按钮设置为:增加.修改和删除

随机推荐