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






