jQuery源代碼解析(3)—— ready載入、queue隊列
ready、queue放在一塊寫,沒有特殊的意思,僅僅是相對來說它倆可能源代碼是最簡單的了。ready是在dom載入完畢後。以最高速度觸發,非常實用。
queue是隊列。比方動畫的順序觸發就是通過默認隊列’fx’處理的。
(本文採用 1.12.0 版本號進行解說,用 #number 來標註行號)
ready
非常多時候,我們須要盡快的載入一個函數,假設裏面含有操作dom的邏輯,那麽最好在dom剛剛載入完畢時調用。window的load事件會在頁面中的一切都載入完畢時(圖像、js文件、css文件、iframe等外部資源)觸發,可能會因外部資源過多而過遲觸發。
DOMContentLoaded
html5規範指定的標準事件,在document上,在形成完整的dom樹後就會觸發(不理會圖像、js文件、css文件等是否下載完畢)。
readystatechange
:IE、Firfox4+、Opera
這個事件的目的是提供與文檔或元素的載入狀態相關的信息,但這個事件的行為有時候非常難預料。支持該事件的每一個對象都有一個readyState屬性,可能包括下列5個值中的一個。
uninitialized(未初始化):對象存在但尚未初始化
loading(正在載入):對象載入數據完畢
interactive(交互):能夠操作對象了,但還沒有全然載入
complete(完畢):對象已經載入完畢
對document而言,值為”interactive”的readyState會在與DOMContentLoaded
大致同樣時刻觸發readystatechange
(行為難料,該階段既可能早於也可能晚於complete階段,jq上報告了一個interactive的bug,所以源代碼中用的complete)。而且在包括較少或較小的外部資源的頁面中,readystatechange
有可能晚於load事件,因此優先使用DOMContentLoaded
。
jQuery思路
jq能夠通過$(xx).ready(fn)
指定dom載入完後須要盡快調用的事件。
我們知道事件一旦錯過了監聽,就不會再觸發,$().ready()添加了遞延支持,這裏自然要使用‘once memory‘
// #3539
jQuery.fn.ready = function( fn ) {
// jQuery.ready.promise() 為deferred對象內的promise對象(即readyList.promise())
jQuery.ready.promise().done( fn );
// 鏈式
return this;
};
有了promise對象。須要dom載入完後,盡快的resolve這個promise。
推斷載入完的方式。就是首先推斷是否已經是載入完畢狀態,假設不是優先使用DOMContentLoaded事件,IE6-8用readystatechange,都要用load事件保底,保證一定觸發。因為readystatechange為complete時機詭異有時甚至慢於load。IE低版本號能夠用定時器反復document.documentElement.doScroll(‘left‘)
推斷,僅僅有dom載入完畢調用該方法才不報錯。從而實現盡快的觸發。
jQuery是富有極客精神的。綁定的觸發函數調用一次後就不再實用,因此觸發函數中不僅能resolve那個promise。還會自己主動解綁觸發函數(方法detach()
)。這樣比方readystatechange、load多事件不會反復觸發,同一時候節省內存。
當然doScroll方法是setTimeout完畢的,假設被readystatechange搶先觸發。須要有變量能告知他取消操作,源代碼中是jQuery.isReady
。
觸發函數->completed()
= 解綁觸發函數->detach()
+ resolve那個promise->jQuery.ready()
jq中添加了holdReady(true)
功能,能夠延緩promise的觸發,holdReady()
不帶參數(即jQuery.ready(true))則消減延遲次數,readyWait
初始為1,減至0觸發。
因為doScroll靠jQuery.isReady防止反復觸發。因此即使暫緩jQuery.ready()也要能正常的設置jQuery.isReady = true
。
jQuery.ready()不僅能觸發promise。之後還會觸發’ready’自己定義事件。
思路整理
jQuery.fn.ready() -> 供外部使用,向promise上綁定待運行函數
jQuery.ready.promise() -> 生成單例promise,綁定事件觸發completed()
complete() -> 解綁觸發函數`detach()` + 無需等待時resolve那個promise`jQuery.ready()`
[源代碼]
// #3536
// readyList.promise() === jQuery.ready.promise()
var readyList;
jQuery.fn.ready = function( fn ) {
// promise後加入回調
jQuery.ready.promise().done( fn );
return this; // 鏈式
};
jQuery.extend( {
// doScroll需借此推斷防止反復觸發
isReady: false,
// 須要幾次jQuery.ready()調用。才會觸發promise和自己定義ready事件
readyWait: 1,
holdReady: function( hold ) {
if ( hold ) {
// true,延遲次數 +1
jQuery.readyWait++;
} else {
// 無參數。消減次數 -1
jQuery.ready( true );
}
},
// 觸發promise和自己定義ready事件
ready: function( wait ) {
// ready(true)時,消減次數的地方。也能取代幹ready()的事
if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
return;
}
// ready()調用時,標記dom已載入完畢
jQuery.isReady = true;
// ready()能夠設置isReady,僅僅能消減默認的那1次
if ( wait !== true && --jQuery.readyWait > 0 ) {
return;
}
// 觸發promise,jQuery.fn.ready(fn)綁定函數都被觸發
readyList.resolveWith( document, [ jQuery ] );
// 觸發自己定義ready事件,並刪除事件綁定
if ( jQuery.fn.triggerHandler ) {
jQuery( document ).triggerHandler( "ready" );
jQuery( document ).off( "ready" );
}
}
} );
// 解綁函數
function detach() {
if ( document.addEventListener ) {
document.removeEventListener( "DOMContentLoaded", completed );
window.removeEventListener( "load", completed );
} else {
document.detachEvent( "onreadystatechange", completed );
window.detachEvent( "onload", completed );
}
}
// detach() + jQuery.ready()
function completed() {
// readyState === "complete" is good enough for us to call the dom ready in oldIE
if ( document.addEventListener ||
window.event.type === "load" ||
document.readyState === "complete" ) {
detach();
jQuery.ready();
}
}
jQuery.ready.promise = function( obj ) {
if ( !readyList ) {
readyList = jQuery.Deferred();
// 推斷運行到這時。是否已經載入完畢
if ( document.readyState === "complete" ) {
// 不再須要綁定不論什麽監聽函數。直接觸發jQuery.ready。延遲一會,等代碼運行完
window.setTimeout( jQuery.ready );
// Standards-based browsers support DOMContentLoaded
} else if ( document.addEventListener ) {
// Use the handy event callback
document.addEventListener( "DOMContentLoaded", completed );
// 個別瀏覽器情況,錯過了事件仍可觸發
window.addEventListener( "load", completed );
// IE6-8不支持"DOMContentLoaded"
} else {
// Ensure firing before onload, maybe late but safe also for iframes
document.attachEvent( "onreadystatechange", completed );
// A fallback to window.onload, that will always work
window.attachEvent( "onload", completed );
// If IE and not a frame
// continually check to see if the document is ready
var top = false;
try {
top = window.frameElement == null && document.documentElement;
} catch ( e ) {}
if ( top && top.doScroll ) {
( function doScrollCheck() {
// 防止反復觸發
if ( !jQuery.isReady ) {
try {
top.doScroll( "left" );
} catch ( e ) {
return window.setTimeout( doScrollCheck, 50 );
}
detach();
jQuery.ready();
}
} )();
}
}
}
return readyList.promise( obj );
};
// 運行。生成deferred對象。綁定好監聽邏輯
jQuery.ready.promise();
queue
jQuery提供了一個多用途隊列,animate加入的動畫就是使用默認的’fx’隊列完畢的。動畫的特點。是在元素上一經加入,即刻觸發。而且該元素一個動畫運行完,才會運行下一個被加入的動畫。動畫的運行是含有異步過程的,從這點上看,queue的價值是同意一系列函數被異步地調用而不會堵塞程序。jq隊列的實現,並非為了僅僅給動畫使用。由核心功能jQuery.queue/dequeue
和外觀jQuery.fn.queue/dequeue/clearQueue/promise
組成。
queue模型
以下是一個簡單的隊列模型。怎樣實現異步調用呢?在棧出的函數fn中傳入next參數就可以實現,僅僅要函數內調用next(),就可以實現異步調用下一個。
// 入隊
function queue( obj, fn ) {
if ( !obj.cache ) obj.cache = [];
obj.cache.push(fn);
}
// 出隊
function dequeue(obj) {
var next = function() {
dequeue(obj);
}
var fn = obj.cache.shift();
if ( fn ) fn(next);
}
jquery實現
jquery的實現更精密,還考慮了隊列棧出為空後調用鉤子函數銷毀,type參數省略自己主動調整。功能自然是兩套:jQuery.xx/jQuery.fn.xx
,使得$()
包裹元素能夠叠代調用,而且$()調用時type為’fx’時。還將能夠加入時即刻運行。儲存位置都在私有緩存jQuery._data( elem, type )
中。
API詳細功能見以下:
內部使用:(type不存在,則為’fx’,後參數不會前挪)
jQuery.queue( elem, type[, fn] )
:向隊列加入fn,若fn為數組,則重定義隊列。type默認’fx’。這裏不會加入_queueHooks
jQuery.dequeue( elem, type)
:type默認’fx’,棧出隊列開頭並運行。若是為’fx’隊列。一旦被dequeue過。總是給隊列開頭添加有一個”inprogress”,之所以這麽做是為了滿足’fx’動畫隊列首個加入的函數要馬上運行。須要一個標記。
還會添加jQuery._queueHooks
鉤子,dequeue在隊列無函數時調用,會調用鉤子來刪除隊列對象和鉤子本身(極客精神-_-||)
外部使用:(type不為字符串,則為’fx’,且後參數會前挪)
jQuery.fn.queue( type, fn )
:type默認’fx’,對於’fx’隊列,加入第一個fn時默認直接運行(動畫加入即運行的原因,第一個加入的開頭沒有”inprogress”)。其它則無此步驟。此方式加入fn都會給元素們的緩存加上用於自毀的鉤子jQuery._queueHooks( this, type )
jQuery.fn.dequeue( type )
:對每一個元素遍歷使用jQuery.dequeue( this, type )
jQuery.fn.clearQueue( type )
:重置隊列為空數組,type默認’fx’,不正確已綁定的_queuehook產生影響
jQuery.fn.promise( type, obj )
: 返回一個deferred對象的promise對象,帶有jQuery._queueHooks
鉤子的所有元素鉤子均被觸發時,觸發resolve(比方幾個元素動畫全都運行完後運行某操作)
在隊列中函數運行時。會向函數註入elem、next、hooks。通過next能夠讓函數內部調用jQuery.dequeue,hooks能夠讓函數內部調用empty方法直接終止、銷毀隊列。或者綁定銷毀時要運行的邏輯。
[源代碼]
// #4111。建議:內部使用接口
jQuery.extend( {
// 有data為設置,無data為讀取,都返回該隊列
queue: function( elem, type, data ) {
var queue;
if ( elem ) {
type = ( type || "fx" ) + "queue";
queue = jQuery._data( elem, type );
// Speed up dequeue by getting out quickly if this is just a lookup
if ( data ) {
// data為數組,則直接替換掉原緩存值。原本無值,則指定為空數組
if ( !queue || jQuery.isArray( data ) ) {
queue = jQuery._data( elem, type, jQuery.makeArray( data ) );
} else {
// 將函數推入隊列
queue.push( data );
}
}
return queue || [];
}
},
dequeue: function( elem, type ) {
type = type || "fx";
var queue = jQuery.queue( elem, type ),
startLength = queue.length,
fn = queue.shift(),
// 單例加入自毀鉤子empty方法,並取出
hooks = jQuery._queueHooks( elem, type ),
next = function() {
jQuery.dequeue( elem, type );
};
/* 1、棧出、運行 */
// 僅僅適用於‘fx‘隊列。凡被dequeue過,開頭都是"inprogress"。須要再shift()一次
if ( fn === "inprogress" ) {
fn = queue.shift();
startLength--;
}
if ( fn ) {
// ‘fx‘隊列,開頭加"inprogress"。用於表明隊列在運行中。不能馬上運行加入的函數
if ( type === "fx" ) {
queue.unshift( "inprogress" );
}
// 動畫中用到的,先無論
delete hooks.stop;
// 參數註入,可用來在fn內部遞歸dequeue
fn.call( elem, next, hooks );
}
/* 2、銷毀 */
// fn不存在,調用鉤子銷毀隊列和鉤子本身
if ( !startLength && hooks ) {
hooks.empty.fire();
}
},
// 自毀鉤子,隊列無函數時dequeue會觸發。存在元素私有緩存上
_queueHooks: function( elem, type ) {
var key = type + "queueHooks";
return jQuery._data( elem, key ) || jQuery._data( elem, key, {
empty: jQuery.Callbacks( "once memory" ).add( function() {
// 銷毀隊列緩存
jQuery._removeData( elem, type + "queue" );
// 銷毀鉤子本身
jQuery._removeData( elem, key );
} )
} );
}
} );
// #4179,用於外部使用的接口
jQuery.fn.extend( {
queue: function( type, data ) {
var setter = 2;
/* 1、修正 */
// type默認值為‘fx‘
if ( typeof type !== "string" ) {
data = type;
type = "fx";
setter--;
}
/* 2、讀取 */
// 無data表示取值。僅僅取this[ 0 ]相應值
if ( arguments.length < setter ) {
return jQuery.queue( this[ 0 ], type );
}
/* 3、寫入 */
return data === undefined ?
// 無data,返回調用者
this :
this.each( function() {
var queue = jQuery.queue( this, type, data );
// 此方法加入,一定會有hooks
jQuery._queueHooks( this, type );
// ‘fx‘動畫隊列。首次加入函數直接觸發
if ( type === "fx" && queue[ 0 ] !== "inprogress" ) {
jQuery.dequeue( this, type );
}
} );
},
dequeue: function( type ) {
// 遍歷觸發,以支持$(elems).dequeue(type)
return this.each( function() {
jQuery.dequeue( this, type );
} );
},
// 重置隊列為空(‘fx‘隊列也沒有了"inprogress",加入即觸發)
clearQueue: function( type ) {
return this.queue( type || "fx", [] );
},
// 返回promise。調用者元素們所有緩存中的_queueHooks自毀均觸發,才會resolve這個promise
promise: function( type, obj ) {
var tmp,
// 計數。hooks會添加計數值。默認一次。在return前resolve()就會觸發這次。
count = 1,
defer = jQuery.Deferred(),
elements = this,
i = this.length,
// 消減計數,推斷promise是否觸發
resolve = function() {
if ( !( --count ) ) {
defer.resolveWith( elements, [ elements ] );
}
};
// 修正type、data
if ( typeof type !== "string" ) {
obj = type;
type = undefined;
}
type = type || "fx";
while ( i-- ) {
// 凡是elem的type相應緩存中帶有hook鉤子的,都會添加一次計數
tmp = jQuery._data( elements[ i ], type + "queueHooks" );
if ( tmp && tmp.empty ) {
count++;
// 該隊列銷毀時會消減添加的這次計數
tmp.empty.add( resolve );
}
}
resolve();
return defer.promise( obj );
}
} );
jQuery源代碼解析(3)—— ready載入、queue隊列