Lazyload 另一種延遲加載效果

Lazyload 延遲加載效果

Lazyload是通過延遲加載來實現按需加載,達到節省資源,加快瀏覽速度的目的。
網上也有不少類似的效果,這個Lazyload主要特點是:
支持使用window(窗口)或元素作爲容器對象;
對靜態(位置大小不變)元素做了大量的優化;
支持垂直、水平或同時兩個方向的延遲。 
由於內容比較多,下一篇再介紹圖片延遲加載效果。 
兼容:ie6/7/8, firefox 3.5.5, opera 10.10, safari 4.0.4, chrome 3.0


效果預覽

模式:  閾值:    

第 1 個加載
第 2 個加載
第 5 個加載
第 3 個加載
第 4 個加載
第 9 個加載
第 6 個加載
第 10 個加載
第 7 個加載
第 8 個加載
第 15 個加載
第 11 個加載
第 12 個加載
第 13 個加載
第 14 個加載
第 16 個加載
第 17 個加載
第 20 個加載
第 18 個加載
第 19 個加載
第 21 個加載
第 25 個加載
第 22 個加載
第 23 個加載
第 24 個加載
第 26 個加載
第 27 個加載
第 28 個加載
第 29 個加載
第 30 個加載
第 31 個加載
第 32 個加載
第 35 個加載
第 33 個加載
第 34 個加載

利用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 
? "_horizontalDirection" : "_verticalDirection" );


可以看出,這是包裝了primary和secondary參數的_getCrossDirection程序。
其中primary是主滾動方向獲取程序,secondary是次滾動方向獲取程序。
在_getCrossDirection中會根據主輔方向的滾動情況設置正交滾動值:

複製代碼
direction = this[ primary ]();
secondary 
= this[ secondary ]();
if ( !direction && !secondary ) {
    
return 0;
else if ( !direction ) {
    
if ( this._direction ) {
        direction 
= -this._direction;
    } 
else {
        force 
= true;
    }
else if ( secondary && direction * this._direction >= 0 ) {
    force 
= true;
}
複製代碼


包括以下幾個情況:
1,主次方向都沒有滾動的話,直接返回0;
2,主方向沒有滾動而次方向有的話,就用上次滾動的反方向,如果沒有上一次滾動就執行強制加載;
3,主次方向都有滾動,同時主滾動方向跟上次不是相反的話,就執行強制加載;
4,主次方向都有滾動,同時主滾動方向跟上次相反的話,按一般情況處理;
5,主方向有滾動而次方向沒有的話,就是一般的情況,不用特別處理。

利用兩個方向要同時判斷的性質,在情況2只要從主方向加載範圍內的元素找出在次方向也在加載範圍內的就行了。
這個可以通過不斷取反方向來實現,即先從3到7判斷,再反方向7到3判斷如此類推。

情況3和情況4一般發生在刷新或設置了延遲時間比較長的情況。
如果主方向跟上次相同的話,可能會出現索引兩邊都有需要加載的元素的情況,不能確定方向,所以只能執行強制加載。
ps:如果在主方向的滾動量超過加載範圍的話也能做優化,不過判斷比較麻煩就不做了。
而如果方向相反的話,需要加載的元素只會出現在索引到加載範圍的方向上,按一般情況歷遍就行了。

cross模式跟其餘兩個靜態加載模式的最主要區別就在於方向的判斷上。
其他部分都差不多的,就不再詳細說明了。


【resize】

先了解一下瀏覽器拖拉觸發resize的方式。
例如在xp的系統性能選項中,設置是否“拖拉時顯示窗口內容”會有不同的拖拉效果:
選擇是的話,由於內容會跟着瀏覽器的拖拉同時渲染頁面,導致resize事件的持續觸發;
選擇否的話,內容在拖拉完成纔會渲染,並觸發resize事件,即在拖拉過程中resize事件只會在確定後才觸發一次;
不過ff有點特殊,即使選擇否,它右下角的觸發點還是會按照拖拉同時渲染頁面的方式觸發的。
後面測試時建議選擇否,會比較準確看到結果。

再看看resize事件的支持情況。
在ie,haslayout的塊級和內聯元素都支持onresize事件,其他瀏覽器只有window對象支持
而ie6/7跟ie8的支持程度也有不同,測試以下代碼:

複製代碼
<!doctype html>
<body>
<div id="show">0</div>
<div id="div" style="border:1px solid #000"></div>
<script>
var i = 0;
div.onresize 
= function(){ show.innerHTML = ++i; }
setTimeout(
'div.innerHTML="test"'1000)
setTimeout(
'div.style.height="50px"'2000)
</script>
</body>
</html>
複製代碼

在ie8兩種情況都會觸發onresize,但ie6/7只有第二種情況觸發。
鑑於情況比較複雜,程序在使用document時才綁定事件,其他情況由使用者自己設置。

resize事件有不少的問題,處理時要小心。
chrome的resize有一個問題(bug?),每次觸發resize都會執行兩次事件,或者說會觸發兩次。
而ie就複雜了,window, body和documentElement的resize會相互影響。
在ie8測試以下代碼:

複製代碼
<!doctype html>
<style>html,body{border:5px solid #06F;}</style>
<body><div id="div" style="height:100px;"></div></body>
</html>
<script>
window.onresize 
= function(){ div.innerHTML += "window, "; }
//document.documentElement.onresize = function(){ div.innerHTML += "documentElement, "; }
//
document.body.onresize = function(){ div.innerHTML += "body, "; }
</script>
複製代碼


當上下拖放時,onresize只會觸發一次,但左右拖放時會觸發兩次。
換成documentElement會有差不多的結果,兩個一起用的話左右拖放時documentElement會觸發兩次,window一次。
只設置body的話感覺就正常了,上下左右都只會觸發一次。
而documentElement和body同時設置的效果跟documentElement和window的效果差不多。
如果window和body同時設置的話,後一個會覆蓋前一個。
看來window和body的onresize對應的是同一個對象事件,可能爲了在body設置也能做到window一樣的效果。
個人推測,window和documentElement多出的一次,可能是由於同時觸發了body的resize造成的。
ps:onresize時,用srcElement獲取不到觸發元素,所以確定不了是那個元素觸發的。
ie7的結果更ie8差不多,ie6就有些不同,不過估計也是盒模式的不同造成的。
具體產生原因還不清楚,這裏我也很糊塗。

雖然問題弄不清楚,解決方法還是有的。
要綁定resize就是因爲視框範圍發生了變化,要重新設置視框範圍,那麼可以通過看兩次resize之間視框範圍有沒有變化來確實是否執行程序。
在resizeDelay方法中,就是通過clientWidth和clientHeight來判斷的:

複製代碼
this.resizeDelay = function(){
    
var clientWidth = container.clientWidth,
        clientHeight 
= container.clientHeight;
    
if( clientWidth != width || clientHeight != height ) {
        width 
= clientWidth; height = clientHeight;
        oThis._delay( oThis.resize );
    }
};
複製代碼

ps:如果只是針對document直接用window的innerHeight/innerWidth就不用理會文檔模式。


【延時加載】

一般情況下,觸發程序會綁定到容器的scroll和resize事件中。
但很多時候scroll和resize會被連續觸發執行,大量連續的執行會佔用很多資源。
爲了防止無意義的連續執行,程序設置了一個_delay方法來做延時:

複製代碼
var oThis = this, delay = this.delay;
if ( this._lock ) {
    
this._timer = setTimeout( function(){ oThis._delay(run); }, delay );
else {
    
this._lock = true; run();
    setTimeout( 
function(){ oThis._lock = false; }, delay );
}
複製代碼


原理是用一個_lock屬性,程序運行一次後_lock設爲true,並用一個setTimeout延時設置它爲false。
在鎖定(_lock爲true)期間,程序不會立即執行,達到延時的效果。
爲了保證最後一次觸發程序即使在鎖定期間也能完成,還用了一個_timer來延時這次執行。

這種延時有什麼好處,直接用setTimeout延時不是更簡單方便嗎?
首先直接用setTimeout只能保證最後一次程序能執行,而這種方式能保證第一次和最後一次都能執行。
直接用setTimeout更大的問題是,如果持續觸發,會導致程序一直不能執行(前提是執行時有正確clear掉定時器),而這種方式能保證程序在時間段內執行一次。

還有一個方法是不綁定事件,只用setTimeout或setInterval來監聽。
好處是沒有連續觸發的問題,也不會有resize的bug,但需要一直監聽視框範圍是否改變。
一般情況下,綁定事件效率應該比較好。


使用技巧

【選擇模式】

如果加載元素位置固定大小不固定的情況下只能選擇"dynamic"動態加載,否則應該優先選擇靜態加載。
在靜態加載中,如果基本上是用於垂直或水平滾動,應該用"vertical"或"horizontal"模式。
兩個方向都需要的話,如果主要是垂直滾動的話就用"cross-vertical"模式,否則用"cross-horizontal"模式。

【延遲html渲染】

Lazyload的一個作用就是延遲html渲染。
原理是先保存元素裏面的html,當判斷元素在加載範圍裏面時,再加載裏面的html。
程序主要是做判斷的部分,而如何保存和加載就看各位的想象力了。
以下幾種方法個人認爲還不錯的:
1,ajax法:保存地址,加載時利用ajax讀取實際內容並插入到元素中;
使用恰當的話能有效節省服務器資源,特別是要讀數據庫的地方,但響應速度受網絡影響,而且不利seo,類似的還可以用iframe。
2,textarea法:把html保存到一個textarea中,加載時把value插入元素中;
利用了textarea的特性,第二個實例就使用了這個方法,淘寶用的也是這個方式,使用簡單,響應速度快。
不過僅僅是html的話,貌似也沒必要延遲,可以考慮關聯一些dom操作之類的。
2,註釋法:把html保存到一個註釋中,加載時把內容插入元素中;
跟textarea法類似,但效率應該更好,加載時找出nodeType爲8的節點,再把nodeValue插入元素中;
但在ie如果用innerHTML添加註釋會被自動忽略掉,使用時注意。

以上方法都有一個問題,在不支持js的時候不能平穩退化,誰有更好的方法的話歡迎賜教。
除此之外,還可以用來延遲js執行,css渲染等,下一篇還會有圖片的延遲加載

【position的bug】

在寫第一個實例的窗口模式時,遇到了兩個bug:
在ie6/7,overflow爲scroll或hidden的元素,其中position爲absolute或relative的子孫元素會出現異常。
解決方法:
1.爲包含塊元素添加屬性position:relative。
2.把該元素的position:relative屬性去掉,使用默認的static定位,並通過margin-top等屬性實現類似的效果。
參考自“IE6 CSS bug”。

還有一個問題是,在ie6,overflow爲visible的元素,會被其內容撐開。
解決方法:
在ie6下,本來overflow爲visible的元素設爲hidden,並把內容position設爲relative。
原理請看“IE6 overflow:visible bug”。

還要注意的是,加載元素只能是容器的子元素。


使用說明

實例化時,必須有一個元素集合作爲參數,可以是元素數組或NodeList集合。

可選參數用來設置系統的默認屬性,包括:
屬性:    默認值//說明
container: window,//容器
mode:  "dynamic",//模式
threshold: 0,//加載範圍閾值
delay:  100,//延時時間
beforeLoad: function(){},//加載前執行
onLoadData: function(){}//顯示加載數據

還提供了以下方法:
load:加載程序;
resize:容器大小改變加載程序,其參數說明是否重置元素集合;
delayLoad:延遲的load程序;
delayResize:延遲的resize程序;
isFinish:指明程序是否執行完成;
dispose:銷燬程序,其參數說明是否加載所有元素。


程序源碼

複製代碼
var LazyLoad = function(elems, options) {
    
//初始化程序
    this._initialize(elems, options);
    
//如果沒有元素就退出
    if ( this.isFinish() ) return;
    
//初始化模式設置
    this._initMode();
    
//進行第一次觸發
    this.resize(true);
};

LazyLoad.prototype 
= {
  
//初始化程序
  _initialize: function(elems, options) {
    
this._elems = elems;//加載元素集合
    this._rect = {};//容器位置參數對象
    this._range = {};//加載範圍參數對象
    this._loadData = null;//加載程序
    this._timer = null;//定時器
    this._lock = false;//延時鎖定
    //靜態使用屬性
    this._index = 0;//記錄索引
    this._direction = 0;//記錄方向
    this._lastScroll = { "left"0"top"0 };//記錄滾動值
    this._setElems = function(){};//重置元素集合程序
    
    
var opt = this._setOptions(options);
    
    
this.delay = opt.delay;
    
this.threshold = opt.threshold;
    
this.beforeLoad = opt.beforeLoad;
    
    
this._onLoadData = opt.onLoadData;
    
this._container = this._initContainer($$(this.options.container));//容器
  },
  
//設置默認屬性
  _setOptions: function(options) {
    
this.options = {//默認值
        container:    window,//容器
        mode:        "dynamic",//模式
        threshold:    0,//加載範圍閾值
        delay:        100,//延時時間
        beforeLoad:    function(){},//加載前執行
        onLoadData:    function(){}//顯示加載數據
    };
    
return $$.extend(this.options, options || {});
  },
  
//初始化容器設置
  _initContainer: function(container) {
    
var doc = document,
        isWindow 
= container == window || container == doc
            
|| !container.tagName || (/^(?:body|html)$/i).test( container.tagName );
    
if ( isWindow ) {
        container 
= doc.compatMode == 'CSS1Compat' ? doc.documentElement : doc.body;
    }
    
//定義執行方法
    var oThis = this, width = 0, height = 0;
    
this.load = $$F.bind( this._load, this );
    
this.resize = $$F.bind( this._resize, this );
    
this.delayLoad = function() { oThis._delay( oThis.load ); };
    
this.delayResize = function(){//防止重複觸發bug
        var clientWidth = container.clientWidth,
            clientHeight 
= container.clientHeight;
        
if( clientWidth != width || clientHeight != height ) {
            width 
= clientWidth; height = clientHeight;
            oThis._delay( oThis.resize );
        }
    };
    
//記錄綁定元素方便移除
    this._binder = isWindow ? window : container;
    
//綁定事件
    $$E.addEvent( this._binder, "scroll"this.delayLoad );
    isWindow 
&& $$E.addEvent( this._binder, "resize"this.delayResize );
    
//獲取容器位置參數函數
    this._getContainerRect = isWindow && ( "innerHeight" in window )
        
? function(){ return {
                
"left":    0"right":    window.innerWidth,
                
"top":    0"bottom":window.innerHeight
            }}
        : 
function(){ return oThis._getRect(container); }    ;
    
//設置獲取scroll值函數
    this._getScroll = isWindow
        
? function() { return {
                
"left": $$D.getScrollLeft(), "top": $$D.getScrollTop()
            }}
        : 
function() { return {
                
"left": container.scrollLeft, "top": container.scrollTop
            }};
    
return container;
  },
  
//初始化模式設置
  _initMode: function() {
    
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;
    }
  },
  
//初始化靜態加載設置
  _initStatic: function(mode, direction) {
    
//設置模式
    var isVertical = direction == "vertical";
    
if ( mode == "cross" ) {
        
this._crossDirection = $$F.bind( this._getCrossDirection, this,
            isVertical 
? "_verticalDirection" : "_horizontalDirection",
            isVertical 
? "_horizontalDirection" : "_verticalDirection" );
    }
    
//設置元素
    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 );
    };
    
//設置加載函數
    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" ) );
  },
  
//延時程序
  _delay: function(run) {
    clearTimeout(
this._timer);
    
if ( this.isFinish() ) return;
    
var oThis = this, delay = this.delay;
    
if ( this._lock ) {//防止連續觸發
        this._timer = setTimeout( function(){ oThis._delay(run); }, delay );
    } 
else {
        
this._lock = true; run();
        setTimeout( 
function(){ oThis._lock = false; }, delay );
    }
  },
  
//重置範圍參數並加載數據
  _resize: function(change) {
    
if ( this.isFinish() ) return;
    
this._rect = this._getContainerRect();
    
//位置改變的話需要重置元素位置
    if ( change ) { this._setElems(); }
    
this._load(true);
  },
  
//加載程序
  _load: function(force) {
    
if ( this.isFinish() ) return;
    
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
    }
    
//加載數據
    this.beforeLoad();
    
this._loadData(force);
  },
  
//動態加載程序
  _loadDynamic: function() {
    
this._elems = $$A.filter( this._elems, function( elem ) {
            
return !this._insideRange( elem );
        }, 
this );
  },
  
//靜態加載程序
  _loadStatic: function(direction, beforeRange, afterRange, force) {
    
//獲取方向
    direction = this[ direction ]( force );
    
if ( !direction ) return;
    
//根據方向歷遍圖片對象
    var elems = this._elems, i = this._index,
        begin 
= [], middle = [], end = [];
    
if ( direction > 0 ) {//向後滾動
        begin = elems.slice( 0, i );
        
for ( var len = elems.length ; i < len; i++ ) {
            
if ( afterRange( middle, elems[i] ) ) {
                end 
= elems.slice( i + 1 ); break;
            }
        }
        i 
= begin.length + middle.length - 1;
    } 
else {//向前滾動
        end = elems.slice( i + 1 );
        
for ( ; i >= 0; i-- ) {
            
if ( beforeRange( middle, elems[i] ) ) {
                begin 
= elems.slice( 0, i ); break;
            }
        }
        middle.reverse();
    }
    
this._index = Math.max( 0, i );
    
this._elems = begin.concat( middle, end );
  },
  
//垂直和水平滾動方向獲取程序
  _verticalDirection: function(force) {
      
return this._getDirection( force, "top" );
  }, 
  _horizontalDirection: 
function(force) {
      
return this._getDirection( force, "left" );
  },
  
//滾動方向獲取程序
  _getDirection: function(force, scroll) {
    
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;
  },
  
//cross滾動方向獲取程序
  _getCrossDirection: function(primary, secondary, force) {
    
var direction;
    
if ( !force ) {
        direction 
= this[ primary ]();
        secondary 
= this[ secondary ]();
        
if ( !direction && !secondary ) {//無滾動
            return 0;
        } 
else if ( !direction ) {//次方向滾動
            if ( this._direction ) {
                direction 
= -this._direction;//用上一次的相反方向
            } else {
                force 
= true;//沒有記錄過方向
            }
        } 
else if ( secondary && direction * this._direction >= 0 ) {
            force 
= true;//同時滾動並且方向跟上一次滾動相同
        }
    }
    
if ( force ) {
        
this._lastScroll = this._getScroll(); this._index = 0; direction = 1;
    }
    
return ( this._direction = direction );
  },
  
//判斷是否加載範圍內
  _insideRange: function(elem, mode) {
    
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" ];
    
//在加載範圍內加載數據
    if ( inside ) { this._onLoadData(elem); }
    
return inside;
  },
  
//判斷是否超過加載範圍
  _outofRange: function(mode, compare, middle, elem) {
    
if ( !this._insideRange( elem, mode ) ) {
        middle.push(elem);
        
return this[ compare ]( elem._rect );
    }
  },
  _horizontalBeforeRange: 
function(rect) { return rect.right < this._range.left; },
  _horizontalAfterRange: 
function(rect) { return rect.left > this._range.right; },
  _verticalBeforeRange: 
function(rect) { return rect.bottom < this._range.top; },
  _verticalAfterRange: 
function(rect) { return rect.top > this._range.bottom; },
  
//獲取位置參數
  _getRect: function(node) {
    
var n = node, left = 0, top = 0;
    
while (n) { left += n.offsetLeft; top += n.offsetTop; n = n.offsetParent; };
    
return {
        
"left": left, "right": left + node.offsetWidth,
        
"top": top, "bottom": top + node.offsetHeight
    };
  },
  
//是否完成加載
  isFinish: function() {
    
if ( !this._elems || !this._elems.length ) {
        
this.dispose(); return true;
    } 
else {
        
return false;
    }
  },
  
//銷燬程序
  dispose: function(load) {
    clearTimeout(
this._timer);
    
if ( this._elems || this._binder ) {
        
//加載全部元素
        if ( load && this._elems ) {
            $$A.forEach( 
this._elems, this._onLoadData, this );
        }
        
//清除關聯
        $$E.removeEvent( this._binder, "scroll"this.delayLoad );
        $$E.removeEvent( 
this._binder, "resize"this.delayResize );
        
this._elems = this._binder = null;
    }
  }
}
複製代碼

 

完整實例下載 

轉載請註明出處:http://www.cnblogs.com/cloudgamer/

如有任何建議或疑問,歡迎留言討論。

如果覺得文章不錯的話,歡迎點一下右下角的推薦。

程序中包含的js工具庫CJL.0.1.min.js,原文在這裏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章