由于jQuery ajax對Callbacks、Deferred、serialize、event等模塊的依賴,建議對這些模塊沒有認識的朋友看一下 jQuery Callbacks 、 jQuery Deferred 、 jQuery serialize 、 jQuery event(上) 、 jQuery event(下) 。
這篇文章主要分析的是擁有380+行的jQuery.ajax函數,該函數是jQuery ajax的核心函數,jQuery的其他ajax方法幾乎都是基于該方法的。
上一篇文章我們了解了Baidu ajax(當然是舊版的,還是被簡化的……),那么我們想給這個簡單的ajax方法添加什么功能呢?
?
可鏈式操作
既然是jQuery必然先要解決的是鏈式操作的問題。
jQuery中的Deferred可以實現異步鏈式模型實現,Promise對象可以輕易的綁定成功、失敗、進行中三種狀態的回調函數,然后通過在狀態碼在來回調不同的函數就行了。
?
但僅僅返回一個promise沒什么用
promise只能處理異步,我們需要返回一個更有用的東西。
jqXHR就是這樣的東西,實際上他是一個山寨版的XHR對象。
這樣我們就可以擴展一下XHR對象的功能, 提供一定的容錯處理,暴露給外部提供一些方法。
// 贗品xhr,或者說山寨xhr……╮(╯▽╰)╭ // 為了能夠實現鏈式操作 // 順便擴展一下xhr對象功能 // 還有提供一定的容錯處理 jqXHR = { // 準備狀態 readyState: 0 , // 如果需要,創建一個響應頭參數的表 getResponseHeader: function ( key ) { var match; // 如果狀態為2,狀態2表示ajax完成 if ( state === 2 ) { // 如果沒有相應頭 if ( ! responseHeaders ) { // 相應頭設空 responseHeaders = {}; while ( (match = rheaders.exec( responseHeadersString )) ) { // 組裝相應頭 responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; } } // 響應頭對應的key的值 match = responseHeaders[ key.toLowerCase() ]; } // 返回 return match == null ? null : match; }, // 返回響應頭字符串 getAllResponseHeaders: function () { // 看看是否接收到了,接收到直接返回,否則為null return state === 2 ? responseHeadersString : null ; }, // 設置請求頭 setRequestHeader: function ( name, value ) { var lname = name.toLowerCase(); // 如果state不為0 if ( ! state ) { // 如果requestHeadersNames[ lname ]不為空, // 則requestHeadersNames[ lname ]不變,name設置為該值 // 否則,requestHeadersNames[ lname ]不空, // 則requestHeadersNames[ lname ]設置為name, // 該映射關系用于避免用戶大小寫書寫錯誤之類的問題,容錯處理 name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; // 現在的name是對的,或者是第一次設置這個name,不需要容錯 // 設置請求頭對應值 requestHeaders[ name ] = value; } return this ; }, // 重寫相應頭content-type overrideMimeType: function ( type ) { if ( ! state ) { s.mimeType = type; } return this ; }, // 對應狀態的回調函數集 statusCode: function ( map ) { var code; // 如果map存在,準備組裝 if ( map ) { // 如果狀態小于2,表示舊的回調可能還沒有用到 if ( state < 2 ) { // 遍歷map里面的所有code for ( code in map ) { // 用類似鏈表的方式添加,以保證舊的回調依然存在 statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; } // 狀態大于2,證明已經完成了 } else { // 無論Deferred成功還是失敗都執行當前狀態回調 jqXHR.always( map[ jqXHR.status ] ); } } return this ; }, // 中斷請求 abort: function ( statusText ) { var finalText = statusText || strAbort; // 可以先理解成XHR對象,當然這也不是真正的XHR對象 if ( transport ) { transport.abort( finalText ); } // 調用done,表示干完了 done( 0 , finalText ); return this ; } };
?
可是這還沒有鏈式啊!
怎么把jqXHR變成一個Promise呢?
讓一個對象擁有另一個對象的方法,大家會想到什么呢?
繼承?
不,直接插上去就好了……這里就體現了Javascript“易插拔”的特點……自己亂起的……囧rz……
應該說動態、弱類的特點。
什么意思?就是將Promise上的方法插到jqXHR對象上就行了。
// 在jqXHR粘上promise的所有方法,此時jqXHR就很像一個promise了 // 實際上就是用jQuery.extend(jqXHR, promise)而已 // jqXHR剛山寨了xhr對象,又開始山寨promise對象了 // 順便把jqXHR的complete綁上completeDeferred.add // 意思是jqXHR狀態完成后調用completeDeferred這個Callbacks列隊 // 話說promise里面并沒有complete這個方法 // 后面我們可以看到,作者大人又要給jqXHR插上complete、success、error方法 // Javascript就是這樣簡單,即插即用……囧rz deferred.promise( jqXHR ).complete = completeDeferred.add; // 綁定jqXHR.success為promise里面的done方法 jqXHR.success = jqXHR.done; // 綁定jqXHR.error為promise里面的fail方法 jqXHR.error = jqXHR.fail;
這樣子直接插上去在強類語言中是做不到的,當然也可以用組合模式來實現一個對象擁有多個對象的方法,但是卻無法動態的去給對象添加各種方法。
強類語言會限制對象一輩子只能“會”那么幾種“方法”,Javascript的對象可以在生命周期內隨意“學習”各種“方法”。
所以說,強類語言和Javascript的對象模型是有差別的,將設計模式生搬硬套進Javascript或許是錯誤的。
我們再舉個例子,比如如果用所謂的Javascript組合模式來重寫上面的代碼可能會是這樣的:
// 定義局部變量deferred和completeDeferred function JQXHR(){ // 定義jqXHR的屬性和方法 for (i in deferred.promise){ this [i] = this .promise[i]; } this .complete = completeDeferred.add; this .success = this .done; this .error = this .done } var jqXHR = new JQXHR();大家覺得哪個好呢?
變成上面的樣子主要是因為要解決下面兩個問題:
- jqXHR由于是暴露在外的,他不能包含deferred和completeDeferred,不允許用戶在外部操作這兩個對象的狀態。
- deferred和completeDeferred也不能成為jqXHR的私有變量,因為還要更具ajax事件觸發。
?
提供全局事件,使得UI可以根據ajax狀態進行改變
這里主要利用了jQuery.event.trigger和jQuery.fn.trigger模擬發事件。
// 如果需要,而且全局事件沒有被觸發過 if ( fireGlobals && jQuery.active++ === 0 ) { // 則通過jQuery.event.trigger模擬觸發 jQuery.event.trigger("ajaxStart" ); }
當ajax開始時模擬全局事件,ajaxStart。
// 如果需要,對特定對象觸發全局事件ajaxSend if ( fireGlobals ) { globalEventContext.trigger( "ajaxSend" , [ jqXHR, s ] ); }
ajax發送消息,觸發ajaxSend。
// 如果需要觸發全局事件 if ( fireGlobals ) { // 對指定元素觸發事件ajaxSuccess或者ajaxError globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError" , [ jqXHR, s, isSuccess ? success : error ] ); } // 回調完成后的Callbacks隊列 completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); // 如果需要觸發全局事件 if ( fireGlobals ) { // 對指定元素觸發事件ajaxComplete globalEventContext.trigger( "ajaxComplete" , [ jqXHR, s ] ); // 該ajax觸發完畢,標記active減1,如果為0,證明所有ajax結束 if ( !( -- jQuery.active ) ) { // 觸發ajaxStop事件 jQuery.event.trigger("ajaxStop" ); } }
結束時候觸發ajaxSuccess或ajaxError,再出發ajaxComplete,如果全部ajax結束則觸發ajaxStop。
?
?
是否允許使用緩存數據
// 如果不需要content // 看看需不需要加其他信息 if ( ! s.hasContent ) { // 如果data存在,那么已經序列化 if ( s.data ) { // 添加到cacheURL中 cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + s.data ); // 刪除掉則后面就不會被發送出去了 delete s.data; } // 看看是否需要避免數據從緩存中讀取 if ( s.cache === false ) { s.url = rts.test( cacheURL ) ? // 如果已經有_參數,那么設置他的值 cacheURL.replace( rts, "$1_=" + ajax_nonce++ ) : // 否則添加一個_ = xxx在URL后面 cacheURL + ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ajax_nonce++ ; } }
通過給地址附加參數_=xxx來避免緩存。
// 看看需不需要設置If-Modified-Since和If-None-Match頭信息 if ( s.ifModified ) { if ( jQuery.lastModified[ cacheURL ] ) { jqXHR.setRequestHeader( "If-Modified-Since" , jQuery.lastModified[ cacheURL ] ); } if ( jQuery.etag[ cacheURL ] ) { jqXHR.setRequestHeader( "If-None-Match" , jQuery.etag[ cacheURL ] ); } }
以及設置If-Modified-Since和If-None-Match頭信息。
// 看看是否需要緩存置If-Modified-Since和If-None-Match頭 if ( s.ifModified ) { // 取得Last-Modified modified = jqXHR.getResponseHeader("Last-Modified" ); // 如果Last-Modified存在 if ( modified ) { // 在jQuery.lastModified[cacheURL]保存Last-Modified jQuery.lastModified[ cacheURL ] = modified; } // 取得etag modified = jqXHR.getResponseHeader("etag" ); // 如果etag存在 if ( modified ) { // 在jQuery.etag[cacheURL]緩存etag jQuery.etag[ cacheURL ] = modified; } }
緩存 If-Modified-Since和If-None-Match頭信息。
?
設置超時
// 如果是異步,并且設置了超時 if ( s.async && s.timeout > 0 ) { // 設置超時 timeoutTimer = setTimeout( function () { jqXHR.abort( "timeout" ); }, s.timeout ); }
主要功能分析完了,完整備注代碼見下。?
?
完整備注
jQuery.ajax = function ( url, options ) { // 如果url是一個obj,模擬1.5版本以前的方法 if ( typeof url === "object" ) { options = url; url = undefined; } // 設置options options = options || {}; var transport, // 緩存cacheURL cacheURL, // 響應頭 responseHeadersString, responseHeaders, // 超時控制器 timeoutTimer, // 跨域判定變量 parts, // 是否需要觸發全局事件 fireGlobals, // 循環變量 i, // 通過jQuery.ajaxSetup改造參數對象 s = jQuery.ajaxSetup( {}, options ), // 回調指定上下文,也就是他的this callbackContext = s.context || s, // 全局事件中的相應函數的指定上下文 // 有s.context,且是DOM節點,或者jQuery收集器 globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ? // 通過jQuery包裝 jQuery( callbackContext ) : // 否則為jQuery.event jQuery.event, // 新建一個deferred deferred = jQuery.Deferred(), // deferred完成后的Callbacks隊列 completeDeferred = jQuery.Callbacks("once memory" ), // 對應狀態的回調函數集 statusCode = s.statusCode || {}, // 請求頭 requestHeaders = {}, requestHeadersNames = {}, // 包裝類jqXHR的狀態 state = 0 , // 默認中斷消息 strAbort = "canceled" , // 贗品xhr,或者說山寨xhr……╮(╯▽╰)╭ // 為了能夠實現鏈式操作 // 順便擴展一下xhr對象功能 // 還有提供一定的容錯處理 jqXHR = { // 準備狀態 readyState: 0 , // 如果需要,創建一個響應頭參數的表 getResponseHeader: function ( key ) { var match; // 如果狀態為2,狀態2表示ajax完成 if ( state === 2 ) { // 如果沒有相應頭 if ( ! responseHeaders ) { // 相應頭設空 responseHeaders = {}; while ( (match = rheaders.exec( responseHeadersString )) ) { // 組裝相應頭 responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; } } // 響應頭對應的key的值 match = responseHeaders[ key.toLowerCase() ]; } // 返回 return match == null ? null : match; }, // 返回響應頭字符串 getAllResponseHeaders: function () { // 看看是否接收到了,接收到直接返回,否則為null return state === 2 ? responseHeadersString : null ; }, // 緩存請求頭 setRequestHeader: function ( name, value ) { var lname = name.toLowerCase(); // 如果state不為0 if ( ! state ) { // 如果requestHeadersNames[ lname ]不為空, // 則requestHeadersNames[ lname ]不變,name設置為該值 // 否則,requestHeadersNames[ lname ]不空, // 則requestHeadersNames[ lname ]設置為name, // 該映射關系用于避免用戶大小寫書寫錯誤之類的問題,容錯處理 name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; // 現在的name是對的,或者是第一次設置這個name,不需要容錯 // 設置請求頭對應值 requestHeaders[ name ] = value; } return this ; }, // 重寫相應頭content-type overrideMimeType: function ( type ) { if ( ! state ) { s.mimeType = type; } return this ; }, // 對應狀態的回調函數集 statusCode: function ( map ) { var code; // 如果map存在,準備組裝 if ( map ) { // 如果狀態小于2,表示舊的回調可能還沒有用到 if ( state < 2 ) { // 遍歷map里面的所有code for ( code in map ) { // 用類似鏈表的方式添加,以保證舊的回調依然存在 statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; } // 狀態大于2,證明已經完成了 } else { // 無論Deferred成功還是失敗都執行當前狀態回調 jqXHR.always( map[ jqXHR.status ] ); } } return this ; }, // 中斷請求 abort: function ( statusText ) { var finalText = statusText || strAbort; // 可以先理解成XHR對象,當然這也不是真正的XHR對象 if ( transport ) { transport.abort( finalText ); } // 調用done,表示干完了 done( 0 , finalText ); return this ; } }; // 在jqXHR粘上promise的所有方法,此時jqXHR就很像一個promise了 // 實際上就是用jQuery.extend(jqXHR, promise)而已 // jqXHR剛山寨了xhr對象,又開始山寨promise對象了 // 順便把jqXHR的complete綁上completeDeferred.add // 意思是jqXHR狀態完成后調用completeDeferred這個Callbacks列隊 // 話說promise里面并沒有complete這個方法 // 后面我們可以看到,作者大人又要給jqXHR插上complete、success、error方法 // Javascript就是這樣簡單,即插即用……囧rz deferred.promise( jqXHR ).complete = completeDeferred.add; // 綁定jqXHR.success為promise里面的done方法 jqXHR.success = jqXHR.done; // 綁定jqXHR.error為promise里面的fail方法 jqXHR.error = jqXHR.fail; // 確定url參數,否則用當前地址。將地址的#號后面的所有東西去掉 // 比如http://127.0.0.1#main,去掉這個#main // 如果開頭是//,及數據傳輸協議沒有,那么用當前頁面的數據傳輸協議替換 // 比如//127.0.0.1,變成http://127.0.0.1 s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "http://" ); // 定義type,向后兼容 s.type = options.method || options.type || s.method || s.type; // 取出數據類型列表 // 沒有則為"*", // 有則認為是用空格分隔的字符串,將其變成數組 s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( core_rnotwhite ) || ["" ]; // 當協議、主機、端口和當前不匹配時,證明這是一個跨域請求 if ( s.crossDomain == null ) { // 分隔當前url parts = rurl.exec( s.url.toLowerCase() ); // 判定是否是跨域 s.crossDomain = !!( parts && ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] || ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) != ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) ) ); } // 如果data已經是一個字符串了,那么就不用轉換了 if ( s.data && s.processData && typeof s.data !== "string" ) { // 序列化 s.data = jQuery.param( s.data, s.traditional ); } // 預過濾 inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); // 如果請求被prefilter終止,則退出 if ( state === 2 ) { return jqXHR; } // 看看需不需要觸發全局事件 fireGlobals = s.global; // 如果需要,而且全局事件沒有被觸發過 if ( fireGlobals && jQuery.active++ === 0 ) { // 則通過jQuery.event.trigger模擬觸發 jQuery.event.trigger("ajaxStart" ); } // 將類型大寫 s.type = s.type.toUpperCase(); // 判斷需不需要設置content s.hasContent = ! rnoContent.test( s.type ); // 緩存URL,用來在之后設置If-Modified-Since和If-None-Match cacheURL = s.url; // 如果不需要content // 看看需不需要加其他信息 if ( ! s.hasContent ) { // 如果data存在,那么已經序列化 if ( s.data ) { // 添加到cacheURL中 cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + s.data ); // 刪除掉則后面就不會被發送出去了 delete s.data; } // 看看是否需要避免數據從緩存中讀取 if ( s.cache === false ) { s.url = rts.test( cacheURL ) ? // 如果已經有_參數,那么設置他的值 cacheURL.replace( rts, "$1_=" + ajax_nonce++ ) : // 否則添加一個_ = xxx在URL后面 cacheURL + ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ajax_nonce++ ; } } // 看看需不需要設置If-Modified-Since和If-None-Match頭信息 if ( s.ifModified ) { if ( jQuery.lastModified[ cacheURL ] ) { jqXHR.setRequestHeader( "If-Modified-Since" , jQuery.lastModified[ cacheURL ] ); } if ( jQuery.etag[ cacheURL ] ) { jqXHR.setRequestHeader( "If-None-Match" , jQuery.etag[ cacheURL ] ); } } // 如果數據需要被發送,設置正確的頭 if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { jqXHR.setRequestHeader( "Content-Type" , s.contentType ); } // 根據dataType,設置一個Accept頭 jqXHR.setRequestHeader( "Accept" , s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? s.accepts[ s.dataTypes[ 0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : s.accepts[ "*" ] }; // 遍歷s.headers,將其內參數設置入請求頭 for ( i in s.headers ) { jqXHR.setRequestHeader( i, s.headers[ i ] ); } // 通過beforeSend檢查是否需要發送 if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { // 終止 return jqXHR.abort(); } // 此時abort函數不是取消ajax,而是中斷了ajax strAbort = "abort" ; // 在jqXHR上綁定成功、錯誤、完成回調函數 for ( i in { success: 1, error: 1, complete: 1 } ) { jqXHR[ i ]( s[ i ] ); } // 得到transport transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); // 如果沒有,自動終止 if ( ! transport ) { done( -1, "No Transport" ); } else { // 否則,設置reayState為1 jqXHR.readyState = 1 ; // 如果需要,對特定對象觸發全局事件ajaxSend if ( fireGlobals ) { globalEventContext.trigger( "ajaxSend" , [ jqXHR, s ] ); } // 如果是異步,并且設置了超時 if ( s.async && s.timeout > 0 ) { // 設置超時 timeoutTimer = setTimeout( function () { jqXHR.abort( "timeout" ); }, s.timeout ); } try { // 設置state為1 state = 1 ; // 開始發送 transport.send( requestHeaders, done ); } catch ( e ) { // 截獲錯誤,如果ajax未完成 if ( state < 2 ) { done( -1 , e ); // 完成了就直接拋出錯誤 } else { throw e; } } } // 完成時的回調函數 function done( status, nativeStatusText, responses, headers ) { var isSuccess, success, error, response, modified, statusText = nativeStatusText; // 如果已經調用過該函數,直接退出 if ( state === 2 ) { return ; } // 設置現在狀態已完成 state = 2 ; // 清除超時設置 if ( timeoutTimer ) { clearTimeout( timeoutTimer ); } // 不管jqXHR對象要被用到何時, // 釋放transport的引用使得他可以先被垃圾回收 transport = undefined; // 緩存響應頭 responseHeadersString = headers || "" ; // 設置readyState jqXHR.readyState = status > 0 ? 4 : 0 ; // 得到響應數據 if ( responses ) { // 通過ajaxHandleResponses處理數據 response = ajaxHandleResponses( s, jqXHR, responses ); } // If successful, handle type chaining // 如果成功 if ( status >= 200 && status < 300 || status === 304 ) { // 看看是否需要緩存If-Modified-Since和If-None-Match頭 if ( s.ifModified ) { // 取得Last-Modified modified = jqXHR.getResponseHeader("Last-Modified" ); // 如果Last-Modified存在 if ( modified ) { // 在jQuery.lastModified[cacheURL]保存Last-Modified jQuery.lastModified[ cacheURL ] = modified; } // 取得etag modified = jqXHR.getResponseHeader("etag" ); // 如果etag存在 if ( modified ) { // 在jQuery.etag[cacheURL]緩存etag jQuery.etag[ cacheURL ] = modified; } } // 如果沒有修改 if ( status === 304 ) { // 設置成功 isSuccess = true ; // 設置狀態為notmodified statusText = "notmodified" ; // 否則得到必要的數據 } else { isSuccess = ajaxConvert( s, response ); statusText = isSuccess.state; success = isSuccess.data; error = isSuccess.error; isSuccess = ! error; } // 如果失敗 } else { // 從statusText獲取error狀態 // 在設置statusText成"error" // 并改變status為0 error = statusText; if ( status || ! statusText ) { statusText = "error" ; if ( status < 0 ) { status = 0 ; } } } // 開始設置山寨xhr對象jqXHR的status和statusText jqXHR.status = status; jqXHR.statusText = ( nativeStatusText || statusText ) + "" ; // 根據成功還是失敗,對deferred進行回調 if ( isSuccess ) { deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); } else { deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); } // 根據目前statusCode回調 jqXHR.statusCode( statusCode ); statusCode = undefined; // 如果需要觸發全局事件 if ( fireGlobals ) { // 對指定元素觸發事件ajaxSuccess或者ajaxError globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError" , [ jqXHR, s, isSuccess ? success : error ] ); } // 回調完成后的Callbacks隊列 completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); // 如果需要觸發全局事件 if ( fireGlobals ) { // 對指定元素觸發事件ajaxComplete globalEventContext.trigger( "ajaxComplete" , [ jqXHR, s ] ); // 該ajax觸發完畢,標記active減1,如果為0,證明所有ajax結束 if ( !( -- jQuery.active ) ) { // 觸發ajaxStop事件 jQuery.event.trigger("ajaxStop" ); } } } // 返回山寨xhr return jqXHR; };
?
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
