如何寫一個 Vue3 的自定義指令
前端巔峰
以下文章來源于微信公眾號前端巔峰
背景
眾所周知,Vue.js 的核心思想是數(shù)據(jù)驅(qū)動 + 組件化,通常我們開發(fā)頁面的過程就是在編寫一些組件,并且通過修改數(shù)據(jù)的方式來驅(qū)動組件的重新渲染。在這個過程中,我們不需要去手動操作 DOM。
然而在有些場景下,我們還是避免不了要操作 DOM。由于 Vue.js 框架接管了 DOM 元素的創(chuàng)建和更新的過程,因此它可以在 DOM 元素的生命周期內(nèi)注入用戶的代碼,于是 Vue.js
設計并提供了自定義指令,允許用戶進行一些底層的 DOM 操作。
舉個實際的例子——圖片懶加載。圖片懶加載是一種常見性能優(yōu)化的方式,由于它只去加載可視區(qū)域圖片,能減少很多不必要的請求,極大的提升用戶體驗。
而圖片懶加載的實現(xiàn)原理也非常簡單,在圖片沒進入可視區(qū)域的時候,我們只需要讓 img 標簽的 src 屬性指向一張默認圖片,在它進入可視區(qū)后,再替換它的 src 指向真實圖片地址即可。
如果我們想在 Vue.js 的項目中實現(xiàn)圖片懶加載,那么用自定義指令就再合適不過了,那么接下來就讓我手把手帶你用 Vue3 去實現(xiàn)一個圖片懶加載的自定義指令 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, { ? // 添加一些配置參數(shù) })
通常一個 Vue3 的插件會暴露 install 函數(shù),當 app 實例 use 該插件時,就會執(zhí)行該函數(shù)。在 install 函數(shù)內(nèi)部,通過 app.directive 去注冊一個全局指令,這樣就可以在組件中使用它們了。
指令的實現(xiàn)
接下來我們要做的就是實現(xiàn)該指令對象,一個指令定義對象可以提供多個鉤子函數(shù),比如 mounted
、updated
、unmounted
等,我們可以在合適的鉤子函數(shù)中編寫相應的代碼來實現(xiàn)需求。
在編寫代碼前,我們不妨思考一下實現(xiàn)圖片懶加載的幾個關(guān)鍵步驟。
圖片的管理:
管理圖片的 DOM、真實的 src、預加載的 url、加載的狀態(tài)以及圖片的加載。
可視區(qū)的判斷:
判斷圖片是否進入可視區(qū)域。
關(guān)于圖片的管理,我們設計了 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 ? ? } ? }) }
首先,對于圖片而言,它有三種狀態(tài),加載中、加載完成和加載失敗。
當 ImageManager
實例化的時候,除了初始化一些數(shù)據(jù),還會把它對應的 img 標簽的 src 執(zhí)行加載中的圖片 loading
,這就相當于默認加載的圖片。
當執(zhí)行 ImageManager
對象的 load 方法時,就會判斷圖片的狀態(tài),如果仍然在加載中,則去加載它的真實 src,這里用到了 loadImage
圖片預加載技術(shù)實現(xiàn)去請求 src 圖片,成功后再替換 img 標簽的 src,并修改狀態(tài),這樣就完成了圖片真實地址的加載。
有了圖片管理器,接下來我們就需要實現(xiàn)可視區(qū)的判斷以及對多個圖片的管理器的管理,設計 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
鉤子函數(shù)執(zhí)行的時候,就會執(zhí)行 Lazy 對象的 add 方法,其中第一個參數(shù) el 對應的就是圖片對應的 DOM 元素對象,第二個參數(shù) binding
就是指令對象綁定的值,比如:
<img class="avatar" v-lazy="item.pic">
其中 item.pic 對應的就是指令綁定的值,因此通過binding.value
就可以獲取到圖片的真實地址。
有了圖片的 DOM 元素對象以及真實圖片地址后,就可以根據(jù)它們創(chuàng)建一個圖片管理器對象,并添加到 managerQueue
中,同時對該圖片 DOM 元素進行可視區(qū)的觀察。
而對于圖片進入可視區(qū)的判斷,主要利用了 IntersectionObserver API,
它對應的回調(diào)函數(shù)的參數(shù) entries,是 IntersectionObserverEntry
對象數(shù)組。當觀測的元素可見比例超過指定閾值時,就會執(zhí)行該回調(diào)函數(shù),對 entries 進行遍歷,拿到每一個 entry,然后判斷 entry.isIntersecting
是否為 true,如果是則說明 entry 對象對應的 DOM 元素進入了可視區(qū)。
然后就根據(jù) DOM 元素的比對從 managerQueue
中找到對應的 manager
,并且判斷它對應圖片的加載狀態(tài)。
如果圖片是加載中的狀態(tài),則此時執(zhí)行manager.load
函數(shù)去完成真實圖片的加載;如果是已加載狀態(tài),則直接從 managerQueue
中移除其對應的管理器,并且停止對圖片 DOM 元素的觀察。
目前,我們實現(xiàn)了圖片元素掛載到頁面后,延時加載的一系列處理。不過,當元素從頁面卸載后,也需要執(zhí)行一些清理的操作:
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 元素的觀察。
此外,如果動態(tài)修改了 v-lazy 指令綁定的值,也就是真實圖片的請求地址,那么指令內(nèi)部也應該做對應的修改:
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) ? ? }) ? } }
至此,我們已經(jīng)實現(xiàn)了一個簡單的圖片懶加載指令,在這個基礎上,還能做一些優(yōu)化嗎?
指令的優(yōu)化
在實現(xiàn)圖片的真實 url 的加載過程中,我們使用了 loadImage
做圖片預加載,那么顯然對于相同 url 的多張圖片,預加載只需要做一次即可。
為了實現(xiàn)上述需求,我們可以在 Lazy 模塊內(nèi)部創(chuàng)建一個緩存 cache:
export default class Lazy { ? constructor(options) { ? ? // ... ? ? this.cache = new Set() ? } }
然后在創(chuàng)建 ImageManager
實例的時候,把該緩存?zhèn)魅耄?/strong>
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() ? ? }) ? ? } }
在每次執(zhí)行 load
前從緩存中判斷是否已存在,然后在執(zhí)行 loadImage
預加載圖片成功后更新緩存。
通過這種空間換時間的手段,就避免了一些重復的 url 請求,達到了優(yōu)化性能的目的。
總結(jié):
懶加載圖片指令完整的指令實現(xiàn),可以在 vue3-lazy 中查看, 在我的課程《Vue3 開發(fā)高質(zhì)量音樂 Web app》中也有應用。
懶加載圖片指令的核心是應用了 IntersectionObserver API
來判斷圖片是否進入可視區(qū),該特性在現(xiàn)代瀏覽器中都支持,但 IE 瀏覽器不支持,此時可以通過監(jiān)聽圖片可滾動父元素的一些事件如 scroll、resize 等,然后通過一些 DOM 計算來判斷圖片元素是否進入可視區(qū)。不過 Vue3 已經(jīng)明確不再支持 IE,那么僅僅使用 IntersectionObserver API 就足夠了。
除了懶加載圖片自定義指令中用到的鉤子函數(shù),Vue3 的自定義指令還提供了一些其它的鉤子函數(shù),你未來在開發(fā)自定義指令時,可以去查閱它的文檔,在適合的鉤子函數(shù)去編寫相應的代碼邏輯。
相關(guān)鏈接:
[1] IntersectionObserver: https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver
[2] vue3-lazy: https://github.com/ustbhuangyi/vue3-lazy
[3] 《Vue3 開發(fā)高質(zhì)量音樂 Web app》:https://coding.imooc.com/class/503.html
[4] Vue3 自定義指令文檔: https://v3.cn.vuejs.org/guide/custom-directive.html
相關(guān)文章
vue3實現(xiàn)監(jiān)聽store中state狀態(tài)變化的簡單方法
這篇文章主要給大家介紹了關(guān)于vue3實現(xiàn)監(jiān)聽store中state狀態(tài)變化的簡單方法,store是一個狀態(tài)管理工具,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2023-10-10