手把手實現圖片懶載入+封裝vue懶載入元件

amandakelake發表於2019-03-03

1、為什麼要懶載入或者預載入

圖片對頁面載入速度影響非常大

當頁面圖片比較多,載入速度慢,非常影響使用者體驗 

思考一下,頁面有可能有幾百張圖片,但是首屏上需要展示的可能就一張而已,其他的那些圖片能不能晚一點再載入,比如使用者往下滾動的時候…… 

這是為什麼要用懶載入的原因
 

那預載入呢?
這個非常語義化,預備,提前……
就是讓使用者感覺到你載入圖片非常快,甚至使用者沒有感受到你在載入圖片

2、懶載入原理

圖片先用佔位符表示,不要將圖片地址放到src屬性中,而是放到其它屬性(data-original)中
頁面載入完成後,監聽視窗滾動,當圖片出現在視窗中時再給它賦予真實的圖片地址,也就是將data-original中的屬性拿出來放到src屬性中
在滾動頁面的過程中,通過給scroll事件繫結lazyload函式,不斷的載入出需要的圖片

注意:請對lazyload函式使用防抖與節流,不懂這兩的可以自己去查

3、懶載入方式

1)純粹的延遲載入,使用setTimeOut或setInterval

這種方式,本質上不算懶載入 載入完首屏內容後,隔一段時間,去載入全部內容 但這個時間差已經完成了使用者對首屏載入速度的期待

2)條件載入

使用者點選或者執行其他操作再載入
其實也包括的滾動可視區域,但大部分情況下,大家說的懶載入都是隻可視區域的圖片懶載入,所以就拿出來說了

3)可視區載入

這裡也分為兩種情況:

1、頁面滾動的時候計算圖片的位置與滾動的位置

2、通過新的API: IntersectionObserver API(可以自動”觀察”元素是否可見)Intersection Observer API – Web API 介面 | MDN

4、懶載入程式碼實現

1、核心原理

將非首屏的圖片的src屬性設定一個預設值,監聽事件scrollresizeorientationchange,判斷元素進入視口viewport時則把真實地址賦予到src

2、img標籤自定義屬性相關

<img class="lazy" src="[佔點陣圖]" src="[真實url地址]" data-srcset="[不同螢幕密度下,不同的url地址]" alt="I`m an image!">
複製程式碼

如上,data-*屬於自定義屬性, ele.dataset.* 可以讀取自定義屬性集合 img.srcset 屬性用於設定不同螢幕密度下,image自動載入不同的圖片,比如<img src="image-128.png" srcset="image-256.png 2x" />

3、判斷元素進入視口viewport

常用的方式有兩種

1)、圖片距離頂部距離 < 視窗高度 + 頁面滾動高度(太LOW了~)

imgEle.offsetTop < window.innerHeight + document.body.scrollTop
複製程式碼

2)getBoundingClientRect (很舒服的一個API)

Element.getBoundingClientRect()方法返回元素的大小及其相對於視口的位置,具體參考文件Element.getBoundingClientRect() – Web API 介面 | MDN

  function isInViewport(ele) {
    // 元素頂部 距離 視口左上角 的距離top <= 視窗高度 (反例:元素在螢幕下方的情況)
    // 元素底部 距離 視口左上角 的距離bottom > 0 (反例:元素在螢幕上方的情況)
    // 元素display樣式不為none
    const notBelow = ele.getBoundingClientRect().top <= window.innerHeight ? true : false;
    const notAbove = ele.getBoundingClientRect().bottom >= 0 ? true : false;
    const visable = getComputedStyle(ele).display !== "none" ? true : false;
    return notBelow && notAbove && visable ? true : false;
  }
複製程式碼

3)Intersection Observer(存在相容性問題,但帥啊)

由於相容性問題,暫時不寫,具體可參考文件 Intersection Observer – Web API 介面 | MDN

4、具體實現(demo)

核心內容都在上面分析完了,下面就是整合一下,

1)適合簡單的HTML檔案或者服務端直出的首頁

注意DOMContentLoaded,在DOM解析完之後立馬執行,不適合前後端分離的單頁應用,因為SPA應用一般來說圖片資料是非同步請求的,在DOMContentLoaded的時候,頁面上未必完全解析完JS和CSS,這時候let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));拿到的不是真正首屏的所有圖片標籤

document.addEventListener("DOMContentLoaded", () => {
  // 獲取所有class為lazy的img標籤
  let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
  // 這個active是節流throttle所用的標誌位,這裡用到了閉包知識
  let active = false;

  const lazyLoad = () => {
    // throttle相關:200ms內只會執行一次lazyLoad方法
    if (active) return;
    active = true;

    setTimeout(() => {
      lazyImages.forEach(lazyImage => {
        // 判斷元素是否進入viewport
        if (isInViewport(lazyImage)) {
          // <img class="lazy" src="[佔點陣圖]" src="[真實url地址]" data-srcset="[不同螢幕密度下,不同的url地址]" alt="I`m an image!">
          // ele.dataset.* 可以讀取自定義屬性集合,比如data-*
          // img.srcset 屬性用於設定不同螢幕密度下,image自動載入不同的圖片  比如<img src="image-128.png" srcset="image-256.png 2x" />
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          // 刪除class  防止下次重複查詢到改img標籤
          lazyImage.classList.remove("lazy");
        }
        // 更新lazyImages陣列,把還沒處理過的元素拿出來
        lazyImages = lazyImages.filter(image => {
          return image !== lazyImage;
        });
        // 當全部處理完了,移除監聽
        if (lazyImages.length === 0) {
          document.removeEventListener("scroll", lazyLoad);
          window.removeEventListener("resize", lazyLoad);
          window.removeEventListener("orientationchange", lazyLoad);
        }
      })

      active = false;
    }, 200);
  }

  document.addEventListener("scroll", lazyLoad);
  document.addEventListener("resize", lazyLoad);
  document.addEventListener("orientationchange", lazyLoad);
})
複製程式碼

2)、適合單頁應用的寫法(模擬封裝vue的懶載入)

① 核心實現

  • 因為是demo,所以執行時機放到vue的全域性mounted鉤子裡面(這樣的首屏體驗其實是不好的),不過足夠理解就好了 
  • 跟上面不同的地方:let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));的獲取時機放在了定時器裡面,不是一開始就拿到全域性的lazyImages,而是每次重新整理時才拿到還沒處理過的
function LazyLoad() {
  // 這個active是節流throttle所用的標誌位,這裡用到了閉包知識
  let active = false;

  const lazyLoad = () => {
    // throttle相關:200ms內只會執行一次lazyLoad方法
    if (active) return;
    active = true;

    setTimeout(() => {
      // 獲取所有class為lazy的img標籤,這裡由於之前已經把處理過的img標籤的class刪掉了  所以不會重複查詢
      let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

      lazyImages.forEach(lazyImage => {
        // 判斷元素是否進入viewport
        if (isInViewport(lazyImage)) {
          // <img class="lazy" src="[佔點陣圖]" src="[真實url地址]" data-srcset="[不同螢幕密度下,不同的url地址]" alt="I`m an image!">
          // ele.dataset.* 可以讀取自定義屬性集合,比如data-*
          // img.srcset 屬性用於設定不同螢幕密度下,image自動載入不同的圖片  比如<img src="image-128.png" srcset="image-256.png 2x" />
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          // 刪除class  防止下次重複查詢到改img標籤
          lazyImage.classList.remove("lazy");
        }

        // 當全部處理完了,移除監聽
        if (lazyImages.length === 0) {
          document.removeEventListener("scroll", lazyLoad);
          window.removeEventListener("resize", lazyLoad);
          window.removeEventListener("orientationchange", lazyLoad);
        }
      })

      active = false;
    }, 200);
  }

  document.addEventListener("scroll", lazyLoad);
  document.addEventListener("resize", lazyLoad);
  document.addEventListener("orientationchange", lazyLoad);
}
複製程式碼

② 在全域性中的`mounted`鉤子中執行

const vm = new Vue({
  el: `.wrap`,
  store,
  mounted: function () {
    LazyLoad();
  }
});
複製程式碼

③ 封裝 img-lazy元件

<template>
  <img :class="[`lazy`, className]" :src="defaultImg" :src="url" :data-srcset="`${url} 1x`" alt="fordeal">
</template>

<script>
  export default {
    props: {
      url: {
        type: String
      },
      defaultImg: {
        type: String,
        default: [預設圖片]
      className: {
        type: String,
        default: ``
      }
    }
  }
</script>
複製程式碼

④ 使用

<img-lazy className="image" :url="item.display_image" />複製程式碼

以上實現的只是比較粗糙的版本,要真正實現效能大幅提升優化還需要處理較多的細節,本文旨在讓幫助部分同學瞭解基本原理,有了巨集觀的認識後,可以嘗試去讀一下相關這種懶載入外掛的原始碼,能學到不少東西。

感謝

感謝您耐心看到這裡,希望有所收穫!

我在學習過程中喜歡做記錄,分享的是自己在前端之路上的一些積累和思考,希望能跟大家一起交流與進步,更多文章請看amandakelake的Github部落格

相關文章