现在的位置: 首页 > 综合 > 正文

Lazyload 延迟加载效果

2011年03月29日 ⁄ 综合 ⁄ 共 19499字 ⁄ 字号 评论关闭

Lazyload是通过延迟加载来实现按需加载,达到节省资源,加快浏览速度的目的。
网上也有不少类似的效果,这个Lazyload主要特点是:
支持使用window(窗口)或元素作为容器对象;
对静态(位置大小不变)元素做了大量的优化;
支持垂直、水平或同时两个方向的延迟。
由于内容比较多,下一篇再介绍图片延迟加载效果
兼容:ie6/7/8, firefox 3.5.5, opera 10.10, safari 4.0.4, chrome 3.0

效果预览


模式:

阈值:


利用textarea加载数据:

程序说明

【基本原理】

首先要有一个容器对象,容器里面是_elems加载元素集合。
用隐藏或替换等方法,停止元素加载内容。
然后历遍集合元素,当元素在加载范围内,再进行加载。
加载范围一般是容器的视框范围,即浏览者的视觉范围内。
当容器滚动或大小改变时,再重新历遍元素判断。
如此重复,直到所有元素都加载后就完成。

【容器对象】

程序一开始先用_initContainer程序初始化容器对象。
先判断是用window(窗口)还是一般元素作为容器对象:

var doc = document,
    isWindow 
= container == window || container == doc
        
|| !container.tagName || (/^(?:body|html)$/i).test( container.tagName );

如果是window,再根据文档渲染模式选择对应的文档对象:

if ( isWindow ) {
    container 
= doc.compatMode == 'CSS1Compat' ? doc.documentElement : doc.body;
}

定义好执行方法后,再绑定scroll和resize事件:

this._binder = isWindow ? window : container;

$$E.addEvent( this._binder, "scroll"this.delayLoad );
isWindow 
&& $$E.addEvent( this._binder, "resize"this.delayResize );

如果是window作为容器,需要绑定到window对象上,为了方便移除用了_binder属性来保存绑定对象。

【加载数据】

当容器滚动或大小改变时,就会通过事件绑定(例如scroll/resize)自动执行_load加载程序。
ps:如果不能绑定事件(如resize),应手动执行load或resize方法。

当容器大小改变(resize)时,还需要先执行_getContainerRect程序获取视框范围。
要获取视框范围,一般元素可以通过_getRect方位参数获取程序来获取。
但如果容器是window就麻烦一点,测试以下代码:

代码

<!doctype html>
<style>html,body{border:5px solid #06F;}</style>
<body>
<div style="border:1px solid #000;height:2000px;"></div>
</body>
</html>
<script>
alert(document.documentElement.offsetHeight)
</script>

在ie会得到想要的结果,但其他浏览器得到的是文档本身的高度。
所以在_getContainerRect程序中,其他浏览器要用innerWidth/innerHeight来获取:

代码

this._getContainerRect = isWindow && ( "innerHeight" in window )
    
? function(){ return {
            
"left":    0"right":    window.innerWidth,
            
"top":    0"bottom":window.innerHeight
        }}
    : 
function(){ return oThis._getRect(container); };

ps:更多相关信息可以看“Finding the size of the browser window”。

在_load程序中,先根据位置参数、滚动值和阈值计算_range加载范围参数:

代码

var rect = this._rect, scroll = this._getScroll(),
    left 
= scroll.left, top = scroll.top,
    threshold 
= Math.max( 0this.threshold | 0 );

this._range = {
    top:    rect.top 
+ top - threshold,
    bottom:    rect.bottom 
+ top + threshold,
    left:    rect.left 
+ left - threshold,
    right:    rect.right 
+ left + threshold
}

在_getScroll获取scroll值程序中,如果是document时会通过$$D来获取,详细看这里dom部分
threshold阈值的作用是在视框范围的基础上增大加载范围,实现类似预加载的功能。
最后执行_loadData数据加载程序。

【加载模式】

程序初始化时会执行_initMode初始化模式设置程序。
根据mode的设置,选择加载模式:

代码

switch ( this.options.mode.toLowerCase() ) {
    
case "vertical" :
        
this._initStatic( "vertical""vertical" );
        
break;
    
case "horizontal" :
        
this._initStatic( "horizontal""horizontal" );
        
break;
    
case "cross" :
    
case "cross-vertical" :
        
this._initStatic( "cross""vertical" );
        
break;
    
case "cross-horizontal" :
        
this._initStatic( "cross""horizontal" );
        
break;
    
case "dynamic" ://动态加载
    default :
        
this._loadData = this._loadDynamic;
}

包括以下几种模式:
vertical:垂直方向加载模式
horizontal:水平方向加载模式
cross/cross-vertical:垂直正交方向加载模式
cross-horizontal:水平正交方向加载模式
dynamic:动态加载模式
其中"dynamic"模式是一般的加载方式,没有约束条件,但也没有任何优化。
其余都属于静态加载模式,适用于加载对象集合元素的位置(相对容器)或大小不会改变(包括加载后)的情况。
其中两个正交方向加载模式("cross"模式)适用于两个方向都需要判断的情况。
程序会对静态加载的情况尽可能做优化,所以应该优先选择静态加载模式。

【动态加载】

动态加载是使用_loadDynamic程序作为加载程序的:

this._elems = $$A.filter( this._elems, function( elem ) {
        
return !this._insideRange( elem );
    }, 
this );

程序会用_insideRange程序来判断元素是否在加载范围内,并用filter筛选出加载范围外的元素,重新设置加载集合。

在_insideRange程序中,先用元素位置和加载范围参数作比较,判断出元素是否在加载范围内:

代码

var range = this._range, rect = elem._rect || this._getRect(elem),
    insideH 
= rect.right >= range.left && rect.left <= range.right,
    insideV 
= rect.bottom >= range.top && rect.top <= range.bottom,
    inside 
= {
            
"horizontal":    insideH,
            
"vertical":        insideV,
            
"cross":        insideH && insideV
        }[ mode 
|| "cross" ];

在动态加载中,不会为元素记录位置参数,所以每次都会用_getRect程序获取加载元素的位置信息。
动态加载会默认使用"cross"模式来判断,即水平和垂直方向都判断。
如果元素在加载范围内,会执行_onLoadData自定义加载程序,进行元素的加载。

【静态加载】

静态加载是程序的重点,也是程序的主要特色。
主要是利用集合元素位置大小固定的性质进行优化,利用这个方式会大大提高程序执行效率,越多加载项会越明显。

原理是对加载集合进行排序,转换成有序集合,这样加载范围内的元素总是加载集合中连续的一段。
即可以把加载集合分成3部分,在加载范围前面的,在加载范围内的和加载范围后面的。
以horizontal模式左右滚动为例,加载过程大致如下:
1,记录每个元素的位置参数,按left坐标的大小对加载集合进行排序(从小到大),设置强制加载,跳到1.1;
1.1,记录加载范围,如果是强制加载,跳到1.2,否则跳到2;
1.2,设置索引为0,跳到3;
2,判断滚动的方向,如果向右滚动跳到3,否则跳到4,没有滚动的话取消执行;
3,向后历遍元素,判断元素是否在加载范围内,是的话跳到3.1,否则跳到3.2,如果没有元素,跳到6;
3.1,加载当前元素,并把它从集合中移除,跳回3;
3.2,判断元素的left是否大于容器的right,是的话跳到5,否则跳回3;
4,向前历遍元素,判断元素是否在加载范围内,是的话跳到4.1,否则跳到4.2,如果没有元素,跳到6;
4.1,加载当前元素,并把它从集合中移除,跳回4;
4.2,判断元素的right是否大于容器的left,是的话跳到5,否则跳回4;
5,当前元素已经超过了加载范围,不用继续历遍,跳到6;
6,合并未加载的元素,并记录当前索引,等待滚动,如果全部元素都加载了,就完成退出。
7,当容器滚动时,跳到1.1;当容器大小改变时,设置强制加载,跳到1.1;当容器位置发生变化时,需要重新修正元素坐标,跳到1;

首先加载元素会在_rect属性中记录位置参数,不用重复获取,是一个优化。
更关键的地方是每次滚动只需对上一次索引到加载范围内的元素进行判断,大大减少了判断次数。
大致理解了原理后,后面再详细分析。

在_initMode模式设置中,对静态加载的情况会调用_initStatic初始化静态加载程序。
并传递两个参数mode(模式)和direction(方向)。
根据方向判断方式分三种模式:"vertical"(垂直)、"horizontal"(水平)和"cross"(正交)。
这里先分析一下前两种模式。

在_initStatic程序中,先根据direction设置排序函数,再设置_setElems重置元素集合程序:

代码

var pos = isVertical ? "top" : "left",
    sortFunction 
= function( x, y ) { return x._rect[ pos ] - y._rect[ pos ]; },
    getRect 
= function( elem ) { elem._rect = this._getRect(elem); return elem; };
this._setElems = function() {
    
this._elems = $$A.map( this._elems, getRect, this ).sort( sortFunction );
};

其中_setElems有两个意义,一个是记录元素的坐标参数,还有是把加载集合用map转换成数组并排序。
因为自定义的加载集合有可以是NodeList,而用sort就必须先把它转换成数组。

最后设置_loadData加载函数:

代码

this._loadData = $$F.bind( this._loadStatic, this,
    
"_" + mode + "Direction",
    $$F.bind( 
this._outofRange, this, mode, "_" + direction + "BeforeRange" ),
    $$F.bind( 
this._outofRange, this, mode, "_" + direction + "AfterRange" ) );

其中_loadStatic静态加载程序是程序的核心部分,优化的核心就所在。
这里给它包装了三个参数:
direction:方向获取程序的程序名;
beforeRange:判断是否超过加载范围前面的程序;
afterRange:判断是否超过加载范围后面的程序。
通过包装,除了方便参数的使用,还能使程序结构更加清晰。

direction可能是"_verticalDirection"(垂直滚动方向获取程序)或"_horizontalDirection"(水平滚动方向获取程序)。
在里面在调用_getDirection程序获取滚动方向:

代码

var now = this._getScroll()[ scroll ], _scroll = this._lastScroll;
if ( force ) { _scroll[ scroll ] = now; this._index = 0return 1; }
var old = _scroll[ scroll ]; _scroll[ scroll ] = now;
return now - old;

原理是通过_getScroll获取当前的滚动值跟上一次的滚动值_lastScroll相差的结果来判断。
如果结果是0,说明没有滚动,如果大于0,说明是向后滚动,否则就是向前滚动。
然后记录当前滚动值作为下一次的参考值。
如果是强制执行(force为true),就重置_index属性为0,并返回1,模拟初始向后滚动的情况。
强制执行适合在不能根据方向做优化的情况下使用,例如第一次加载、resize、刷新等。
这时虽然不能做优化,但保证了加载的准确性。

在_loadStatic中,先用direction获取方向值:

direction = this[ direction ]( force );
if ( !direction ) return;

没有滚动的话就直接返回。

然后根据方向和上一次的索引来历遍加载集合,其中关键的一点是判断元素是否超过加载范围。
这个主要是通过beforeRange和afterRange程序来判断的。
从_loadData的设置可以看出,它们是包装了对应compare判断程序参数的_outofRange程序。
在"_vertical"方向,compare可能是:
_verticalBeforeRange:垂直平方向上判断元素是否超过加载范围的上边;
_verticalAfterRange:垂直方向上判断元素是否超过加载范围的下边。
在"horizontal"方向,compare可能是:
_horizontalBeforeRange:水平方向上判断元素是否超过加载范围的左边;
_horizontalAfterRange:水平方向上判断元素是否超过加载范围的右边。
在_outofRange中,通过compare来判断是否超过范围:

if ( !this._insideRange( elem, mode ) ) {
    middle.push(elem);
    
return this[ compare ]( elem._rect );
}

先用_insideRange判断元素是否在加载范围内,不是的话把元素保存到middle,再用compare判断是否超过加载范围。

回到_loadStatic程序,根据方向判断,如果是向后滚动,先根据索引,取出加载范围前面的元素,保存到begin:

begin = elems.slice( 0, i );

这一部分肯定在加载范围外,不需要再历遍,再向后历遍集合:

for ( var len = elems.length ; i < len; i++ ) {
    
if ( afterRange( middle, elems[i] ) ) {
        end 
= elems.slice( i + 1 ); break;
    }
}
= begin.length + middle.length - 1;

当afterRange判断超过加载范围后面,根据当前索引取出后面的元素,保存到end。
然后修正索引,给下一次使用。

如果是向前滚动,跟前面相反,根据索引取出加载范围后面的元素,保存到end:

end = elems.slice( i + 1 );

再向前历遍集合:

for ( ; i >= 0; i-- ) {
    
if ( beforeRange( middle, elems[i] ) ) {
        begin 
= elems.slice( 0, i ); break;
    }
}
middle.reverse();

当beforeRange判断超过加载范围前面,根据当前索引取出前面的元素,保存到begin。
由于middle在beforeRange里面是用push添加的,但实际上是倒序历遍,所以要reverse一下。
ps:虽然push/reverse可以直接用unshift代替,但元素越多前者的效率会越高。

最后修正一下索引,合并begin、middle和end成为新的加载集合:

this._index = Math.max( 0, i );
this._elems = begin.concat( middle, end );

这样就完成了一次加载,等待下一次了。

这部分有点抽象,不太好表达,有什么疑问的地方欢迎提出。

【cross模式】

cross模式即正交方向加载模式,是指垂直和水平都需要判断的模式。
也就是说,元素需要同时在两个方向的加载范围内才会加载。
按主次方向又分两种模式:"cross-vertical"(垂直正交)和"cross-horizontal"(水平正交)。
前者以垂直方向为主,水平方向为次,后者相反。

在_initStatic程序中,如果使用cross模式,会设置_crossDirection滚动方向获取程序:

this._crossDirection = $$F.bind( this._getCrossDirection, this,
    isVertical 
? "_verticalDirection" : "_horizontalDirection",
    isVertical 
?

抱歉!评论已关闭.