lazyload延迟加载组件

lazyload现在网上已经用的很多(淘宝商城,新浪微博等等),先放demo:mylazyLoad.zip 

效果:

【基本原理】

在有大量数据加载的页面中,我们需要一个容器,这个容器可以是浏览器窗口,也可以是页面中的一个容器,在页面加载的时候,我们可以将这容器显示之外的内容阻止其加载,当我们滚动这个容器到相应区域的时候才将该区域内容加载出来,以此达到加快浏览速度的目的。

延迟加载一般分静态和动态两种。

静态的典型例子就是淘宝商城,我们在观察淘宝商城的html时发现,其页面采用了大量的textarea来存放页面元素,我们想要把页面元素存放在html里,而又不想这些元素被解析,同时又能轻松方便的获取,textarea正好满足了这些条件(真不知道哪位牛人想到的)。

而动态的可以说就是ajax获取数据再绑定,典型例子就是新浪微博,当页面滚动到底部时触发加载函数。

我的理解是将需要延迟加载的触发元素存入一个数组中,当容器scroll/resize时遍历这个数组,如果触发元素在视窗范围内就执行加载函数,并将这个元素从数组中删除来提高效率。

【程序说明】

一般在创建实例的时候,需要定义两个属性:elems和container,elems是触发加载的元素集合,或者说是加载内容集合,container就是容器。

1.因为elems需要进行删除,所以首先需要将elems转换为数组Array。

$A() code
/*将参数转换为数组
 * @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)||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。

container
var doc=document;
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,即浏览器视窗的大小。

getContainerRange
/*获取容器显示范围方法*/
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事件。

addEventHandler
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);

container是window话,scroll是不会改变container的range的,而container是元素的话,range会随着window的scroll而改变,所以这里做分支处理。

另外当我们讲window的滚动条滚至中间位置,再F5刷新页面之后,滚动条同样会在刚刚的位置,IE下首先会将滚动条提至顶部,再定位到刚刚的位置,这样就出现了一个问题,在第一次执行lazyload的时候,container的range是window滚动条在顶部时候的值,而并非我们需要的实际值。所以我这里在非isWin情况下,给window绑定一次_noWinScroll事件,滚动条定位后删除这个事件。

5.下面讲的是具体的判断、执行

_doLoad
/*加载判断,防止多次调用
 @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
/*加载运行*/
_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"]
}

这里的逻辑如下:

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)当元素集合为空时,摧毁所有的绑定。

_release
_release:function(){
    removeEventHandler(
this.binder,"scroll",this._scrollload);
    removeEventHandler(
this.binder,"resize",this._resizeload);
        
this._onDataEnd();
}

6._onDataLoad默认情况是静态加载。

_onDataLoad
/*删除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;
    }
}
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-~~

【完整代码】

javascript library
// JavaScript Document
/*
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)||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
// JavaScript Document
/*
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;
                }
            })
        }
    })
}

posted on 2011-04-08 15:01  Lecaf  阅读(3471)  评论(0编辑  收藏  举报