Vue3 的自定義指令圖片懶加載

手把手帶你寫(xiě)一個(gè) Vue3 的自定義指令

以下文章來(lái)源于老黃的前端私房菜 褒傅,作者黃軼黃老師

背景

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

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

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

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

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

插件

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

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

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)部苛秕,通過(guò) app.directive 去注冊(cè)一個(gè)全局指令找默,這樣就可以在組件中使用它們了吼驶。

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

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

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

  • 圖片的管理

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

  • 可視區(qū)的判斷

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

關(guān)于圖片的管理温赔,我們?cè)O(shè)計(jì)了 ImageManager 類(lèi):

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
    }
  })
}

首先鬼癣,對(duì)于圖片而言,它有三種狀態(tài)骇窍,加載中腹纳、加載完成和加載失敗。

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

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

有了圖片管理器贝咙,接下來(lái)我們就需要實(shí)現(xiàn)可視區(qū)的判斷以及對(duì)多個(gè)圖片的管理器的管理,設(shè)計(jì) Lazy 類(lèi):

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 對(duì)象的 add 方法吊说,其中第一個(gè)參數(shù) el 對(duì)應(yīng)的就是圖片對(duì)應(yīng)的 DOM 元素對(duì)象,第二個(gè)參數(shù) binding 就是指令對(duì)象綁定的值厅贪,比如:

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

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

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

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

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

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

目前荧库,我們實(shí)現(xiàn)了圖片元素掛載到頁(yè)面后分衫,延時(shí)加載的一系列處理。不過(guò)牵现,當(dāng)元素從頁(yè)面卸載后邀桑,也需要執(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)元素被卸載后概漱,其對(duì)應(yīng)的圖片管理器也會(huì)從 managerQueue 中被移除,并且停止對(duì)圖片 DOM 元素的觀察竿裂。

此外腻异,如果動(dòng)態(tài)修改了 v-lazy 指令綁定的值,也就是真實(shí)圖片的請(qǐng)求地址影斑,那么指令內(nèi)部也應(yīng)該做對(duì)應(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è)簡(jiǎn)單的圖片懶加載指令残邀,在這個(gè)基礎(chǔ)上芥挣,還能做一些優(yōu)化嗎?

指令的優(yōu)化

在實(shí)現(xiàn)圖片的真實(shí) url 的加載過(guò)程中空另,我們使用了 loadImage 做圖片預(yù)加載蹋砚,那么顯然對(duì)于相同 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)魅耄?/p>

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

然后對(duì) 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ù)加載圖片成功后更新緩存油狂。

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

總結(jié)

懶加載圖片指令完整的指令實(shí)現(xiàn)味咳,可以在 vue3-lazy 中查看, 在我的課程《Vue3 開(kāi)發(fā)高質(zhì)量音樂(lè) Web app》中也有應(yīng)用责嚷。

懶加載圖片指令的核心是應(yīng)用了 IntersectionObserver API 來(lái)判斷圖片是否進(jìn)入可視區(qū)再层,該特性在現(xiàn)代瀏覽器中都支持堡纬,但 IE 瀏覽器不支持,此時(shí)可以通過(guò)監(jiān)聽(tīng)圖片可滾動(dòng)父元素的一些事件如 scroll蛋济、resize 等碗旅,然后通過(guò)一些 DOM 計(jì)算來(lái)判斷圖片元素是否進(jìn)入可視區(qū)镜悉。不過(guò) Vue3 已經(jīng)明確不再支持 IE侣肄,那么僅僅使用 IntersectionObserver API 就足夠了。

除了懶加載圖片自定義指令中用到的鉤子函數(shù)吼具,Vue3 的自定義指令還提供了一些其它的鉤子函數(shù)矩距,你未來(lái)在開(kāi)發(fā)自定義指令時(shí)锥债,可以去查閱它的文檔痊臭,在適合的鉤子函數(shù)去編寫(xiě)相應(yīng)的代碼邏輯趣兄。

相關(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 自定義指令文檔: https://v3.cn.vuejs.org/guide/custom-directive.html

推薦閱讀:

關(guān)注小編不定時(shí)更新更多精彩內(nèi)容

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市迄汛,隨后出現(xiàn)的幾起案子骤视,更是在濱河造成了極大的恐慌专酗,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沉填,死亡現(xiàn)場(chǎng)離奇詭異翼闹,居然都是意外死亡蒋纬,警方通過(guò)查閱死者的電腦和手機(jī)颠锉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)琼掠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)停撞,“玉大人,你說(shuō)我怎么就攤上這事艰猬」谔遥” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)樱报。 經(jīng)常有香客問(wèn)我迹蛤,道長(zhǎng),這世上最難降的妖魔是什么嚷量? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任津肛,我火速辦了婚禮身坐,結(jié)果婚禮上落包,老公的妹妹穿的比我還像新娘咐蝇。我一直安慰自己,他們只是感情好抹腿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布警绩。 她就那樣靜靜地躺著盅称,像睡著了一般。 火紅的嫁衣襯著肌膚如雪岸霹。 梳的紋絲不亂的頭發(fā)上将饺,一...
    開(kāi)封第一講書(shū)人閱讀 51,590評(píng)論 1 305
  • 那天予弧,我揣著相機(jī)與錄音桌肴,去河邊找鬼。 笑死水醋,一個(gè)胖子當(dāng)著我的面吹牛彪置,可吹牛的內(nèi)容都是我干的拳魁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼救恨!你這毒婦竟也來(lái)了释树?” 一聲冷哼從身側(cè)響起奢啥,我...
    開(kāi)封第一講書(shū)人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤校翔,失蹤者是張志新(化名)和其女友劉穎正驻,沒(méi)想到半個(gè)月后姑曙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年焕梅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贞言。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阀蒂。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡该窗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蚤霞,到底是詐尸還是另有隱情酗失,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布昧绣,位于F島的核電站规肴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏夜畴。R本人自食惡果不足惜拖刃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望序调。 院中可真熱鬧,春花似錦兔簇、人聲如沸发绢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)边酒。三九已至,卻和暖如春狸窘,著一層夾襖步出監(jiān)牢的瞬間墩朦,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工翻擒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留氓涣,地道東北人牛哺。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像劳吠,于是被迫代替她去往敵國(guó)和親引润。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 定義自定義指令js 原理就是一開(kāi)始不給src附圖片地址的值痒玩,等到進(jìn)入指定區(qū)域再給它賦值淳附。 這里用到了一個(gè)接口I...
    沃德麻鴨閱讀 920評(píng)論 0 1
  • 什么是圖片懶加載 進(jìn)入頁(yè)面的時(shí)候,只請(qǐng)求可視區(qū)域的圖片資源 懶加載原理 圖片的標(biāo)簽是 img 標(biāo)簽蠢古,圖片的來(lái)源主要...
    歡欣的膜笛閱讀 1,227評(píng)論 0 1
  • v-LazyLoad 背景:在類(lèi)電商類(lèi)項(xiàng)目奴曙,往往存在大量的圖片,如 banner 廣告圖草讶,菜單導(dǎo)航圖洽糟,美團(tuán)等商家列...
    HTAO濤閱讀 390評(píng)論 0 0
  • 在 Vue,除了核心功能默認(rèn)內(nèi)置的指令 ( v-model 和 v-show )堕战,Vue 也允許注冊(cè)自定義指令脊框。它...
    半壺雪閱讀 250評(píng)論 0 0
  • 在 Vue,除了核心功能默認(rèn)內(nèi)置的指令 ( v-model 和 v-show )践啄,Vue 也允許注冊(cè) 自定義指令浇雹。...
    抽瘋的稻草繩閱讀 414評(píng)論 0 4