懶加載(LazyLoad)一直是前端的優化方案之一。它的核心思想是:當用戶想看頁面某個區域時,再加載該區域的數據。這在一定程度上減輕了服務器端的壓力,也加快了頁面的呈現速度。懶加載多用於圖片,因爲它屬於流量的大頭。最典型的懶加載實現方案是先將需要懶加載的圖片的src隱藏掉,這樣圖片就不會下載,然後在圖片需要呈現給用戶時再加上src屬性。
公司內部庫的懶加載正是採用這種方案。它會遍歷頁面中所有的圖片,將其src緩存起來後刪除圖片的src屬性,當圖片進入用戶的可視區域後再爲圖片附加src屬性。這種方案存在着以下不足:
① 在IE和FF下,懶加載的腳本運行時,有部分圖片已經於服務器建立鏈接,這部分abort掉,再在滾動時延遲加載,反而增加了鏈接數。
② 在chrome下,由於webkit內核bug,導致無法abort掉下載,懶加載腳本完全無用。
③ 它只能針對圖片的懶加載,但無法懶加載頁面的某個模塊(即延遲渲染頁面的DOM節點)。
因此,在原有的技術方案之上,必須實現新的方案來解決這些問題。受到淘寶的懶加載模塊啓發,思路如下:
① 提供一種方式來讓我們手動爲頁面中每個需要懶加載的圖片緩存它的src屬性,例如:原來的圖片爲<img src="xxx.jpg" />,現在改爲<img data-src="xxx.jpg">。這樣,頁面在解析的時候,所有懶加載的圖片在所有的瀏覽器下都不會下載,圖片進入視野區域時再將data-src賦值給src屬性。
② 提供延遲加載頁面模塊的方案。將研究發現,textarea是個不錯的容器,瀏覽器會將該標籤內的內容當作普通文本看待。因此,可以將頁面中需要懶加載的模塊放入textarea容器中,帶需要的時候再將其取出。淘寶美食網正是大量運用了模塊延遲加載方案。http://chi.taobao.com/market/food/auto.php?spm=885.125570.154248.13.F5s7Bt。
基於上述思路,我寫了一個懶加載的組件。該組件基於jquery,提供的接口如下:
return { init : _init, addCallBack : _addCallBack };init函數可以初始化該組件,它提供給我們的自定義選項如下:
var config = {
mod : 'auto', //分爲auto和manul
IMG_SRC_DATA : 'img-lazyload',
AREA_DATA_CLS : 'area-datalazyload'
};
mod 分爲自動和手動模式,自動模式正是前面討論到的目前存在的實現方案,而手動方式是後來討論的方案①,在手動方式下,我們需要將每個需要懶加載的圖片的src屬性緩存到一個用戶可以自定義的屬性中,默認爲'img-lazyload',即原始的圖片改爲<img img-lazyload='xxx.jpg'>。
此外,不管是自動模式和手動模式下,都可以進行模塊的懶加載,這時候,需要在每個模塊的外層添加textarea容器,並且,將其visibility屬性設置爲hidden,class設置爲一個用戶可以自定義的值,默認爲'area-datalazyload'。
實例如下:
//自動模式
datalazyload.init({
'mod' :auto
});
//手動模式
datalazyload.init({
'mod' :manual,
'IMG_SRC_DATA' : 'data-src'
});
addCallback是特定元素即將出現時的回調函數。調用如下:
datalazyload.addCallback($el,function(event){
//TO DO
})
其中$el是某個需要延遲加載的jquery對象,function是自定義的回調函數。
組件適用場景:① 有許多圖片的頁面,例如遊戲特權首頁:http://vip.qq.com/game.html
② 有許多模塊,並且每個模塊分工明確的頁面,例如淘寶美食:http://chi.taobao.com/market/food/auto.php?spm=885.125570.154248.13.F5s7Bt。
組件如下:
/**
* @fileOverview 數據懶加載組件
* @require jQuery
*/
datalazyload = (function($){
var config = {
mod : 'auto', //分爲auto和manul
IMG_SRC_DATA : 'img-lazyload',
AREA_DATA_CLS : 'area-datalazyload'
};
var IMG_SRC_DATA = '';
var AREA_DATA_CLS = '';
//用來存放需要懶加載的圖片和數據塊
var imgArr = [];
var areaArr = [];
//支持用戶回調的事件類型
var eventType = 'lazy';
/**
* 提供給外部的接口
* @param {Object} [userConfig] 用戶自定義配置
* @private
*/
function _init(userConfig) {
config = $.extend(config,userConfig);
console.log(config);
IMG_SRC_DATA = config.IMG_SRC_DATA;
AREA_DATA_CLS = config.AREA_DATA_CLS;
_filterItems();
_initEvent();
}
/**
* 處理需要懶加載的圖片和數據塊的入口
* @private
*/
function _filterItems() {
_filterImgs();
_filterAreas();
}
/**
* 事件綁定
* @private
*/
function _initEvent() {
$(window).scroll(_eventHandler);
$(window).resize(_eventHandler);
_eventHandler();
}
/**
* 處理需要懶加載的圖片
* @private
*/
function _filterImgs() {
if (config.mod === 'auto') {
//自動模式
var $imgs = $("img");
$imgs.each(function() {
imgArr.push(this);
var $img = $(this);
$img.targetY = _getTargetY($img[0]);//先計算出每個圖片距離頁面頂部的高度,避免在事件事件處理函數中進行大量重複計算
var dataSrc = $img.attr(IMG_SRC_DATA);
//對於已存在IMG_SRC_DATA的,可能其它實例處理過,我們直接跳過去
if (!dataSrc) {
$img.attr(IMG_SRC_DATA,$img.attr('src'));
$img.removeAttr('src');
}
});
} else {
//手動模式下,已經在需要懶加載的IMG中設置了IMG_SRC_DATA屬性,所以不作任何處理
var $imgs = $("img["+IMG_SRC_DATA+"]");
$imgs.each(function() {
imgArr.push(this);
var $img = $(this);
$img.targetY = _getTargetY($img[0]);//先計算出每個圖片距離頁面頂部的高度,避免在事件事件處理函數中進行大量重複計算
});
}
}
/**
* 處理需要懶加載的數據塊
* @private
*/
function _filterAreas() {
var $areas = $("textarea[class='"+AREA_DATA_CLS+"']");
$areas.each(function() {
areaArr.push(this);
var $area = $(this);
$area.targetY = _getTargetY($area[0]);
});
}
/**
* window節點的scroll和resize的事件處理函數
* @private
*/
function _eventHandler() {
$.each(imgArr,function(i,el){
if (el !== undefined) {
var $img = $(el);
if (_checkBounding($img)) {
$img.attr('src',$img.attr(IMG_SRC_DATA));
$img.trigger(eventType);
$img.unbind(eventType);
delete imgArr[i];
}
}
});
$.each(areaArr,function(i,el){
if (el !== undefined) {
var $area = $(el);
if (_checkBounding($area)) {
$area.hide();
$area.removeClass(AREA_DATA_CLS);
var $div = $("<div></div>");
$div.insertBefore($area);
$div.html($area.val());
delete areaArr[i];
}
}
});
}
/**
* 檢查需要懶加載的節點是否進入可視區域
* @param {jQuery Object} [el]
* @private
*/
function _checkBounding($el) {
var scrollY = document.body.scrollTop || document.documentElement.scrollTop || window.pageYOffset || 0;//頁面滾動條高度
var seeY = window.innerHeight || document.documentElement.clientHeight;//瀏覽器可視區域高度
if ($el.targetY) {
var targetY = $el.targetY;
} else {
var targetY = _getTargetY($el[0]);
}
//當目標節點進入可使區域
if (Math.abs(targetY - scrollY) < seeY) {
return true;
} else {
return false;
}
}
/**
* 獲取目標節點距離頁面頂部高度
* @param {HTML Element} [el]
* @private
*/
function _getTargetY(el) {
var tp = el.offsetTop;
if (el.offsetParent) {
while (el = el.offsetParent) {
tp += el.offsetTop;
}
}
return tp;
}
/**
* 特定元素即將出現時的回調函數
* @param {jQuery Obj} [$el]
* @param {Function} [func]
* @private
*/
function _addCallBack($el,func) {
$el.bind(eventType,function(event) {
func.call($el,event);
});
}
return {
init : _init,
addCallBack : _addCallBack
};
})(jQuery);