本文相對比較初級措伐,為了節(jié)約時間特纤,請小神及其以上級別的同學(xué)直接忽略。
有同學(xué)可能會問:那么多第三方庫侥加,為什么要自己動手寫呢捧存。景科同學(xué)的想法很簡單,因?yàn)楸救四壳斑€是一個前端小白担败,只有通過不斷的寫昔穴,不斷的學(xué),在與bug的相愛相殺中才能更快速的進(jìn)步提前。在證明可行可用之后不僅可以減少項(xiàng)目的第三方依賴吗货,即便出現(xiàn)bug,解決自己代碼的bug也要比解決別人代碼的bug要好過一些狈网。話不多說宙搬,且入正題。
一. 基礎(chǔ)結(jié)構(gòu)
手摸手第一步拓哺。在第一步害淤,先把基礎(chǔ)結(jié)構(gòu)構(gòu)思搭建一下,以方便后續(xù)擼碼拓售。圖片懶加載本身就不是什么復(fù)雜的實(shí)現(xiàn)窥摄,所以基本結(jié)構(gòu)也比較簡單,無非就是初始化一下參數(shù)础淤,容一下錯崭放,綁定幾個函數(shù)哨苛,實(shí)現(xiàn)一下功能而已。具體代碼且往下看:
(function (global, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
module.exports = factory(global)
} else if (typeof define === 'function' && define.amd) {
define(factory)
} else {
global.Lazy = factory(global)
}
})(this, function () {
// 全局變量币砂,所有方法在此對象上擴(kuò)展
var Lazy = {};
// 計時器
var timer = null;
// 節(jié)流延遲時間
var delay = 150;
// 是否開啟節(jié)流
var debounce = true;
// 是否開啟圖片懶加載圖片的重載建峭。解釋一下:就是圖片離開懶加載區(qū)域要把圖片狀態(tài)復(fù)原,再次進(jìn)入懶加載區(qū)域要在視覺上呈現(xiàn)懶加載效果
// 先呵呵一下這個功能决摧,正常的我肯定想不到這么個抽風(fēng)的需求亿蒸,誰讓我曾經(jīng)碰到過一個抽風(fēng)的產(chǎn)品經(jīng)理呢,實(shí)現(xiàn)不難掌桩,這里也順便實(shí)現(xiàn)一下
var unload = false;
// 回掉函數(shù)
var callback = function () {};
// 元素相對于視窗的位置集合
var boxRect = {};
/**
* 判斷目標(biāo)元素是否可見 #1
* @param {HTMLElement} element
* @returns {boolean}
*/
var isHidden = function (element) {};
/**
* 判斷目標(biāo)元素是否進(jìn)入懶加載區(qū)域 #2
* @param {HTMLElement} element
* @returns {boolean}
*/
var canLoadImg = function (element) {};
/**
* 節(jié)流函數(shù) #3
*/
var onDebounceRender = function () {};
/**
* 始化方法边锁,外部直接調(diào)用,配置參數(shù)在此接收 #4
* @param {Object} options
* @param {String} options.root - 圖片滾動區(qū)域根元素選擇器
* @param {Number} options.offset - 懶加載閾值波岛,當(dāng)沒有【上下左右】4個值時將以此為準(zhǔn)
* @param {Number} options.offsetTop - 懶加載閾值【上】
* @param {Number} options.offsetRight - 懶加載閾值【右】
* @param {Number} options.offsetBottom - 懶加載閾值【下】
* @param {Number} options.offsetLeft - 懶加載閾值【左】
* @param {Boolean} options.debounce - 是否開啟函數(shù)節(jié)流
* @param {Number} options.delay - 函數(shù)節(jié)流時間閾值
* @param {Boolean} options.unload - 圖片重載
* @param {Function} options.callback - 懶加載操作完成時的回掉函數(shù)
*/
Lazy.init = function(options) {};
/**
* 懶加載實(shí)現(xiàn) #5
* @param {HTMLElement} element
*/
Lazy.render = function(element) {};
/**
* 不滿足懶加載條件時銷毀 #6
*/
Lazy.destroy = function() {};
// 返回
return Lazy;
});
由于項(xiàng)目本身并不復(fù)雜茅坛,所以基礎(chǔ)的結(jié)構(gòu)也比較簡單,剩下的幾步我們只需要手摸手去做填空題(#1则拷、#2贡蓖、#3、#4煌茬、#5斥铺、#6)就好了。so easy坛善,let`s go晾蜘!
二. 功能函數(shù)實(shí)現(xiàn)
手摸手第二步。在第二步我們先把#1浑吟、#2、#3三個功能函數(shù)實(shí)現(xiàn)一下耗溜。
首先是#1函數(shù)isHidden
的實(shí)現(xiàn)组力。
/**
* 判斷目標(biāo)元素是否可見
* https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/offsetParent
* @param {HTMLElement} element
* @returns {boolean}
*/
var isHidden = function (element) {
return element.offsetParent === null;
};
接下來是#2函數(shù)canLoadImg
的實(shí)現(xiàn)。這兒用到了Element.getBoundingClientRect()
方法返回元素的大小及其相對于視口的位置抖拴。關(guān)于Element.getBoundingClientRect()
的信息請點(diǎn)擊傳送陣了解更多燎字。
/**
* 判斷目標(biāo)元素是否進(jìn)入懶加載區(qū)域
* 此函數(shù)依賴isHidden函數(shù)和boxRect全局變量
* @param {HTMLElement} element
* @returns {boolean}
*/
var canLoadImg = function (element) {
if (isHidden(element)) return false;
var eleRect = element.getBoundingClientRect();
return (eleRect.top <= boxRect.b && eleRect.right >= boxRect.l && eleRect.bottom >= boxRect.t && eleRect.left <= boxRect.r);
};
最后是#3函數(shù)onDebounceRender
的實(shí)現(xiàn)。由于這里對節(jié)流函數(shù)沒什么特殊需求阿宅,所以實(shí)現(xiàn)的比較簡單候衍,如果看官同學(xué)需要完整的debounce函數(shù),請點(diǎn)擊lodash/debounce.js了解更多洒放。
/**
* 節(jié)流函數(shù)
* 此函數(shù)依賴Lazy.render蛉鹿、debounce、timer往湿、delay
*/
var onDebounceRender = function () {
if (!debounce) {
Lazy.render();
} else {
clearTimeout(timer);
timer = setTimeout(function () {
Lazy.render();
timer = null;
}, delay);
}
};
三. 初始化函數(shù)
手摸手第三步妖异。我們來實(shí)現(xiàn)一下
Lazy.init
初始化函數(shù)惋戏。這個函數(shù)的作用就是接收參數(shù)、綁定事件處理函數(shù)他膳,所以更簡單响逢。
/**
* 始化方法,外部直接調(diào)用棕孙,配置參數(shù)在此接收
* @param {Object} options
* @param {String} options.root - 圖片滾動區(qū)域根元素選擇器
* @param {Number} options.offset - 懶加載閾值舔亭,當(dāng)沒有上下左右有4個值時將以此為準(zhǔn)
* @param {Number} options.offsetTop - 懶加載閾值【上】
* @param {Number} options.offsetRight - 懶加載閾值【右】
* @param {Number} options.offsetBottom - 懶加載閾值【下】
* @param {Number} options.offsetLeft - 懶加載閾值【左】
* @param {Boolean} options.debounce - 是否開啟函數(shù)節(jié)流
* @param {Number} options.delay - 函數(shù)節(jié)流時間閾值
* @param {Boolean} options.unload - 圖片重載
* @param {Function} options.callback - 懶加載操作完成時的回掉函數(shù)
*/
Lazy.init = function (options) {
options = options || {};
global = document.querySelector(options.root) || global;
debounce = options.debounce !== false;
delay = options.delay || delay;
unload = options.unload || unload;
callback = options.callback || callback;
// 懶加載區(qū)域,寫的雖然有點(diǎn)長但是不難理解
boxRect = {
t: 0 - (options.offsetTop || options.offset || 0),
r: (global.innerWidth || document.documentElement.clientWidth) + (options.offsetRight || options.offset || 0),
b: (global.innerHeight || document.documentElement.clientHeight) + (options.offsetBottom || options.offset || 0),
l: 0 - (options.offsetLeft || options.offset || 0)
};
// 這里提前調(diào)用一次蟀俊,因?yàn)槿绻鹍ebounce為true钦铺,load之后會有最低250ms的延遲
Lazy.render();
// 綁定事件
if (global.addEventListener) {
global.addEventListener('load', onDebounceRender, false);
global.addEventListener('scroll', onDebounceRender, false);
} else {
global.attachEvent('onload', onDebounceRender);
global.attachEvent('onscroll', onDebounceRender);
}
};
四. 核心方法實(shí)現(xiàn)
手摸手第四步。在第四步我們來完成
Lazy.render
函數(shù)的實(shí)現(xiàn)欧漱。這個函數(shù)也是本項(xiàng)目的核心方法职抡,所有的懶加載實(shí)現(xiàn)都是在此處完成。思路不復(fù)雜误甚,所以實(shí)現(xiàn)起來也比較簡單缚甩。
/**
* 懶加載實(shí)現(xiàn)
* @param {HTMLElement} element
*/
Lazy.render = function (element) {
// dom結(jié)構(gòu)約定:img標(biāo)簽要有[data-lazy]自定義屬性,需要設(shè)置背景的標(biāo)簽需要有[data-lazy-background]自定義屬性
var nodes = (element || document).querySelectorAll('[data-lazy], [data-lazy-background]');
var len = nodes.length;
for (var i = 0; i < len; i++) {
var node = nodes[i];
// 目標(biāo)元素在懶加載區(qū)域和不在懶加載區(qū)域
if (canLoadImg(node)) {
// 懶加載圖片需要重載窑邦,懶加載之前先將占位圖片存儲
if (unload && node.src && !node.getAttribute('data-lazy-placeholder')) {
node.setAttribute('data-lazy-placeholder', node.src);
}
// 圖片的懶加載
var src = node.getAttribute('data-lazy');
if (src !== null && node.src !== src) {
node.src = src;
}
// 背景的懶加載
var bgUrl = node.getAttribute('data-lazy-background');
if (bgUrl !== null && node.style.backgroundImage !== bgUrl) {
node.style.backgroundImage = 'url(' + bgUrl + ')';
}
// 如果圖片不需要重載擅威,懶加載完成移除[data-lazy]自定義屬性
if (!unload) {
node.removeAttribute('data-lazy');
}
// 懶加載完成移除[data-lazy-background]自定義屬性
node.removeAttribute('data-lazy-background');
// 懶加載完成觸發(fā)回掉
callback(node, 'load');
} else {
// 當(dāng)圖片不在懶加載區(qū)域時做重載處理
var placeholder = node.getAttribute('data-lazy-placeholder');
if (unload && placeholder !== null) {
node.src = placeholder;
// 移除[data-lazy-placeholder]自定義屬性
node.removeAttribute('data-lazy-placeholder');
// 重載完成觸發(fā)回掉
callback(node, 'unload');
}
}
}
// 如果沒有懶加載的元素,銷毀Lazy.init添加的事件
if (len === 0) {
Lazy.destroy();
}
};
五. 解綁方法實(shí)現(xiàn)
手摸手第五步冈钦。這一步更簡單郊丛,話不多說直接看代碼。
/**
* 不滿足懶加載條件時移除綁定的事件瞧筛,重置定時器引用
*/
Lazy.destroy = function () {
if (document.removeEventListener) {
global.removeEventListener('scroll', onDebounceRender);
} else {
global.detachEvent('onscroll', onDebounceRender);
}
clearTimeout(timer);
};
六. 結(jié)語
由于景科同學(xué)剛開始寫博文厉熟,語文老師走的又早(是真的早)??,文筆難免摳腳较幌,不足之處還望各位看官同學(xué)多多包含揍瑟。本項(xiàng)目是景科同學(xué)自寫自測,雖然比較簡單乍炉,但是不保證沒有隱藏的bug绢片。所以如果看官同學(xué)發(fā)現(xiàn)還望留言指正,景科同學(xué)在此以示感謝岛琼。
本文完整代碼請點(diǎn)這里底循。
如果大神同學(xué)看到此處,更希望你能留下批評指正的意見槐瑞,這樣景科同學(xué)才能更快的進(jìn)步熙涤,下次如果你們不小心點(diǎn)開景科同學(xué)寫的文章才不會白花花的浪費(fèi)寶貴的時間,誰說不是呢??!