Vue2 中自定義圖片懶載入指令 v-lazy

語言: CN / TW / HK

Vue2 中自定義圖片懶載入指令 v-lazy

由於我在開發的個人部落格前臺頁面時,想優化網站的響應速度,所以想實現圖片懶載入效果。

我是通過自定義指令v-lazy實現的,所以在這跟大家分享一下這個指令的開發流程及其難點的解決方法。

1.涉及到的主要知識講解

自定義圖片懶載入指令主要涉及以下三塊知識:

  • Vue2 中自定義指令
  • 使用事件匯流排進行模組之間的通訊
  • 使用到的 Web API
  • Element.clientHeight
  • Element.getBoundingClientRect()

下面我會對這些知識點進行一一介紹。

1.1 Vue2 中自定義指令

下面我只對自定義指令做簡單的介紹,詳細介紹大家可以參照Vue 官網 - 自定義指令

1.1.1 指令物件的鉤子函式

  • bind:只調用一次,指令第一次繫結到元素時呼叫。在這裡可以進行一次性的初始化設定。
  • inserted:被繫結元素插入父節點時呼叫 (僅保證父節點存在,但不一定已被插入文件中)。
  • update:所在元件的 VNode 更新時呼叫,但是可能發生在其子 VNode 更新之前。指令的值可能發生了改變,也可能沒有。可通過比較更新前後的值來忽略不必要的模板更新 (詳細的鉤子函式引數見下)。
  • componentUpdated:指令所在元件的 VNode 及其子 VNode 全部更新後呼叫。
  • unbind:只調用一次,指令與元素解綁時呼叫。

鉤子函式的引數主要有這四個el、binding、vnode、oldVnode

1.1.2 鉤子函式引數

  • el:指令所繫結的元素,可以用來直接操作 DOM。
  • binding:一個物件,包含以下 property:
  • name:指令名,不包括 v- 字首。
  • value:指令的繫結值,如:v-my-directive="1 + 1" 中,繫結值為 2。
  • oldValue:指令繫結的前一個值,僅在 update 和 componentUpdated 鉤子中可用。無論值是否改變都可用。
  • expression:字串形式的指令表示式。如 v-my-directive="1 + 1" 中,表示式為 "1 + 1"。
  • arg:傳給指令的引數,可選。例如 v-my-directive:foo 中,引數為 "foo"。
  • modifiers:一個包含修飾符的物件。例如:v-my-directive.foo.bar 中,修飾符物件為 { foo: true, bar: true }。
  • vnode:Vue 編譯生成的虛擬節點。移步 VNode API 來了解更多詳情。
  • oldVnode:上一個虛擬節點,僅在 update 和 componentUpdated 鉤子中可用。

1.2 使用事件匯流排進行模組之間的通訊

對事件匯流排不熟悉的朋友,可以參照該部落格什麼是 Vue 事件匯流排(EventBus)

  • 監聽事件總線上的事件---呼叫 $on 方法
  • 觸發事件總線上的事件---呼叫 $emit 方法
  • 取消監聽事件總線上的事件---呼叫 $off 方法

我們可以藉助 vue 示例來實現事件匯流排,也可以自行封裝;我使用了第一種方法。

因此事件匯流排配置檔案---eventBus.js的程式碼如下:

javascript import Vue from "vue"; const eventBus = new Vue({}); /* * 事件名:mainScroll * 含義:主區域滾動條位置變化後觸發 * 引數: * - 滾動的dom元素,如果是undefined,則表示dom元素已經不存在 */ //在Vue.prototype原型上註冊事件匯流排,方便vue例項物件監聽和觸發 Vue.prototype.$bus = eventBus; //匯出事件匯流排,方便在其他js模組監聽和觸發事件總線上的事件 export default eventBus;

1.3 使用到的 Web API

1.3.1 Element.clientHeight

首先Element.clientHeight是一個只讀屬性,具有以下特點:

  • 對於那些沒有定義 CSS 或者內聯佈局盒子的元素,該 API 會返回 0;
  • 對於根元素(html 元素)或怪異模式下的 body 元素,該 API 將返回視口高度(不包含任何滾動條)
  • 其他情況,該 API 會返回元素內部的高度(以畫素為單位),包含contentpadding,不包含bordermargin與水平滾動條(如果存在)。

另外改 API 會將獲取的值四捨五入取整數。如果你需要小數結果,可以使用 element.getBoundingClientRect()方法。

示例圖如下:

Element.clientHeight參考圖.png

該 API 的詳細文件可參照MDN - Element.clientHeight

1.3.2 Element.getBoundingClientRect()

Element.getBoundingClientRect()方法返回一個DOMRect物件,其提供了元素的大小及其相對於視口的位置。 該方法無引數,返回值為DOMRect物件,該物件的屬性以下幾個:

  • width:就是元素自身寬度
  • height: 元素自身高度
  • left(x):元素開始位置到視窗左邊的距離
  • right: 元素的右邊到視窗左邊的距離
  • bottom: 元素的下邊到視窗上邊的距離
  • top(y): 元素的上邊到視窗上邊的距離
  • x 和 y 相當於 left 和 top

示意圖如下:

Element.getBoundingClientRect()參考圖.png

該 API 的詳細文件可以參照MDN - Element.getBoundingClientRect()

2.圖片懶載入指令的基本介紹

2.1 最終的實現效果

最終效果如下圖:

圖片懶載入.gif

2.2 圖片懶載入指令的註冊與使用

由於在個人部落格系統中圖片懶載入指令使用的比較頻繁,使用我選擇了全域性註冊該指令。

另外因為我使用事件匯流排這方法來進行通訊,所以還需引入事件匯流排配置檔案---eventBus.js

所以 main.js入口檔案的程式碼如下:

javascript import Vue from "vue"; import App from "./App.vue"; import "./eventBus"; //引入事件匯流排 import vLazy from "./directives/lazy"; Vue.directive("lazy", vLazy); //全域性註冊指令 new Vue({ render: (h) => h(App), }).$mount("#app");

使用 v-lazy 指令的示例程式碼如下:

```Vue

```

3. 實現圖片懶載入的原理

要實現圖片懶載入效果,我們首先要思考以下四個關鍵問題:

  1. 如何監聽容器的滾動條的滾動?
  2. 使用自定義指令哪些鉤子函式?
  3. 如何判斷圖片 img 元素是否在使用者的可見範圍內?
  4. 如何處理圖片 img 元素的載入?

3.1 如何監聽容器的滾動條的滾動?

對於這問題,由於我的部落格系統在處理其他元件之間的傳值問題時,使用了事件匯流排方法,所以為了方便,我也使用這一方法,當然大家可以針對實際場景使用其他方法來解決這問題。

所以我們要在 v-lazy 圖片懶載入指令配置檔案---lazy.js檔案中監聽事件匯流排 eventBus 中的mainScroll事件,同時為了效能優化,我們需要進行 mainScroll 事件的事件防抖

其中事件防抖工具函式---debounce.js程式碼如下:

javascript /** * @param {Function} fn 需要進行防抖操作的事件函式 * @param {Number} duration 間隔時間 * @returns {Function} 已進行防抖的函式 */ export default function (fn, duration = 100) { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { fn(...args); }, duration); }; }

圖片懶載入指令配置檔案---lazy.js該部分程式碼如下:

```javascript import eventBus from "@/eventBus"; //引入事件匯流排 import { debounce } from "@/utils"; //引入函式防抖工具函式

// 呼叫setImages函式,就可以處理那些符合條件的圖片 function setImages() {}

//監聽事件匯流排中的mainScroll事件,該事件觸發時呼叫setImages函式來載入圖片 eventBus.$on("mainScroll", debounce(setImages, 50)); ```

3.2 使用自定義指令哪些鉤子函式?

經過場景分析,我選用了insertedunbind這兩個鉤子函式,當 img 元素剛插入父節點時收集 img 的資訊,並在內部使用一個 imgs 陣列儲存已收集到的資訊,當指令與元素解綁時,進行 imgs 陣列清空操作。

另外我們還需獲取圖片 img 元素的 DOM 節點和 src 屬性值

  • 由於我們將指令繫結到了 img'元素上,所以可通過自定義指令鉤子函樹中的el引數得到其 DOM 節點
  • 由於我們將 src 值傳給了指令,所以可通過bindings.value引數得到其 src 屬性值

所以此時圖片懶載入指令配置檔案---lazy.js該部分程式碼如下:

```javascript import eventBus from "@/eventBus"; //引入事件匯流排 import { debounce } from "@/utils"; //引入函式防抖工具函式

// 呼叫setImages函式,就可以處理那些符合條件的圖片 function setImages() {}

//監聽事件匯流排中的mainScroll事件,該事件觸發時呼叫setImages函式來載入圖片 eventBus.$on("mainScroll", debounce(setImages, 50));

//上面程式碼是3.1 如何監聽容器的滾動條的滾動? //下面程式碼是3.2 使用自定義指令哪些鉤子函式?

let imgs = []; //儲存收集到的的圖片資訊 當圖片載入好後刪除該圖片資訊

//呼叫setImage函式,就可以進行單張圖片的載入 function setImage(img) {}

export default { inserted(el, bindings) { //剛插入父節點時 收集img節點資訊 const img = { dom: el, //img 元素DOM節點 src: bindings.value, //img的src屬性值 }; imgs.push(img); //先將圖片資訊儲存到imgs陣列 setImage(img); // 立即判斷該圖片是否要載入 }, unbind(el) { //解綁時 刪除 imgs 中的所有圖片資訊 imgs = imgs.filter((img) => img.dom !== el); }, }; ```

3.3 如何判斷圖片 img 元素是否在使用者的可見範圍內?

對於上面這問題,我們先進行問題拆分:

  1. 獲得使用者的可見範圍(視口)

  2. 由於我的部落格系統只需考慮視口高度,所以我只使用了Element.clientHeight 這 API。(如果還需要考慮寬度就再使用Element.clientWidth)

  3. 獲得圖片 img 元素的位置資訊

  4. 我使用了Element.getBoundingClientRect()這 API。

  5. 判斷圖片 img 元素是否在視口內

  6. img.getBoundingClientRect().top > 0 時,說明圖片在視口內或視口下方

  7. 當 img.getBoundingClientRect().top <= document.documentElement.clientHeight 時,該 img 元素在視口內
  8. 反之則不在視口內
  9. img.getBoundingClientRect().top < 0 時,說明圖片在視口內或視口上方
  10. 當-img.getBoundingClientRect().top <= img.getBoundingClientRect().height 時,該 img 元素在視口內
  11. 反之則不在視口內

圖片懶載入指令配置檔案---lazy.js該部分程式碼如下:

```javascript import eventBus from "@/eventBus"; //引入事件匯流排 import { debounce } from "@/utils"; //引入函式防抖工具函式

let imgs = []; //儲存收集到的的圖片資訊

// 呼叫setImages函式,就可以處理那些符合條件的圖片 function setImages() { for (const img of imgs) { setImage(img); // 處理該圖片 } }

//監聽事件匯流排中的mainScroll事件,該事件觸發時呼叫setImages函式來載入符合條件圖片 eventBus.$on("mainScroll", debounce(setImages, 50));

//當圖片載入好後刪除該圖片資訊 export default { inserted(el, bindings) { //剛插入父節點時 收集img節點資訊 const img = { dom: el, //img 元素DOM節點 src: bindings.value, //img的src屬性值 }; imgs.push(img); //先將圖片資訊儲存到imgs陣列 setImage(img); // 立即判斷該圖片是否要載入 }, unbind(el) { //解綁時 刪除 imgs 中的所有圖片資訊 imgs = imgs.filter((img) => img.dom !== el); }, };

//上面程式碼是3.1 如何監聽容器的滾動條的滾動?+ 3.2 使用自定義指令哪些鉤子函式? //下面程式碼是3.3 如何判斷圖片 img 元素是否在使用者的可見範圍內?

//呼叫setImage函式,就可以進行單張圖片的載入 function setImage(img) { const clientHeight = document.documentElement.clientHeight; //視口高度 const rect = img.dom.getBoundingClientRect(); //圖片的位置資訊 //取預設值150 是為了解決圖片未載入成功時高度缺失的問題 const height = rect.height || 150; //圖片的高度

// 判斷該圖片是否在視口範圍內 if (-rect.top <= height && rect.top <= clientHeight) { // 在視口範圍內 進行相關處理操作 } else { // 不在視口範圍內 不進行操作 } } ```

3.4 如何處理圖片 img 元素的載入?

由效果圖我們可看出一開始所有 img 元素都是一張預設的 GIF 圖片---defaultGif,等該 img 元素進入到視口範圍時,開始載入該圖片,載入完成後再進行替換。

這裡我還進行一個優化操作,就是先新建一個 Image 物件例項,代替 img 元素載入圖片,因為圖片載入完成後會觸發onload事件,所以我們只需對onload事件進行改寫,在其內部執行 img 元素的 src 屬性替換操作,這樣就解決了載入過程中圖片空白的情況。

所以圖片懶載入指令配置檔案---lazy.js完整的程式碼如下:

```javascript import eventBus from "@/eventBus"; //引入事件匯流排 import { debounce } from "@/utils"; //引入函式防抖工具函式 import defaultGif from "@/assets/default.gif"; //在assets靜態資料夾下放入預設圖

let imgs = []; //儲存收集到的且未載入的圖片資訊

//呼叫setImage函式,就可以進行單張圖片的載入 function setImage(img) { img.dom.src = defaultGif; // 先暫時使用預設圖片 const clientHeight = document.documentElement.clientHeight; //視口高度 const rect = img.dom.getBoundingClientRect(); //圖片的位置資訊 //取預設值150 是為了解決圖片未載入成功時 高度缺失的問題 const height = rect.height || 150; //圖片的高度 // 判斷該圖片是否在視口範圍內 if (-rect.top <= height && rect.top <= clientHeight) { // 在視口範圍內 進行相關處理操作 const tempImg = new Image(); //新建Image物件例項 //改寫onload事件 tempImg.onload = function () { // 當圖片載入完成之後 img.dom.src = img.src; //替換img元素的src屬性 }; tempImg.src = img.src; imgs = imgs.filter((i) => i !== img); //將已載入好的圖片進行刪除 } }

// 呼叫setImages函式,就可以處理那些符合條件的圖片 function setImages() { for (const img of imgs) { setImage(img); // 處理該圖片 } }

//監聽事件匯流排中的mainScroll事件,該事件觸發時呼叫setImages函式來載入符合條件圖片 eventBus.$on("mainScroll", debounce(setImages, 50));

//當圖片載入好後刪除該圖片資訊 export default { inserted(el, bindings) { //剛插入父節點時 收集img節點資訊 const img = { dom: el, //img 元素DOM節點 src: bindings.value, //img的src屬性值 }; imgs.push(img); //先將圖片資訊儲存到imgs陣列 setImage(img); // 立即判斷該圖片是否要載入 }, unbind(el) { //解綁時 清空 imgs imgs = imgs.filter((img) => img.dom !== el); }, }; `` 由於評論區的朋友分享了Intersection Observer API`,我瞭解之後發現確實這方法更簡單,效能也更好。

所以我寫了篇Intersection Observer API詳解 ,喜歡的朋友看完這篇可以去看看哦。

我又用Intersection Observer API方法實現了圖片懶載入效果,大家可以去看看哦。Vue2 中自定義圖片懶載入指令 2.0

結語

這是我目前所瞭解的知識面中最好的解答,當然也有可能存在一定的誤區。

所以如果對本文存在疑惑,可以在評論區留言,我會及時回覆的,歡迎大家指出文中的錯誤觀點。

最後碼字不易,覺得有幫助的朋友點贊、收藏、關注走一波。