lazyload延迟加载组件
lazyload现在网上已经用的很多(淘宝商城,新浪微博等等),先放demo:mylazyLoad.zip
效果:
【基本原理】
在有大量数据加载的页面中,我们需要一个容器,这个容器可以是浏览器窗口,也可以是页面中的一个容器,在页面加载的时候,我们可以将这容器显示之外的内容阻止其加载,当我们滚动这个容器到相应区域的时候才将该区域内容加载出来,以此达到加快浏览速度的目的。
延迟加载一般分静态和动态两种。
静态的典型例子就是淘宝商城,我们在观察淘宝商城的html时发现,其页面采用了大量的textarea来存放页面元素,我们想要把页面元素存放在html里,而又不想这些元素被解析,同时又能轻松方便的获取,textarea正好满足了这些条件(真不知道哪位牛人想到的)。
而动态的可以说就是ajax获取数据再绑定,典型例子就是新浪微博,当页面滚动到底部时触发加载函数。
我的理解是将需要延迟加载的触发元素存入一个数组中,当容器scroll/resize时遍历这个数组,如果触发元素在视窗范围内就执行加载函数,并将这个元素从数组中删除来提高效率。
【程序说明】
一般在创建实例的时候,需要定义两个属性:elems和container,elems是触发加载的元素集合,或者说是加载内容集合,container就是容器。
1.因为elems需要进行删除,所以首先需要将elems转换为数组Array。
* @param {all} a 参数
*/
var $A=function(a){
if(!a)return [];
if(a instanceof Array) return a;
var arr=[],len=a.length;
if(/string|number/.test(typeof a)||a instanceof Function || len===undefined){
arr[0]=a;
}else{
for(var i=0;i<len;i++){
arr[i]=a[i];
}
}
return arr;
}
this.elems=$A(this.options.elems);/*加载对象转换成数组*/
参数a可以是string,number,object,array,function,HTMLCollection,null。
PS:我发现object.length===undefined
2.container是容器,可以是window,也可以是页面元素,所以初始化时先判断是否是window。
var isWin = c==window||c==doc||c==null||!c.tagName||/body|html/i.test(c.tagName);/*判断容器是否是window*/
if(isWin)c=doc.documentElement;
this.container=c;
如果不是明确的非body/html的dom元素,将都视为容器为window。
3.获取container的显示范围,即容器相对于浏览器视窗左上角的top/bottom/left/right距离,如果是window,即浏览器视窗的大小。
var _getContainerRange=isWin&&window.innerWidth?function(){
return {top:0,left:0,right:window.innerWidth,bottom:window.innerHeight}
}:function(){
return _this._getRect(c);
}
this._refreshRange=function(){
_this.range=_getContainerRange();
}
this._refreshRange();
/*获取元素位置参数*/
_getRect:function(elem){
var r=elem.getBoundingClientRect();/*元素到窗口左上角距离*/
return {top:r.top,left:r.left,bottom:r.bottom,right:r.right}
}
获取浏览器视窗大小在IE下可以通过offsetWidth/offsetHeight获得,但非IE浏览器则略有差异,好在非IE下有innerWidth/innerHeight可以使用。另外还有dom.getBoundingClientRect()这个方法,它的作用是获得dom相对于视窗左上角的top/left/bottom/right距离,而且这个方法已经得到所有浏览器的支持,大大提高了我们的效率。
PS:getBoundingClientRect可以看下http://www.cnblogs.com/qieqing/archive/2008/10/06/1304399.html ,另外谈到IE下2px问题,因为这里我都是用这个方法来获取容器以及元素位置,所以这2px差异可以忽略,也许这不太严谨,但同时也为了提高点效率考虑。
4.接下来就是给container绑定scroll/resize事件。
if(!isWin){_this._refreshRange();}
_this._doLoad();
}
this._noWinScroll=function(){ /*解决刷新时window滚动条定位后造成range错误bug*/
_this.range=_getContainerRange();
removeEventHandler(window,"scroll",_this._noWinScroll);
}
this._resizeload=function(){
_this._refreshRange();
_this._doLoad();
}
this.binder = isWin ? window : c;
if(!isWin)addEventHandler(window,"scroll",this._noWinScroll);
addEventHandler(this.binder,"scroll",this._scrollload);
addEventHandler(this.binder,"resize",this._resizeload);
container是window话,scroll是不会改变container的range的,而container是元素的话,range会随着window的scroll而改变,所以这里做分支处理。
另外当我们讲window的滚动条滚至中间位置,再F5刷新页面之后,滚动条同样会在刚刚的位置,IE下首先会将滚动条提至顶部,再定位到刚刚的位置,这样就出现了一个问题,在第一次执行lazyload的时候,container的range是window滚动条在顶部时候的值,而并非我们需要的实际值。所以我这里在非isWin情况下,给window绑定一次_noWinScroll事件,滚动条定位后删除这个事件。
5.下面讲的是具体的判断、执行
@lock锁定,加载过程中锁定。如果为false,执行加载;如果为true,延迟递归
*/
_doLoad:function(){
var _this=this;
if(!this.lock){
this.lock=true;
setTimeout(function(){_this._loadRun()},100);
}else{
clearTimeout(this.timer);
var self=arguments.callee;
this.timer=setTimeout(function(){self.call(_this)},100);
}
}
scroll事件在各个浏览器里的执行次数不同,最好的是FF,一次滚轮滚动只会执行一次,而其他浏览器或多或少的会执行多次。为了优化这点,用了一个lock判断,当在进行加载函数_loadRun时,我们锁住scroll,具体方法就是"clearTimeout(this.timer);this.timer=setTimeout(function(){_this._doLoad();},100);",使用setTimeout延迟递归_doLoad,执行第二次递归前clearTimeout上一次,这样就能保证一次滚动只执行两次,开始一次,结束一次,中间的全部锁住。
_loadRun:function(){
var elems=this.elems;
if(elems.length){
for(var i=0;i<elems.length;i++){
var rect=this._getRect(elems[i]);
var side=this._isRange(this._inRange(rect));
if(side&&side!=0){
if(side==1&&!this.elock){
this.elock=true;
this._onDataLoad(elems[i]);
elems.splice(i--,1);/*加载完之后将该对象从队列中删除*/
}else{break;}
}
}
if(!elems.length){
this._release();
}
}
this.lock=false;
}
var range=this.range;
var side={
v : rect.top<=range.bottom ? rect.bottom>=range.top ? "in" : "" : "bottom",/*垂直位置*/
h : rect.left<=range.right ? rect.right>=range.left ? "in" : "" : "right" /*水平位置*/
};
return side;
},
_isRange:function(side){
/*1:加载 -1:跳出循环 0:不加载执行下一个*/
return {
v:side.v ? side.v=="in"?1:-1 : 0,
h:side.h ? side.h=="in"?1:-1 : 0,
c:side.v&&side.h ? side.v=="in"&&side.h=="in"? 1:side.v!="in"?-1:0 : 0
}[this.mode||"c"]
}
这里的逻辑如下:
1)遍历elems数组,获取元素[0]的rect,根据rect和container的range做比较,判断元素[0]相对于container的位置("[top/left]","bottom/right","in")
2)再根据我们的mode获得操作类型(-1,0,1)。-1表示在容器显示范围的后面,之后的元素可以不再判断,执行跳出循环;0表示在容器显示范围的前面,不执行加载,进行下个元素的判断;1表示在显示范围内,需要加载。
3)当返回的side为1时,执行_onDataLoad(),然后从元素集合中删除该元素,用的方法是Array.splice(index,num),同时i--,使得能准确的找到下个元素。这里的elock用来锁定元素加载,主要用在动态ajax加载的时候,因为动态加载的时候,我们希望当多个元素同时存在于container以及多次触发scroll时,只执行第一个元素的加载。
4)然后进行下个元素[1]的判断,重复之前的步骤。
5)当元素集合为空时,摧毁所有的绑定。
removeEventHandler(this.binder,"scroll",this._scrollload);
removeEventHandler(this.binder,"resize",this._resizeload);
this._onDataEnd();
}
6._onDataLoad默认情况是静态加载。
String.prototype.removeJS=function(){
return this.replace(/<script[^>]*?>([\w\W]*?)<\/script>/ig,"");
}
/*将Script字符串转换为Script对象,返回Script or false*/
String.prototype.getJS=function(){
var js=this.replace(/[\s\S]*?<script[^>]*?>([\w\W]*?)<\/script>[\s\S]*?/g,"$1\r");
if(js==this){
return false;
}else{
var s=document.createElement("script");
s.text=js;
return s;
}
}
this._onDataLoad=this.options.ondataload || function(elem){ /*数据加载*/
var h=elem.getElementsByTagName("textarea");
if(h.length){
var js=h[0].value.getJS(); /*解决innerHTML javascript不执行的问题*/
if(js){
elem.innerHTML=h[0].value.removeJS(); /*删除javascript字符串*/
elem.appendChild(js);
}else{
elem.innerHTML=h[0].value;
}
}
this.elock=false;
}
这里主要说的是html里的javascript代码问题,通过innerHTML的javascript代码是不会执行的,所以在这里需要提取html里的script代码,创建一个script元素,appendChild进容器内才能执行。
【总结】
总的来说在优化上以及需求的考虑上都有了提高。也越来越喜欢用自己整理的框架去组件,这样就能做到不仅知其然而且还能知其所以然。希望今后能在算法上得到指点。
demo丑了点,大家凑合凑合(^。^)y-~~
【完整代码】
/*my javascript library v1.2*/
/*written by Lecaf*/
/*update by 2011.4.12*/
/*删除Script字符串内容*/
String.prototype.removeJS=function(){
return this.replace(/<script[^>]*?>([\w\W]*?)<\/script>/ig,'');
}
/*将Script字符串转换为Script对象,返回Script or false*/
String.prototype.getJS=function(){
var js=this.replace(/[\s\S]*?<script[^>]*?>([\w\W]*?)<\/script>[\s\S]*?/g,'$1\r');
if(js==this){
return false;
}else{
var s=document.createElement('script');
s.text=js;
return s;
}
}
/*getElementById
* @param {String} id ID值
*/
var $id = function(id){
if(typeof id!='undefined' && typeof id === 'string'){
return document.getElementById(id);
}
return null;
}
/*讲参数转换为数组
* @param {all} a 参数
*/
var $A=function(a){
if(!a)return [];
if(a instanceof Array) return a;
var arr=[],len=a.length;
if(/string|number/.test(typeof a)||a instanceof Function || len===undefined){
arr[0]=a;
}else{
for(var i=0;i<len;i++){
arr[i]=a[i];
}
}
return arr;
}
/*注销事件
* @param {Object} oTarget 对象
* @param {String} sEventType 事件类型
* @param {Function} fnHandler 事件方法
*/
var removeEventHandler=function(oTarget, sEventType, fnHandler) {
if(oTarget.listeners[sEventType]){
var listeners=oTarget.listeners[sEventType];
for(var i=0,fn;fn=listeners[i++];){
if(fn==fnHandler){
listeners.splice(--i,1);
}
}
if(!listeners.length&&listeners["_handler"]){
oTarget.removeEventListener ? oTarget.removeEventListener(sEventType, listeners["_handler"], false) : oTarget.detachEvent('on' + sEventType, listeners["_handler"]);
}
}
}
/*添加事件
* @param {Object} oTarget 对象
* @param {String} sEventType 事件类型
* @param {Function} fnHandler 事件方法
*/
var addEventHandler=function(oTarget, sEventType, fnHandler) {
oTarget.listeners=oTarget.listeners||{};
var listeners = oTarget.listeners[sEventType] = oTarget.listeners[sEventType]||[];
listeners.push(fnHandler);
if(!listeners["_handler"]){
listeners["_handler"]=function(e){
var e=e||window.event;
for(var i=0,fn;fn=listeners[i++];){
fn.call(oTarget,e)
}
}
oTarget.addEventListener ? oTarget.addEventListener(sEventType, listeners["_handler"], false) : oTarget.attachEvent('on' + sEventType, listeners["_handler"]);
}
}
/*触发事件
* @param {Object} oTarget 对象
* @param {String} sEventType 事件类型
*/
var dispatchEventHandler=function(oTarget,sEventType){
if(oTarget.dispatchEvent){
var e=document.createEvent('Event');
e.initEvent(sEventType,true,true);
oTarget.dispatchEvent(e);
}else{
oTarget.fireEvent('on'+sEventType);
}
}
/*json扩展
* @param {Object} target 目标json
* @param {Object} src 源json
*/
var extendJson=function(target,src){
for(var para in src){
target[para]=src[para];
}
return target;
}
/*在目标元素之后插入新元素 js自带方法: target.appendChild(newDoc);target.insertBefore(newDoc,existingChild);
* @param {Document} newEl 新元素
* @param {Document} targetEl 目标元素
*/
var insertAfter=function(newEl,targetEl){
var parentEl = targetEl.parentNode;
if(parentEl.lastChild == targetEl){
parentEl.appendChild(newEl);
}else{
parentEl.insertBefore(newEl,targetEl.nextSibling);
}
}
/*动态加载CSS文件
* @param {String} file css路径
* @param {String} cssid css link ID
*/
var loadCSS=function (file,cssid){
var cssTag = cssid ? document.getElementById(cssid) : null;
var head = document.getElementsByTagName('head').item(0);
if(cssTag) head.removeChild(cssTag);
css = document.createElement('link');
css.href = file;
css.rel = 'stylesheet';
css.type = 'text/css';
if(cssid){css.id = cssid;}
head.appendChild(css);
}
/*ajax封装
* @param {Object} options 参数集
* @param {String} url 链接
* @param {String} type 传参方式 'POST' or 'GET'(默认)
* @param {Bool} async 是否异步 true异步(默认) false同步
* @param {String} dataType 返回数据类型 'html'(默认) 'xml' 'json'
* @param {Function} beforeSend 发送请求前调用函数
* @param {Function} success 请求成功后回调函数
* @param {Function} complete 请求完成后回调函数(不管成功与否)
*/
var ajaxFun = function(options){
var ajaxops={
url:'',
type:'GET',
async:true,
dataType:'html',
beforeSend:null,
success:function(){},
complete:null
}
var ajaxops = extendJson(ajaxops,options);
if(ajaxops.url){
var xmlHttp;
try{
// Firefox, Opera 8.0+, Safari
xmlHttp=new XMLHttpRequest();
}catch (e){
// Internet Explorer
try{
xmlHttp=new ActiveXObject('Msxml2.XMLHTTP');
}catch (e){
try{
xmlHttp=new ActiveXObject('Microsoft.XMLHTTP');
}catch (e){
alert('您的浏览器不支持AJAX!');
return false;
}
}
}
var requestDone=false;
if(!ajaxops.async&&navigator.userAgent.indexOf('Firefox')>0){
xmlHttp.onload=function(){
if(( xmlHttp.status >= 200 && xmlHttp.status < 300 ) || xmlHttp.status === 304 || xmlHttp.status === 1223 || xmlHttp.status === 0){
var msg;
switch(ajaxops.dataType){
case 'html':
msg=xmlHttp.responseText;
break;
case 'xml':
msg=xmlHttp.responseXML;
break;
case 'json':
msg=xmlHttp.responseText;
msg=(new Function('return '+msg))();
break;
default:
msg=xmlHttp.responseText;
break;
}
ajaxops.success(msg);
}
if(ajaxops.complete && !requestDone){
ajaxops.complete(msg);
requestDone=true;
}
}
}else{
xmlHttp.onreadystatechange=function(){
if(xmlHttp.readyState===4){
if(( xmlHttp.status >= 200 && xmlHttp.status < 300 ) || xmlHttp.status === 304 || xmlHttp.status === 1223 || xmlHttp.status === 0){
var msg;
switch(ajaxops.dataType){
case 'html':
msg=xmlHttp.responseText;
break;
case 'xml':
msg=xmlHttp.responseXML;
break;
case 'json':
msg=xmlHttp.responseText;
msg=(new Function('return '+msg))();
break;
default:
msg=xmlHttp.responseText;
break;
}
ajaxops.success(msg);
}
if(ajaxops.complete && !requestDone){
ajaxops.complete(msg);
requestDone=true;
}
}
}
}
if(ajaxops.beforeSend){
ajaxops.beforeSend();
}
xmlHttp.open(ajaxops.type,ajaxops.url,ajaxops.async);
xmlHttp.send(null);
}
}
/*
* $class 写类工具函数
* @param {Function} constructor
* @param {Object} prototype
* write by Snandy http://www.cnblogs.com/snandy/
*/
var $class = function(constructor,prototype) {
var c = constructor || function(){};
var p = prototype || {};
return function() {
for(var atr in p) {
arguments.callee.prototype[atr] = p[atr];
}
c.apply(this,arguments);
}
}
/*Lazyload v1.2*/
/*written by Lecaf*/
/*update by 2011.4.8*/
var Lazyload=function(options){
this._init(options);/*初始化*/
this._doLoad();/*第一次加载*/
if(!this.elems.length)this._release();/*如果加载元素为空,释放*/
}
var proto={
/*初始化参数*/
_init:function(options){
this.binder=null; /*加载容器对象*/
this.range={}; /*加载容器显示范围*/
this.elems=[];/*加载对象队列*/
this.container=null;
this.mode="";
this.lock=false;/*加载容器锁定*/
this.elock=false;/*加载元素锁定*/
this.timer=null;/*_doLoad计时器*/
this.options={ /*定制参数*/
container:window,/*加载容器*/
elems:null,/*加载数据集合*/
mode:"v",/*加载模式 v(垂直加载) h(水平加载) c(交叉加载) 默认v*/
ondataload:null,/*数据加载方式*/
ondataend:function(){}/*数据加载完毕*/
}
extendJson(this.options,options||{});
this.elems=$A(this.options.elems);/*加载对象转换成数组*/
this.mode=this.options.mode;
this._onDataLoad=this.options.ondataload || function(elem){ /*数据加载*/
var h=elem.getElementsByTagName("textarea");
if(h.length){
var js=h[0].value.getJS(); /*解决innerHTML javascript不执行的问题*/
if(js){
elem.innerHTML=h[0].value.removeJS(); /*删除javascript字符串*/
elem.appendChild(js);
}else{
elem.innerHTML=h[0].value;
}
}
this.elock=false;
}
this._onDataEnd=this.options.ondataend; /*所有内容加载完执行*/
this._initContainer(this.options.container);/*初始化容器*/
},
/*初始化容器*/
_initContainer:function(c){
var doc=document;
var _this=this;
var isWin = c==window||c==doc||c==null||!c.tagName||/body|html/i.test(c.tagName);/*判断容器是否是window*/
if(isWin)c=doc.documentElement;
this.container=c;
/*获取容器显示范围方法*/
var _getContainerRange=isWin&&window.innerWidth?function(){
return {top:0,left:0,right:window.innerWidth,bottom:window.innerHeight}
}:function(){
return _this._getRect(c);
}
this._refreshRange=function(){
_this.range=_getContainerRange();
}
this._refreshRange();
this._scrollload=function(){
if(!isWin){_this._refreshRange();}
_this._doLoad();
}
this._noWinScroll=function(){ /*解决刷新时window滚动条定位后造成range错误bug*/
_this.range=_getContainerRange();
removeEventHandler(window,"scroll",_this._noWinScroll);
}
this._resizeload=function(){
_this._refreshRange();
_this._doLoad();
}
this.binder = isWin ? window : c;
if(!isWin)addEventHandler(window,"scroll",this._noWinScroll);
addEventHandler(this.binder,"scroll",this._scrollload);
addEventHandler(this.binder,"resize",this._resizeload);
},
/*获取元素位置参数*/
_getRect:function(elem){
var r=elem.getBoundingClientRect();/*元素到窗口左上角距离*/
return {top:r.top,left:r.left,bottom:r.bottom,right:r.right}
},
/*加载判断,防止多次调用
@lock锁定,加载过程中锁定。如果为false,执行加载;如果为true,延迟递归
*/
_doLoad:function(){
var _this=this;
if(!this.lock){
this.lock=true;
setTimeout(function(){_this._loadRun()},100);
}else{
clearTimeout(this.timer);
var self=arguments.callee;
this.timer=setTimeout(function(){self.call(_this)},100);
}
},
/*加载运行*/
_loadRun:function(){
var elems=this.elems;
if(elems.length){
for(var i=0;i<elems.length;i++){
var rect=this._getRect(elems[i]);
var side=this._isRange(this._inRange(rect));
if(side&&side!=0){
if(side==1&&!this.elock){
this.elock=true;
this._onDataLoad(elems[i]);
elems.splice(i--,1);/*加载完之后将该对象从队列中删除*/
}else{break;}
}
}
if(!elems.length){
this._release();
}
}
this.lock=false;
},
/*判断对象相对容器位置*/
_inRange:function(rect){
var range=this.range;
var side={
v : rect.top<=range.bottom ? rect.bottom>=range.top ? "in" : "" : "bottom",/*垂直位置*/
h : rect.left<=range.right ? rect.right>=range.left ? "in" : "" : "right" /*水平位置*/
};
return side;
},
_isRange:function(side){
/*1:加载 -1:跳出循环 0:不加载执行下一个*/
return {
v:side.v ? side.v=="in"?1:-1 : 0,
h:side.h ? side.h=="in"?1:-1 : 0,
c:side.v&&side.h ? side.v=="in"&&side.h=="in"? 1:side.v!="in"?-1:0 : 0
}[this.mode||"c"]
},
/*释放*/
_release:function(){
removeEventHandler(this.binder,"scroll",this._scrollload);
removeEventHandler(this.binder,"resize",this._resizeload);
this._onDataEnd();
}
}
window.onload=function(){
var Divload=$class(Lazyload,proto);
var divload=new Divload({
elems:document.getElementById("loadmain").getElementsByTagName("div"),
container:$id("loadbox"),
mode:"c"
});
var Winload=$class(Lazyload,proto);
var winload=new Winload({
elems:$id("ajaxbox").getElementsByTagName("div"),
container:window,
ondataload:function(elem){
var othis=this;
ajaxFun({
url:"ajax.html",
beforeSend:function(){
elem.getElementsByTagName("p")[0].style.display="";
},
success:function(msg){
var box=document.getElementById("ajaxload");
box.innerHTML=box.innerHTML+msg;
},
complete:function(){
othis.elock=false;
}
})
}
})
}