手把手實現圖片懶載入+封裝vue懶載入元件
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屬性設定一個預設值,監聽事件scroll
、resize
、orientationchange
,判斷元素進入視口viewport時則把真實地址賦予到src
上
2、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" />
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="[佔位圖]" 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
鉤子裡面(這樣的首屏體驗其實是不好的),不過足夠理解就好了 -
跟上面不同的地方:l
et 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部落格