什么是延時加載?
圖片延遲加載也稱 “懶加載”,通常應(yīng)用于圖片比較多的網(wǎng)頁
為什么要使用延時加載?
假如一個網(wǎng)頁中,含有大量的圖片,當(dāng)用戶訪問網(wǎng)頁時,那么瀏覽器會發(fā)送n個圖片的請求,加載速度會變得緩慢,性能也會下降。如果使用了延時加載,當(dāng)用戶訪問頁面的時候,只加載首屏中的圖片;后續(xù)的圖片只有在用戶滾動時,即將要呈現(xiàn)給用戶瀏覽時再按需加載,這樣可以提高頁面的加載速度,也提升了用戶體驗。而且,統(tǒng)一時間內(nèi)更少的請求也減輕了服務(wù)器中的負擔(dān)。
延時加載的原理
基本原理就是最開始時,所有圖片都先放一張占位圖片(如灰色背景圖),真實的圖片地址則放在 data-src 中,這么一來,網(wǎng)頁在打開時只會加載一張圖片。
然后,再給 window 或 body 或者是圖片主體內(nèi)容綁定一個滾動監(jiān)聽事件,當(dāng)圖片出現(xiàn)在可視區(qū)域內(nèi),即滾動距離 + 窗體可視距離 > 圖片到容器頂部的距離時,將講真實圖片地址賦值給圖片的 src,否則不加載。
使用原生js實現(xiàn)圖片的延時加載
延時加載需要傳入的參數(shù):
var selector = options.selector || 'img', imgSrc = options.src || 'data-src', defaultSrc = options.defaultSrc || '', wrApper = options.wrap || body;
其中:
- wrapper :延時加載的容器。在該容器下,所有符合圖片選擇器條件的圖片均會延時加載。
- selector :圖片選擇器。表示需要延遲加載的圖片的選擇器,如 img.lazyload-image ,默認為所有的 img 標簽。
- imgSrc :圖片真實地址存放屬性。表示圖片的真實路徑存放在標簽的哪個屬性中,默認為 data-src。
- defaultSrc :初始加載的圖片地址,默認為空,當(dāng)為空時,不處理延時加載的圖片的路徑,若圖片本身沒有路徑,則顯示為空。
- 獲取容器中所有的圖片。
function getAllImages(selector){ return Array.prototype.concat.apply([], wrapper.querySelectorAll(selector)); }
該函數(shù)在容器中查找出所有需要延時加載的圖片,并將 NodeList 類型的對象轉(zhuǎn)換為允許使用 map 函數(shù)的數(shù)組。
如果設(shè)置了初始圖片地址,則加載。
function setDefault(){ images.map(function(img){ img.src = defaultSrc; }) }
給 window 綁定滾動事件
function loadImage(){ var nowHeight = body.scrollTop || doc.documentElement.scrollTop; console.log(nowHeight); if (images.length > 0){ images.map(function(img, index) { if (nowHeight + winHeight > img.offsetTop) { img.src = img.getAttribute(imgSrc); images.splice(index, 1); } }) }else{ window.onscroll = null; } } window.onscroll = loadImage();
每次滾動網(wǎng)頁時,都會遍歷所有的圖片,將圖片的位置與當(dāng)前滾動位置作對比,當(dāng)符合加載條件時,將圖片的真實地址賦值給圖片,并將圖片從集合中移除;當(dāng)所有需要延時加載的圖片都加載完畢后,將滾動事件取消綁定。
測試是否可行
測試結(jié)果:
從chrome的網(wǎng)絡(luò)請求圖中可見,5張圖片并不是在網(wǎng)頁打開的時候就請求了,而是當(dāng)滑動到某個區(qū)域時才觸發(fā)加載,基本實現(xiàn)了圖片的延時加載。
測試結(jié)果
性能調(diào)整
上述只是簡單的實現(xiàn)了一個延時加載的 demo,還有很多地方需要調(diào)整和完善。
調(diào)整 1:onscroll 函數(shù)可能會被覆蓋
問題:
因為有時候頁面需要滾動無限加載時,插件會重寫 window 的 onscroll 函數(shù),從而導(dǎo)致圖片的延時加載滾動監(jiān)聽失效。
解決辦法:
需要更改為將監(jiān)聽事件注冊到 window 上,移除時只需要移除相應(yīng)的事件即可。
調(diào)整后的代碼
function bindListener(element, type, callback){ if (element.addEventListener) { element.addEventListener(type, callback); }else if (element.attachEvent) { //兼容至 IE8 element.attachEvent('on'+type, callback) }else{ element['on'+type] = callback; } } function removeListener(element, type, callback){ if (element.removeEventListener) { element.removeEventListener(type, callback); }else if (element.detachEvent) { element.detachEvent('on'+type, callback) }else{ element['on'+type] = callback; } } function loadImage(){ var nowHeight = body.scrollTop || doc.documentElement.scrollTop; console.log(nowHeight); if (images.length > 0){ images.map(function(img, index) { if (nowHeight + winHeight > img.offsetTop) { img.src = img.getAttribute(imgSrc); images.splice(index, 1); } }) }else{ //解綁滾動事件 removeListener(window, 'scroll', loadImage) } } //綁定滾動事件 bindListener(window, 'scroll', loadImage)
調(diào)整2:滾動時的回調(diào)函數(shù)執(zhí)行次數(shù)太多
問題
在本次測試中,從動圖最后可以看到,當(dāng)滾動網(wǎng)頁時,loadImage 函數(shù)執(zhí)行了非常多次,滾輪每向下滾動 100px 基本上就要執(zhí)行 10 次左右的 loadImage,若處理函數(shù)稍微復(fù)雜,響應(yīng)速度跟不上觸發(fā)頻率,則會造成瀏覽器的卡頓甚至假死,影響用戶體驗。
解決辦法
使用 throttle 控制觸發(fā)頻率,讓瀏覽器有更多的時間間隔去執(zhí)行相應(yīng)操作,減少頁面抖動。
調(diào)整后的代碼:
//參考 `underscore` 的源碼 var throttle = function(func, wait, options) { var context, args, result; var timeout = null; // 上次執(zhí)行時間點 var previous = 0; if (!options) options = {}; // 延遲執(zhí)行函數(shù) var later = function() { // 若設(shè)定了開始邊界不執(zhí)行選項,上次執(zhí)行時間始終為0 previous = options.leading === false ? 0 : _now(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { var now = _now(); // 首次執(zhí)行時,如果設(shè)定了開始邊界不執(zhí)行選項,將上次執(zhí)行時間設(shè)定為當(dāng)前時間。 if (!previous && options.leading === false) previous = now; // 延遲執(zhí)行時間間隔 var remaining = wait - (now - previous); context = this; args = arguments; // 延遲時間間隔remaining小于等于0,表示上次執(zhí)行至此所間隔時間已經(jīng)超過一個時間窗口 // remaining大于時間窗口wait,表示客戶端系統(tǒng)時間被調(diào)整過 if (remaining <= 0 || remaining > wait) { clearTimeout(timeout); timeout = null; previous = now; result = func.apply(context, args); if (!timeout) context = args = null; //如果延遲執(zhí)行不存在,且沒有設(shè)定結(jié)尾邊界不執(zhí)行選項 } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; }; //在調(diào)用高頻率觸發(fā)函數(shù)處使用 throttle 控制頻率在 次/wait var load = throttle(loadImage, 250); //綁定滾動事件 bindListener(window, 'scroll', load); //解綁滾動事件 removeListener(window, 'scroll', load)
調(diào)整后的測試
調(diào)整后的測試結(jié)果
封裝為插件形式
;(function(window, undefined){ function _now(){ return new Date().getTime(); } //輔助函數(shù) var throttle = function(func, wait, options) { var context, args, result; var timeout = null; // 上次執(zhí)行時間點 var previous = 0; if (!options) options = {}; // 延遲執(zhí)行函數(shù) var later = function() { // 若設(shè)定了開始邊界不執(zhí)行選項,上次執(zhí)行時間始終為0 previous = options.leading === false ? 0 : _now(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { var now = _now(); // 首次執(zhí)行時,如果設(shè)定了開始邊界不執(zhí)行選項,將上次執(zhí)行時間設(shè)定為當(dāng)前時間。 if (!previous && options.leading === false) previous = now; // 延遲執(zhí)行時間間隔 var remaining = wait - (now - previous); context = this; args = arguments; // 延遲時間間隔remaining小于等于0,表示上次執(zhí)行至此所間隔時間已經(jīng)超過一個時間窗口 // remaining大于時間窗口wait,表示客戶端系統(tǒng)時間被調(diào)整過 if (remaining <= 0 || remaining > wait) { clearTimeout(timeout); timeout = null; previous = now; result = func.apply(context, args); if (!timeout) context = args = null; //如果延遲執(zhí)行不存在,且沒有設(shè)定結(jié)尾邊界不執(zhí)行選項 } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; }; //分析參數(shù) function extend(custom, src){ var result = {}; for(var attr in src){ result[attr] = custom[attr] || src[attr] } return result; } //綁定事件,兼容處理 function bindListener(element, type, callback){ if (element.addEventListener) { element.addEventListener(type, callback); }else if (element.attachEvent) { element.attachEvent('on'+type, callback) }else{ element['on'+type] = callback; } } //解綁事件,兼容處理 function removeListener(element, type, callback){ if (element.removeEventListener) { element.removeEventListener(type, callback); }else if (element.detachEvent) { element.detachEvent('on'+type, callback) }else{ element['on'+type] = null; } } //判斷一個元素是否為DOM對象,兼容處理 function isElement(o) { if(o && (typeof htmlElement==="function" || typeof HTMLElement==="object") && o instanceof HTMLElement){ return true; }else{ return (o && o.nodeType && o.nodeType===1) ? true : false; }; }; var lazyload = function(options){ //輔助變量 var images = [], doc = document, body = document.body, winHeight = screen.availHeight; //參數(shù)配置 var opt = extend(options, { wrapper: body, selector: 'img', imgSrc: 'data-src', defaultSrc: '' }); if (!isElement(opt.wrapper)) { console.log('not an HTMLElement'); if(typeof opt.wrapper != 'string'){ //若 wrapper 不是DOM對象 或者不是字符串,報錯 throw new Error('wrapper should be an HTMLElement or a selector string'); }else{ //選擇器 opt.wrapper = doc.querySelector(opt.wrapper) || body; } } //查找所有需要延時加載的圖片 function getAllImages(selector){ return Array.prototype.concat.apply([], opt.wrapper.querySelectorAll(selector)); } //設(shè)置默認顯示圖片 function setDefault(){ images.map(function(img){ img.src = opt.defaultSrc; }) } //加載圖片 function loadImage(){ var nowHeight = body.scrollTop || doc.documentElement.scrollTop; console.log(nowHeight); if (images.length > 0){ images.map(function(img, index) { if (nowHeight + winHeight > img.offsetTop) { img.src = img.getAttribute(opt.imgSrc); console.log('loaded'); images.splice(index, 1); } }) }else{ removeListener(window, 'scroll', load) } } var load = throttle(loadImage, 250); return (function(){ images = getAllImages(opt.selector); bindListener(window, 'scroll', load); opt.defaultSrc && setDefault() loadImage(); })() }; window.lazyload = lazyload; })(window);
上述代碼拷貝到項目中即可使用,使用方式:
//使用默認參數(shù) new lazyload(); //使用自定義參數(shù) new lazyload({ wrapper: '.article-content', selector: '.image', src: 'data-image', defaultSrc: 'example.com/static/images/default.png' });
若在 IE8 中使用,沒有 map 函數(shù)時,請在引用插件前加入下列處理 map 函數(shù)兼容性的代碼:
// 實現(xiàn) ECMA-262, Edition 5, 15.4.4.19 // 參考: http://es5.github.com/#x15.4.4.19 if (!Array.prototype.map) { Array.prototype.map = function(callback, thisArg) { var T, A, k; if (this == null) { throw new TypeError(" this is null or not defined"); } // 1. 將O賦值為調(diào)用map方法的數(shù)組. var O = Object(this); // 2.將len賦值為數(shù)組O的長度. var len = O.length >>> 0; // 3.如果callback不是函數(shù),則拋出TypeError異常. if (Object.prototype.toString.call(callback) != "[object Function]") { throw new TypeError(callback + " is not a function"); } // 4. 如果參數(shù)thisArg有值,則將T賦值為thisArg;否則T為undefined. if (thisArg) { T = thisArg; } // 5. 創(chuàng)建新數(shù)組A,長度為原數(shù)組O長度len A = new Array(len); // 6. 將k賦值為0 k = 0; // 7. 當(dāng) k < len 時,執(zhí)行循環(huán). while (k < len) { var kValue, mappedValue; //遍歷O,k為原數(shù)組索引 if (k in O) { //kValue為索引k對應(yīng)的值. kValue = O[k]; // 執(zhí)行callback,this指向T,參數(shù)有三個.分別是kValue:值,k:索引,O:原數(shù)組. mappedValue = callback.call(T, kValue, k, O); // 返回值添加到新數(shù)組A中. A[k] = mappedValue; } // k自增1 k++; } // 8. 返回新數(shù)組A return A; }; }