這裡先分析前置過濾器和請求分發器,類型轉換器下一節再講。
15.4.1 前置過濾器和請求分發器的初始化
前置過濾器和請求分發器在執行時,分別遍歷內部變量prefilters和transports,這兩個變量在jQuery加載完畢後立即初始化,初始化的過程很有意思。
首先,prefilters和transports被置為空對象:
prefilters = {}, // 過濾器
transports = {}, // 分發器
然後,創建jQuery.ajaxPrefilter和jQuery.ajaxTransport,這兩個方法都調用瞭內部函數addToPrefiltersOrTransports,addToPrefiltersOrTransports返回一個匿名閉包函數,這個匿名閉包函數負責將單一前置過濾和單一請求分發器分別放入prefilters和transports。我們知道閉包會保持對它所在環境變量的引用,而jQuery.ajaxPrefilter和jQuery.ajaxTransport的實現又完全一樣,都是對Map結構的對象進行賦值操作,因此這裡利用閉包的特性巧妙的將兩個方法的實現合二為一。函數addToPrefiltersOrTransports可視為模板模式的一種實現。
ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), // 通過閉包保持對prefilters的引用,將前置過濾器添加到prefilters
ajaxTransport: addToPrefiltersOrTransports( transports ), // 通過閉包保持對transports的引用,將請求分發器添加到transports
// 添加全局前置過濾器或請求分發器,過濾器的在發送之前調用,分發器用來區分ajax請求和script標簽請求
function addToPrefiltersOrTransports( structure ) {
// 通過閉包訪問structure
// 之所以能同時支持Prefilters和Transports,關鍵在於structure引用的時哪個對象
// dataTypeExpression is optional and defaults to "*"
// dataTypeExpression是可選參數,默認為*
return function( dataTypeExpression, func ) {
// 修正參數
if ( typeof dataTypeExpression !== "string" ) {
func = dataTypeExpression;
dataTypeExpression = "*";
}
if ( jQuery.isFunction( func ) ) {
var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ), // 用空格分割數據類型表達式dataTypeExpression
i = 0,
length = dataTypes.length,
dataType,
list,
placeBefore;
// For each dataType in the dataTypeExpression
for(; i < length; i++ ) {
dataType = dataTypes[ i ];
// We control if we're asked to add before
// any existing element
// 如果以+開頭,過濾+
placeBefore = /^\+/.test( dataType );
if ( placeBefore ) {
dataType = dataType.substr( 1 ) || "*";
}
list = structure[ dataType ] = structure[ dataType ] || [];
// then we add to the structure accordingly
// 如果以+開頭,則插入開始位置,否則添加到末尾
// 實際上操作的是structure
list[ placeBefore ? "unshift" : "push" ]( func );
}
}
};
}
最後,分別調用jQuery.ajaxPrefilter和jQuery.ajaxTransport填充prefilters和transports.
填充prefilters:
// Detect, normalize options and install callbacks for jsonp requests
// 向前置過濾器對象中添加特定類型的過濾器
// 添加的過濾器將格式化參數,並且為jsonp請求增加callbacks
// MARK:AJAX模塊初始化
jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
var inspectData = s.contentType === "application/x-www-form-urlencoded" &&
( typeof s.data === "string" ); // 如果是表單提交,則需要檢查數據
// 這個方法隻處理jsonp,如果json的url或data有jsonp的特征,會被當成jsonp處理
// 觸發jsonp的3種方式:
if ( s.dataTypes[ 0 ] === "jsonp" || // 如果是jsonp
s.jsonp !== false && ( jsre.test( s.url ) || // 未禁止jsonp,s.url中包含=?& =?$ ??
inspectData && jsre.test( s.data ) ) ) { // s.data中包含=?& =?$ ??
var responseContainer,
jsonpCallback = s.jsonpCallback =
jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback, // s.jsonpCallback時函數,則執行函數用返回值做為回調函數名
previous = window[ jsonpCallback ],
url = s.url,
data = s.data,
// jsre = /(\=)\?(&|$)|\?\?/i; // =?& =?$ ??
replace = "$1" + jsonpCallback + "$2"; // $1 =, $2 &|$
if ( s.jsonp !== false ) {
url = url.replace( jsre, replace ); // 將回調函數名插入url
if ( s.url === url ) { // 如果url沒有變化,則嘗試修改data
if ( inspectData ) {
data = data.replace( jsre, replace ); // 將回調函數名插入data
}
if ( s.data === data ) { // 如果data也沒有變化
// Add callback manually
url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback; // 自動再url後附加回調函數名
}
}
}
// 存儲可能改變過的url、data
s.url = url;
s.data = data;
// Install callback
window[ jsonpCallback ] = function( response ) { // 在window上註冊回調函數
responseContainer = [ response ];
};
// Clean-up function
jqXHR.always(function() {
// Set callback back to previous value
// 將備份的previous函數恢復
window[ jsonpCallback ] = previous;
// Call if it was a function and we have a response
// 響應完成時調用jsonp回調函數,問題是這個函數不是自動執行的麼?
if ( responseContainer && jQuery.isFunction( previous ) ) {
window[ jsonpCallback ]( responseContainer[ 0 ] ); // 為什麼要再次執行previous呢?
}
});
// Use data converter to retrieve json after script execution
s.converters["script json"] = function() {
if ( !responseContainer ) { // 如果
jQuery.error( jsonpCallback + " was not called" );
}
return responseContainer[ 0 ]; // 因為是作為方法的參數傳入,本身就是一個json對象,不需要再做轉換
};
// force json dataType
s.dataTypes[ 0 ] = "json"; // 強制為json
// Delegate to script
return "script"; // jsonp > json
}
});
// Handle cache's special case and global
// 設置script的前置過濾器,script並不一定意思著跨域
// MARK:AJAX模塊初始化
jQuery.ajaxPrefilter( "script", function( s ) {
if ( s.cache === undefined ) { // 如果緩存未設置,則設置false
s.cache = false;
}
if ( s.crossDomain ) { // 跨域未被禁用,強制類型為GET,不觸發全局時間
s.type = "GET";
s.global = false;
}
});
填充transports:
// Bind script tag hack transport
// 綁定script分發器,通過在header中創建script標簽異步載入js,實現過程很簡介
// MARK:AJAX模塊初始化
jQuery.ajaxTransport( "script", function(s) {
// This transport only deals with cross domain requests
if ( s.crossDomain ) { // script可能時json或jsonp,jsonp需要跨域,ajax模塊大約有1/3的代碼時跨域的
// 如果在本域中設置瞭跨域會怎麼處理呢?
var script,
head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; // 充分利用佈爾表達式的計算順序
return {
send: function( _, callback ) { // 提供與同域請求一致的接口
script = document.createElement( "script" ); // 通過創script標簽來實現
script.async = "async";
if ( s.scriptCharset ) {
script.charset = s.scriptCharset; // 字符集
}
script.src = s.url; // 動態載入
// Attach handlers for all browsers
script.onload = script.onreadystatechange = function( _, isAbort ) {
if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
// Handle memory leak in IE
script.onload = script.onreadystatechange = null; // onload事件觸發後,銷毀事件句柄,因為IE內存泄漏?
// Remove the script
if ( head && script.parentNode ) {
head.removeChild( script ); // onloda後,刪除script節點
}
// Dereference the script
script = undefined; // 註銷script變量
// Callback if not abort
if ( !isAbort ) {
callback( 200, "success" ); // 執行回調函數,200為HTTP狀態碼
}
}
};
// Use insertBefore instead of appendChild to circumvent an IE6 bug.
// This arises when a base node is used (#2709 and #4378).
// 用insertBefore代替appendChild,如果IE6的bug
head.insertBefore( script, head.firstChild );
},
abort: function() {
if ( script ) {
script.onload( 0, 1 ); // 手動觸發onload事件,jqXHR狀態碼為0,HTTP狀態碼為1xx
}
}
};
}
});
// Create transport if the browser can provide an xhr
if ( jQuery.support.ajax ) {
// MARK:AJAX模塊初始化
// 普通AJAX請求分發器,dataType默認為*
jQuery.ajaxTransport(function( s ) { // *
// Cross domain only allowed if supported through XMLHttpRequest
// 如果不是跨域請求,或支持身份驗證
if ( !s.crossDomain || jQuery.support.cors ) {
var callback;
return {
send: function( headers, complete ) {
// Get a new xhr
// 創建一個XHR
var xhr = s.xhr(),
handle,
i;
// Open the socket
// Passing null username, generates a login popup on Opera (#2865)
// 調用XHR的open方法
if ( s.username ) {
xhr.open( s.type, s.url, s.async, s.username, s.password ); // 如果需要身份驗證
} else {
xhr.open( s.type, s.url, s.async );
}
// Apply custom fields if provided
// 在XHR上綁定自定義屬性
if ( s.xhrFields ) {
for ( i in s.xhrFields ) {
xhr[ i ] = s.xhrFields[ i ];
}
}
// Override mime type if needed
// 如果有必要的話覆蓋mineType,overrideMimeType並不是一個標準接口,因此需要做特性檢測
if ( s.mimeType && xhr.overrideMimeType ) {
xhr.overrideMimeType( s.mimeType );
}
// X-Requested-With header
// For cross-domain requests, seeing as conditions for a preflight are
// akin to a jigsaw puzzle, we simply never set it to be sure.
// (it can always be set on a per-request basis or even using ajaxSetup)
// For same-domain requests, won't change header if already provided.
// X-Requested-With同樣不是一個標註HTTP頭,主要用於標識Ajax請求.大部分JavaScript框架將這個頭設置為XMLHttpRequest
if ( !s.crossDomain && !headers["X-Requested-With"] ) {
headers[ "X-Requested-With" ] = "XMLHttpRequest";
}
// Need an extra try/catch for cross domain requests in Firefox 3
// 設置請求頭
try {
for ( i in headers ) {
xhr.setRequestHeader( i, headers[ i ] );
}
} catch( _ ) {}
// Do send the request
// This may raise an exception which is actually
// handled in jQuery.ajax (so no try/catch here)
// 調用XHR的send方法
xhr.send( ( s.hasContent && s.data ) || null );
// Listener
// 封裝回調函數
callback = function( _, isAbort ) {
var status,
statusText,
responseHeaders,
responses, // 響應內容,格式為text:text, xml:xml
xml;
// Firefox throws exceptions when accessing properties
// of an xhr when a network error occured
// http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
// 在FF下當網絡異常時,訪問XHR的屬性會拋出異常
try {
// Was never called and is aborted or complete
if ( callback && ( isAbort || xhr.readyState === 4 ) ) { // 4表示響應完成
// Only called once
callback = undefined; // callback隻調用一次,註銷callback
// Do not keep as active anymore
if ( handle ) {
xhr.onreadystatechange = jQuery.noop; // 將onreadystatechange句柄重置為空函數
if ( xhrOnUnloadAbort ) { // 如果是界面退出導致本次請求取消
delete xhrCallbacks[ handle ]; // 註銷句柄
}
}
// If it's an abort
if ( isAbort ) { // 如果是取消本次請求
// Abort it manually if needed
if ( xhr.readyState !== 4 ) {
xhr.abort(); // 調用xhr原生的abort方法
}
} else {
status = xhr.status;
responseHeaders = xhr.getAllResponseHeaders();
responses = {};
xml = xhr.responseXML;
// Construct response list
if ( xml && xml.documentElement /* #4958 */ ) {
responses.xml = xml; // 提取xml
}
responses.text = xhr.responseText; // 提取text
// Firefox throws an exception when accessing
// statusText for faulty cross-domain requests
// FF在跨域請求中訪問statusText會拋出異常
try {
statusText = xhr.statusText;
} catch( e ) {
// We normalize with Webkit giving an empty statusText
statusText = ""; // 像WebKit一樣將statusText置為空字符串
}
// Filter status for non standard behaviors
// If the request is local and we have data: assume a success
// (success with no data won't get notified, that's the best we
// can do given current implementations)
// 過濾不標準的服務器狀態碼
if ( !status && s.isLocal && !s.crossDomain ) {
status = responses.text ? 200 : 404; //
// IE – #1450: sometimes returns 1223 when it should be 204
// 204 No Content
} else if ( status === 1223 ) {
status = 204;
}
}
}
} catch( firefoxAccessException ) {
if ( !isAbort ) {
complete( -1, firefoxAccessException ); // 手動調用回調函數
}
}
// Call complete if needed
// 在回調函數的最後,如果請求完成,立即調用回調函數
if ( responses ) {
complete( status, statusText, responses, responseHeaders );
}
};
// if we're in sync mode or it's in cache
// and has been retrieved directly (IE6 & IE7)
// we need to manually fire the callback
// 同步模式下:同步導致阻塞一致到服務器響應完成,所以這裡可以立即調用callback
if ( !s.async || xhr.readyState === 4 ) {
callback();
} else {
handle = ++xhrId; // 請求計數
// 如果時頁面退出導致本次請求取消,修正在IE下不斷開連接的bug
if ( xhrOnUnloadAbort ) {
// Create the active xhrs callbacks list if needed
// and attach the unload handler
if ( !xhrCallbacks ) {
xhrCallbacks = {};
jQuery( window ).unload( xhrOnUnloadAbort ); // 手動觸發頁面銷毀事件
}
// Add to list of active xhrs callbacks
// 將回調函數存儲在全局變量中,以便在響應完成或頁面退出時能註銷回調函數
xhrCallbacks[ handle ] = callback;
}
xhr.onreadystatechange = callback; // 綁定句柄,這裡和傳統的ajax寫法沒什麼區別
}
},
abort: function() {
if ( callback ) {
callback(0,1); // 1表示調用callback時,isAbort為true,在callback執行過程中能區分出是響應完成還是取消導致的調用
}
}
};
}
});
}
15.4.2 前置過濾器和請求分發器的執行過程
prefilters中的前置過濾器在請求發送之前、設置請求參數的過程中被調用,調用prefilters的是函數inspectPrefiltersOrTransports;巧妙的時,transports中的請求分發器在大部分參數設置完成後,也通過函數inspectPrefiltersOrTransports取到與請求類型匹配的請求分發器:
// some code…
// Apply prefilters
// 應用前置過濾器,參數說明:
inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
// If request was aborted inside a prefiler, stop there
// 如果請求已經結束,直接返回
if ( state === 2 ) {
return false;
}
// some code…
// 註意:從這裡開始要發送瞭
// Get transport
// 請求分發器
transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
// some code…
函數inspectPrefiltersOrTransports從prefilters或transports中取到與數據類型匹配的函數數組,然後遍歷執行,看看它的實現:
// Base inspection function for prefilters and transports
// 執行前置過濾器或獲取請求分發器
function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR,
dataType /* internal */, inspected /* internal */ ) {
dataType = dataType || options.dataTypes[ 0 ];
inspected = inspected || {};
inspected[ dataType ] = true;
var list = structure[ dataType ],
i = 0,
length = list ? list.length : 0,
executeOnly = ( structure === prefilters ),
selection;
for(; i < length && ( executeOnly || !selection ); i++ ) {
selection = list[ i ]( options, originalOptions, jqXHR ); // 遍歷執行
// If we got redirected to another dataType
// we try there if executing only and not done already
if ( typeof selection === "string" ) {
if ( !executeOnly || inspected[ selection ] ) {
selection = undefined;
} else {
options.dataTypes.unshift( selection );
selection = inspectPrefiltersOrTransports(
structure, options, originalOptions, jqXHR, selection, inspected );
}
}
}
// If we're only executing or nothing was selected
// we try the catchall dataType if not done already
if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) {
selection = inspectPrefiltersOrTransports(
structure, options, originalOptions, jqXHR, "*", inspected );
}
// unnecessary when only executing (prefilters)
// but it'll be ignored by the caller in that case
return selection;
}
15.4.3 總結
通過前面的源碼解析,可以將前置過濾器和請求分發器總結如下:
前置過濾器 jQuery.ajaxPrefilter,prefilters
屬性 |
值 |
功能 |
* |
undefined |
不做任何處理,事實上也沒有*屬性 |
json |
[ function ] |
被當作*處理 |
jsonp |
[ function ] |
修正url或data,增加回調函數名 在window上註冊回調函數 註冊script>json數據轉換器 (被當作script處理) |
script |
[ function ] |
設置設置以下參數: 是否緩存 cache、(如果跨域)請求類型 、(如果跨域)是否觸發AJAX全局事件 |
請求分發器 jQuery.ajaxTransport,transports
屬性 |
值 |
功能 |
* |
[ function ] |
返回xhr分發器,分發器帶有send、abort方法 send方法依次調用XMLHTTPRequest的open、send方法,向服務端發送請求,並綁定onreadystatechange事件句柄 |
script |
[ function ] |
返回script分發器,分發器帶有send、abort方法 send方法通過在header中創建script標簽異步載入js,並在script元素上綁定onload、script.onreadystatechange事件句柄 |
作者“知行合一”