懒加载(LazyLoad)一直是前端的优化方案之一。它的核心思想是:当用户想看页面某个区域时,再加载该区域的数据。这在一定程度上减轻了服务器端的压力,也加快了页面的呈现速度。 懒加载多用于图片,因为它属于流量的大头。最典型的懒加载实现方案是先将需要懒加载的图片的src隐藏掉,这样图片就不会下载,然后在图片需要呈现给用户时再加上src属性。
公司内部库的懒加载正是采用这种方案。它会遍历页面中所有的图片,将其src缓存起来后删除图片的src属性,当图片进入用户的可视区域后再为图片附加src属性。这种方案存在着以下不足:
① 在IE和FF下,懒加载的脚本运行时,有部分图片已经于服务器建立链接,这部分abort掉,再在滚动时延迟加载,反而增加了链接数。
② 在chrome下,由于webkit内核bug,导致无法abort掉下载,懒加载脚本完全无用。
③ 它只能针对图片的懒加载,但无法懒加载页面的某个模块(即延迟渲染页面的DOM节点)。
因此,在原有的技术方案之上,必须实现新的方案来解决这些问题。受到淘宝的懒加载模块启发,思路如下:
① 提供一种方式来让我们手动为页面中每个需要懒加载的图片缓存它的src属性,例如:原来的图片为,现在改为。这样,页面在解析的时候,所有懒加载的图片在所有的浏览器下都不会下载,图片进入视野区域时再将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',即原始的图片改为。
此外,不管是自动模式和手动模式下,都可以进行模块的懒加载,这时候,需要在每个模块的外层添加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.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);