1. 程式人生 > 程式設計 >如何寫一個 Vue3 的自定義指令

如何寫一個 Vue3 的自定義指令

目錄
  • 背景
  • 外掛
  • 指令的實現

前端巔峰
以下文章來源於微信公眾號前端巔峰

背景

眾所周知,. 的核心思想是資料驅動 + 元件化,通常我們開發頁面的過程就是在編寫一些元件,並且通過修改資料的方式來驅動元件的重新渲染。在這個過程中,我們不需要去手動操作 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

updatedunmounted 等,我們可以在合適的鉤子函式中編寫相應的程式碼來實現需求。

在編寫程式碼前,我們不妨思考一下實現圖片懶載入的幾個關鍵步驟。

圖片的管理:

管理圖片的 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 > Statehttp://www.cppcns.com.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 函式去完成真www.cppcns.com實圖片的載入;如果是已載入狀態,則直接從 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,{
   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 manag客棧er = this.managerQueue.find((manager) => {
   return manager.el === el
  })
  if (manager) {
   manager.update(src)
  }
 }  
}

const lazyPlugin = {
 install (app,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,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) {www.cppcns.com
  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://.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