溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么寫一個(gè)Vue3的自定義指令

發(fā)布時(shí)間:2022-01-07 11:11:08 來源:億速云 閱讀:123 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容主要講解“怎么寫一個(gè)Vue3的自定義指令”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“怎么寫一個(gè)Vue3的自定義指令”吧!

背景

眾所周知,Vue.js 的核心思想是數(shù)據(jù)驅(qū)動(dòng) + 組件化,通常我們開發(fā)頁面的過程就是在編寫一些組件,并且通過修改數(shù)據(jù)的方式來驅(qū)動(dòng)組件的重新渲染。在這個(gè)過程中,我們不需要去手動(dòng)操作 DOM。

然而在有些場景下,我們還是避免不了要操作 DOM。由于 Vue.js 框架接管了 DOM 元素的創(chuàng)建和更新的過程,因此它可以在 DOM 元素的生命周期內(nèi)注入用戶的代碼,于是 Vue.js 設(shè)計(jì)并提供了自定義指令,允許用戶進(jìn)行一些底層的 DOM 操作。

舉個(gè)實(shí)際的例子——圖片懶加載。圖片懶加載是一種常見性能優(yōu)化的方式,由于它只去加載可視區(qū)域圖片,能減少很多不必要的請求,極大的提升用戶體驗(yàn)。

而圖片懶加載的實(shí)現(xiàn)原理也非常簡單,在圖片沒進(jìn)入可視區(qū)域的時(shí)候,我們只需要讓 img 標(biāo)簽的 src 屬性指向一張默認(rèn)圖片,在它進(jìn)入可視區(qū)后,再替換它的 src 指向真實(shí)圖片地址即可。

如果我們想在 Vue.js 的項(xiàng)目中實(shí)現(xiàn)圖片懶加載,那么用自定義指令就再合適不過了,那么接下來就讓我手把手帶你用 Vue3 去實(shí)現(xiàn)一個(gè)圖片懶加載的自定義指令 v-lazy。

插件

為了讓這個(gè)指令方便地給多個(gè)項(xiàng)目使用,我們把它做成一個(gè)插件:

const lazyPlugin = {
  install (app, options) {
    app.directive('lazy', {
      // 指令對象
    })
  }
}

export default lazyPlugin

然后在項(xiàng)目中引用它:

import { createApp } from 'vue'
import App from './App.vue'
import lazyPlugin from 'vue3-lazy'

createApp(App).use(lazyPlugin, {
  // 添加一些配置參數(shù)
})

通常一個(gè) Vue3 的插件會(huì)暴露 install 函數(shù),當(dāng) app 實(shí)例 use 該插件時(shí),就會(huì)執(zhí)行該函數(shù)。在 install 函數(shù)內(nèi)部,通過 app.directive 去注冊一個(gè)全局指令,這樣就可以在組件中使用它們了。

指令的實(shí)現(xiàn)

接下來我們要做的就是實(shí)現(xiàn)該指令對象,一個(gè)指令定義對象可以提供多個(gè)鉤子函數(shù),比如 mounted、updatedunmounted 等,我們可以在合適的鉤子函數(shù)中編寫相應(yīng)的代碼來實(shí)現(xiàn)需求。

在編寫代碼前,我們不妨思考一下實(shí)現(xiàn)圖片懶加載的幾個(gè)關(guān)鍵步驟。

圖片的管理:

管理圖片的 DOM、真實(shí)的 src、預(yù)加載的 url、加載的狀態(tài)以及圖片的加載。

可視區(qū)的判斷:

判斷圖片是否進(jìn)入可視區(qū)域。

關(guān)于圖片的管理,我們設(shè)計(jì)了 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),加載中、加載完成和加載失敗。

當(dāng) ImageManager 實(shí)例化的時(shí)候,除了初始化一些數(shù)據(jù),還會(huì)把它對應(yīng)的 img 標(biāo)簽的 src 執(zhí)行加載中的圖片 loading,這就相當(dāng)于默認(rèn)加載的圖片。

當(dāng)執(zhí)行 ImageManager 對象的 load 方法時(shí),就會(huì)判斷圖片的狀態(tài),如果仍然在加載中,則去加載它的真實(shí) src,這里用到了 loadImage 圖片預(yù)加載技術(shù)實(shí)現(xiàn)去請求 src 圖片,成功后再替換 img 標(biāo)簽的 src,并修改狀態(tài),這樣就完成了圖片真實(shí)地址的加載。

有了圖片管理器,接下來我們就需要實(shí)現(xiàn)可視區(qū)的判斷以及對多個(gè)圖片的管理器的管理,設(shè)計(jì) 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)
    })
  }
}

這樣每當(dāng)圖片元素綁定 v-lazy 指令,且在 mounted 鉤子函數(shù)執(zhí)行的時(shí)候,就會(huì)執(zhí)行 Lazy 對象的 add 方法,其中第一個(gè)參數(shù) el 對應(yīng)的就是圖片對應(yīng)的 DOM 元素對象,第二個(gè)參數(shù) binding 就是指令對象綁定的值,比如:

<img class="avatar" v-lazy="item.pic">

其中 item.pic 對應(yīng)的就是指令綁定的值,因此通過binding.value 就可以獲取到圖片的真實(shí)地址。

有了圖片的 DOM 元素對象以及真實(shí)圖片地址后,就可以根據(jù)它們創(chuàng)建一個(gè)圖片管理器對象,并添加到 managerQueue 中,同時(shí)對該圖片 DOM 元素進(jìn)行可視區(qū)的觀察。

而對于圖片進(jìn)入可視區(qū)的判斷,主要利用了 IntersectionObserver API,它對應(yīng)的回調(diào)函數(shù)的參數(shù) entries,是 IntersectionObserverEntry 對象數(shù)組。當(dāng)觀測的元素可見比例超過指定閾值時(shí),就會(huì)執(zhí)行該回調(diào)函數(shù),對 entries 進(jìn)行遍歷,拿到每一個(gè) entry,然后判斷 entry.isIntersecting 是否為 true,如果是則說明 entry 對象對應(yīng)的 DOM 元素進(jìn)入了可視區(qū)。

然后就根據(jù) DOM 元素的比對從 managerQueue 中找到對應(yīng)的 manager,并且判斷它對應(yīng)圖片的加載狀態(tài)。

如果圖片是加載中的狀態(tài),則此時(shí)執(zhí)行manager.load 函數(shù)去完成真實(shí)圖片的加載;如果是已加載狀態(tài),則直接從 managerQueue 中移除其對應(yīng)的管理器,并且停止對圖片 DOM 元素的觀察。

目前,我們實(shí)現(xiàn)了圖片元素掛載到頁面后,延時(shí)加載的一系列處理。不過,當(dāng)元素從頁面卸載后,也需要執(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)
    })
  }
}

當(dāng)元素被卸載后,其對應(yīng)的圖片管理器也會(huì)從 managerQueue 中被移除,并且停止對圖片 DOM 元素的觀察。

此外,如果動(dòng)態(tài)修改了 v-lazy 指令綁定的值,也就是真實(shí)圖片的請求地址,那么指令內(nèi)部也應(yīng)該做對應(yīng)的修改:

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)實(shí)現(xiàn)了一個(gè)簡單的圖片懶加載指令,在這個(gè)基礎(chǔ)上,還能做一些優(yōu)化嗎?

指令的優(yōu)化
在實(shí)現(xiàn)圖片的真實(shí) url 的加載過程中,我們使用了 loadImage 做圖片預(yù)加載,那么顯然對于相同 url 的多張圖片,預(yù)加載只需要做一次即可。

為了實(shí)現(xiàn)上述需求,我們可以在 Lazy 模塊內(nèi)部創(chuàng)建一個(gè)緩存 cache:

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

然后在創(chuàng)建 ImageManager 實(shí)例的時(shí)候,把該緩存?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 預(yù)加載圖片成功后更新緩存。

通過這種空間換時(shí)間的手段,就避免了一些重復(fù)的 url 請求,達(dá)到了優(yōu)化性能的目的。

到此,相信大家對“怎么寫一個(gè)Vue3的自定義指令”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI