手把手實現圖片懶加載+封裝vue懶加載組件

一、爲何要懶加載或者預加載

圖片對頁面加載速度影響很是大前端

當頁面圖片比較多,加載速度慢,很是影響用戶體驗  vue

思考一下,頁面有可能有幾百張圖片,可是首屏上須要展現的可能就一張而已,其餘的那些圖片能不能晚一點再加載,好比用戶往下滾動的時候…… git

這是爲何要用懶加載的緣由  github

那預加載呢? 這個很是語義化,預備,提早…… 就是讓用戶感受到你加載圖片很是快,甚至用戶沒有感覺到你在加載圖片
後端

二、懶加載原理

圖片先用佔位符表示,不要將圖片地址放到src屬性中,而是放到其它屬性(data-original)中 頁面加載完成後,監聽窗口滾動,當圖片出如今視窗中時再給它賦予真實的圖片地址,也就是將data-original中的屬性拿出來放到src屬性中 在滾動頁面的過程當中,經過給scroll事件綁定lazyload函數,不斷的加載出須要的圖片
數組


注意:請對lazyload函數使用防抖與節流,不懂這兩的能夠本身去查
bash

三、懶加載方式

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

這種方式,本質上不算懶加載 加載完首屏內容後,隔一段時間,去加載所有內容 但這個時間差已經完成了用戶對首屏加載速度的期待
閉包

2)條件加載

用戶點擊或者執行其餘操做再加載 其實也包括的滾動可視區域,但大部分狀況下,你們說的懶加載都是隻可視區域的圖片懶加載,因此就拿出來講了前後端分離

3)可視區加載

這裏也分爲兩種狀況:
異步

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

二、經過新的API: IntersectionObserver API(能夠自動"觀察"元素是否可見)Intersection Observer API - Web API 接口 | MDN

四、懶加載代碼實現

一、核心原理

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

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

<img class="lazy" src="[佔位圖]" data-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" />

三、判斷元素進入視口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

四、具體實現(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="[佔位圖]" data-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="[佔位圖]" data-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" :data-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博客

相關文章
相關標籤/搜索