jQuery-1.9.1源碼分析系列(十六)ajax——ajax處理流程以及核心函數


  先來看一看jQuery的ajax核心處理流程($.ajax)

a. ajax( [url,] options )執行流程


  第一步,為傳遞的參數做適配。url可以包含在options中

//傳遞的參數只是一個對象
if ( typeof url === "object" ) {
    options = url;
    url = undefined;
}

//options強制轉成對象
options = options || {};

  第二步,創建一些變量,比較重要的是:創建最終選項對象s、全局事件上下文是callbackContext、創建deferred和completeDeferred、創建jqXHR對象。

var //跨域檢測變量
    parts,
    ...
    //創建最終選項對象
    s = jQuery.ajaxSetup( {}, options ),
    //回調上下文
    callbackContext = s.context || s,
    //全局事件上下文是callbackContext,如果他是一個DOM節點或jQuery集合(對象)
    globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?
    jQuery( callbackContext ) :
    jQuery.event,
    // Deferreds
    deferred = jQuery.Deferred(),
    completeDeferred = jQuery.Callbacks("once memory"),
    ...
    jqXHR = {
        readyState: 0,
        //建立請求頭哈希表
        getResponseHeader: function( key ) {...},
        // Raw string
        getAllResponseHeaders: function() {...},
        //緩存請求頭
        setRequestHeader: function( name, value ) {...},
        //重寫響應content-type頭
        overrideMimeType: function( type ) {...},
        //取決於狀態的回調
        statusCode: function( map ) {...},
        //取消請求
        abort: function( statusText ) {...}
    };

//添加延時事件
deferred.promise( jqXHR ).complete = completeDeferred.add; jqXHR.success = jqXHR.done; jqXHR.error = jqXHR.fail;

  第三步,檢查是否跨域。其中需要注意的是ajaxLocParts在jQuery初始化的時候就定義了

//rurl = /^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/
//需要注意的是本地文件一般形如"file:///C:/Users/Administrator/Desktop/jquery/test.html"
//最終結果為["file://", "file:", "", undefined]
//正常http請求如"http://www.baidu.com"
//的到結果為["http://www.baidu.com", "http:", "www.baidu.com", undefined]
//如果是"http://192.168.0.17:8080/baidu/com"
//則得到的結果["http://192.168.0.17:8080", "http:", "192.168.0.17", "8080"]
ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
//跨域請求是為了當我們有一個協議:host:port不匹配的時候
if ( s.crossDomain == null ) {
    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轉化成一個查詢字符串

//processData默認為true
//默認情況下,通過data屬性傳遞進來的數據,如果是一個對象(技術上講,只要不是字符串),
//都會處理轉化成一個查詢字符串,以配合默認內容類型 "application/x-www-form-urlencoded"
if ( s.data && s.processData && typeof s.data !== "string" ) {
    s.data = jQuery.param( s.data, s.traditional );
}

  第五步,運行prefilters進行預處理

//運行prefilters
inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );

  預處理和分發器使用的是同一個函數inspectPrefiltersOrTransports,需要注意的是當dataType為jsonp的時候是以dataType為script的方式來處理的

  第六步,根據傳遞的選項設置默認參數處理(主要包括如果type是GET等類型,傳遞的數據將被附加到URL上;添加請求頭如If-Modified-Since/If-None-Match、Content-Type、Accept等;)

//沒有請求內容(type一般為GET的情況)
if ( !s.hasContent ) {

    //如果data可用,添加到url
    if ( s.data ) {
        cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
        // #9682:刪除data保證重試是不會被使用
        delete s.data;
    }

    //cache默認值:true(dataType為'script'或'jsonp'時,則默認為false)。
    //指示是否緩存URL請求。如果設為false將強制瀏覽器不緩存當前URL請求。
    //該參數只對HEAD、GET請求有效(POST請求本身就不會緩存)
    if ( s.cache === false ) {
        //rts = /([?&])_=[^&]*/
        s.url = rts.test( cacheURL ) ?

            //如果已經有一個'_'參數,設置他的值
            cacheURL.replace( rts, "$1_=" + ajax_nonce++ ) :

            //否則添加到url后面
            //ajax_rquery = /\?/
            cacheURL + ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ajax_nonce++;
    }
}

// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
//ifModified默認為false
//允許當前請求僅在服務器數據改變時獲取新數據(如未更改,瀏覽器從緩存中獲取數據)
//它使用HTTP頭信息Last-Modified來判斷。從jQuery 1.4開始,他也會檢查服務器指定的'etag'來確定數據是否已被修改。
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 ] );
    }
}

//contentType默認值:'application/x-www-form-urlencoded; charset=UTF-8'。
//使用指定的內容編碼類型將數據發送給服務器。
//W3C的XMLHttpRequest規范規定charset始終是UTF-8,將其改也無法強制瀏覽器更改字符編碼。
if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
    jqXHR.setRequestHeader( "Content-Type", s.contentType );
}

//設置Accept頭,依賴於dataType
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[ "*" ]
);

// Check for headers option
//headers默認值:{}。
//以對象形式指定附加的請求頭信息。請求頭X-Requested-With: XMLHttpRequest將始終被添加,
//當然你也可以在此處修改默認的XMLHttpRequest值。
//headers中的值可以覆蓋beforeSend回調函數中設置的請求頭(意即beforeSend先被調用)。
for ( i in s.headers ) {
    jqXHR.setRequestHeader( i, s.headers[ i ] );
}
…
//安裝回調到deferreds上
for ( i in { success: 1, error: 1, complete: 1 } ) {
    jqXHR[ i ]( s[ i ] );
}
View Code

  第七步,執行請求分發

//執行請求分發
transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );

  第八步,發送請求,附加上回調處理done,以及異常處理。done函數里面處理ajax請求完成(成功或失敗)后的處理。

//如果沒有transport,自動中止
if ( !transport ) {
    done( -1, "No Transport" );
} else {
    jqXHR.readyState = 1;

    //觸發ajaxSend事件
    ...
    //請求允許時長限時處理
//發送ajax請求
    try {
        state = 1;
        transport.send( requestHeaders, done );
    } catch ( e ) {
        // 傳播異常的錯誤,如果沒有成功
        if ( state < 2 ) {
            done( -1, e );
        //其他情況簡單的處理
        } else {
            throw e;
        }
    }

  接下來是請求返回之后的處理,全部在done函數中完成:包括更新一些全局的狀態、調用ajaxHandleResponses解析響應數據、針對響應返回的狀態碼自動判斷執行那些Deferred延時以及觸發哪些全局事件。把done函數的主要源碼貼一下

//ajax完成后的回調
function done( status, nativeStatusText, responses, headers ) {
    ...
    //狀態改為"done"
    state = 2;

    //清除timeout
    if ( timeoutTimer ) {
        clearTimeout( timeoutTimer );
    }
    ...
    //獲取響應數據
    if ( responses ) {
        response = ajaxHandleResponses( s, jqXHR, responses );
    }
    ...
    //如果成功,處理之
    if ( status >= 200 && status < 300 || status === 304 ) {

        //在ifModified模式下設置 If-Modified-Since and/or If-None-Match header
        if ( s.ifModified ) {...}

        //沒有新文檔
        if ( status === 204 ) {
            ...

        //客戶端有緩沖的文檔並發出了一個條件性的請求
        //(一般是提供If-Modified-Since頭表示客戶只想比指定日期更新的文檔)。
        //服務器告訴客戶,原來緩沖的文檔還可以繼續使用。
        } else if ( status === 304 ) {
            ...

        //如果有數據,我們轉換他
        } else {
            isSuccess = ajaxConvert( s, response );
            statusText = isSuccess.state;
            success = isSuccess.data;
            error = isSuccess.error;
            isSuccess = !error;
        }

    //如果出錯
    } else {
        //我們從狀態文本提取錯誤,然后正常化狀態文本和狀態給沒有中止的請求
        error = statusText;
        if ( status || !statusText ) {
            statusText = "error";
            if ( status < 0 ) {
                status = 0;
            }
        }
    }

    //為jqXHR對象設置數據
    jqXHR.status = status;
    jqXHR.statusText = ( nativeStatusText || statusText ) + "";

    //Deferred執行和全局事件觸發處理
    ...
}

  到此,整個流程完結。

 

b. 預處理prefilters和請求分發trasports結構


  預處理prefilters和請求分發trasports的初始化都是調用addToPrefiltersOrTransports返回一個包裝函數,然后調用這個包裝函數給prefilters和transports添加屬性。

jQuery.extend({
  ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
  ajaxTransport: addToPrefiltersOrTransports( transports ),
}

// jQuery.ajaxPrefilter和jQuery.ajaxTransport基礎構造函數構造函數
function addToPrefiltersOrTransports( structure ) {

    // dataTypeExpression is optional and defaults to "*"
    return function( dataTypeExpression, func ) {...};
}

  舉一個初始化預處理prefilters的例子

    jQuery.ajaxPrefilter( "script", function( s ) {
        if ( s.cache === undefined ) {
            s.cache = false;
        }
        if ( s.crossDomain ) {
            s.type = "GET";
            s.global = false;
        }
    });

  其他的初始化都是類似,最終預處理prefilters初始化完成以后的結果是

  

  而分發器初始化完成后的結果是

  

  預處理和分發器使用的是同一個函數inspectPrefiltersOrTransports來觸發,需要注意的是當dataType為jsonp的時候是以dataType為script的方式來處理的。

// 基本功能用於預處理過濾器和分發器
//structure對應的是prefilters和transports
/*
prefilters = {
    script: [function(s){...}],
    json:   [function(s, originalSettings, jqXHR){...}],
    jsonp:  [function(s, originalSettings, jqXHR){...}]
}

transports = {
    *:      [function(s){...}],
    script: [function(s){...}]
}
*/
function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
  var inspected = {},
  seekingTransport = ( structure === transports );

  function inspect( dataType ) {
    var selected;
    inspected[ dataType ] = true;
    //structure[ dataType ]獲取置頂的處理函數數組(目前數組長度都是1)
    jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
      //執行預處理或分發函數
      var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );

      //“jsonp”的預處理進入該分支(dataTypeOrTransport為“script”),jsonp最終以datatype為“script”的方式來處理
      if( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {         options.dataTypes.unshift( dataTypeOrTransport );         //dataTypeOrTransport為"script",執行script的預處理
        inspect( dataTypeOrTransport );         return false;
      } else if ( seekingTransport ) {
        return !( selected = dataTypeOrTransport );
      }
    });
    return selected;
  }
  return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
}

 

c. 預處理prefilters詳細分析


  預處理有三種:json/jsonp/script

  

  首先我們需要明白為什么需要進行預處理。

  在dataType為"script"的情況下,我們需要強制處理緩存的特殊情況;如果是跨域則需要強制類型為GET,並禁止觸發全局事件。這個處理的源碼如下

jQuery.ajaxPrefilter( "script", function( s ) {
    if ( s.cache === undefined ) {
        s.cache = false;
    }
    //注意:在遠程請求時(不在同一個域下),所有POST請求都將轉為GET請求。(因為將使用DOM的script標簽來加載)
    if ( s.crossDomain ) {
        s.type = "GET";
        s.global = false;
    }
});

  在dataType為"json"的情況下實際上是什么都沒有做。

  另一個需要預處理的是當dataType為jsonp的情況。 jsonp下情況比較特殊,jsonp的原理詳見

  jsonp原理頁面也有jQuery處理的分析。這里就簡單介紹了。處理步驟如下(jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR )的源碼

  首先我們需要設置回調函數名稱,可以自己定義也可以讓jQuery自動設置。

//獲取回調函數名稱(這個名稱可以在ajax的jsonpCallback選項上設置,
//否則通過jQuery默認的方式jsonpCallback()來設置)
//這個回調函數名稱是用來告訴后台需要將返回數據包裹到該函數中,返回前端后執行
callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
s.jsonpCallback() :
s.jsonpCallback;

  然后將回調插入到url或者data選項中(一般來說是URL

if ( jsonProp ) {
    s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
} else if ( s.jsonp !== false ) {
    s.url += ( ajax_rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
}

  再然后,安裝回調。

//安裝回調
overwritten = window[ callbackName ];
window[ callbackName ] = function() {
        responseContainer = arguments;
};

  很明顯,本來后台應該執行overwritten這個回調的,但是現在換成了執行后面重寫的這個回調。別急overwritten后面有掉用到。 

  在安裝回調之前還有一個步驟:添加"script json"轉換器

//使用數據轉換器,腳本執行后取回JSON
s.converters["script json"] = function() {
    if ( !responseContainer ) {
        jQuery.error( callbackName + " was not called" );
    }
    return responseContainer[ 0 ];
};

  我們可能疑惑responseContainer是怎么來的?看到了安裝回調了么,responseContainer就是后台調用了window[ callbackName ]這個回調以后獲取到的。

  最后,添加延時對象的always響應執行overwritten。

//清除函數(轉換完成后執行)
jqXHR.always(function() {
    //保存先前存的值
    window[ callbackName ] = overwritten;

    //將jsonpCallback設置回原始值
    if ( s[ callbackName ] ) {
        //確保重新使用jsonpCallback選項沒有雜質
        s.jsonpCallback = originalSettings.jsonpCallback;

        //將callbackName壓入oldCallbacks以備將來使用
        oldCallbacks.push( callbackName );
    }

    //在請求響應后,如果jsonpCallback指定的回調是一個函數則調用它
    if ( responseContainer && jQuery.isFunction( overwritten ) ) {
        overwritten( responseContainer[ 0 ] );
    }

    responseContainer = overwritten = undefined;
});

  所以overwritten是在這里面執行的,前面源碼中重載過的那個回調在后台調用后獲取到了responseContainer值也被用到了。可見我們在設置的jsonpCallback選項指定的回調名(例如chuaRemote)對應的回調先被保存到overwritten中,而這個原始的chuaRemote被賦值為function() {  responseContainer = arguments;};

  在響應處理中chuaRemote會被調用讓responseContainer獲取到響應值。最后會執行到jqXHR.always添加的函數處理。將chuaRemote恢復到原來的函數overwritten,並執行overwritten(jsonpCallback指定的回調)。主要是always這個監聽處理中還清除callbackName指定的函數,以及添加回到歷史等等處理

 

d. 分發器ajaxTransport


  分發器干啥的?前面不是說了jQuery的ajax處理方式有兩種么,一種直接使用瀏覽器的ajax接口處理,另一種是使用script的src來處理。分發器就是將這兩中情況分發給他們兩者的專用處理器來處理。

  分發器ajaxTransport在jQuery初始化完成后得到了分發處理的兩種類型

  

  兩種類型中除開跨域使用script指定的方式外,都使用*指定的方式。

 

  我們先看一下script方式的分發器如下

jQuery.ajaxTransport( "script", function(s) {
  // 僅用於跨域
  if ( s.crossDomain ) {
    var script,
    head = document.head || jQuery("head")[0] || document.documentElement;
    return {
      send: function( _, callback ) {
        script = document.createElement("script");
        script.async = true;
        if ( s.scriptCharset ) {
          script.charset = s.scriptCharset;
        }
        script.src = s.url;
        //添加事件處理
        script.onload = script.onreadystatechange = function( _, isAbort ) {…};
        //使用本地DOM操作,以避免我們的domManip AJAX掛羊頭賣狗肉
        head.insertBefore( script, head.firstChild );
      },

      abort: function() {
        if ( script ) {
          script.onload( undefined, true );
        }
      }
    };
  }
});

  可見跨域請求使用動態加載script標簽的方式來完成,所有的參數都附加到url上。dataType為jsonp也是使用該方式

  需要注意一點的是判斷script標簽加載完成的回調處理

script.onload = script.onreadystatechange = function( _, isAbort ) {
    //這種寫法的取巧之處在於onload和onreadystatechage都用同一個函數,
    //Firefox/Safari/Chrome/Opera中不支持onreadystatechage事件,也沒有readyState屬性,
    //所以 !this.readyState 是針對這些瀏覽器。readyState是針對IE瀏覽器,載入完畢的情況是loaded,
    //緩存的情況下可能會出現readyState為complete。所以兩個不能少。
    //但由於IE9/10也已經支持onload事件了,會造成callback執行2次。
    //所以執行一次以后設置了script.onload = script.onreadystatechange = null;
    if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
        //處理IE內存
        script.onload = script.onreadystatechange = null;

        //移除script節點
        if ( script.parentNode ) {
            script.parentNode.removeChild( script );
        }

        //script內存清空
        script = null;

        // Callback if not abort
        if ( !isAbort ) {
            callback( 200, "success" );
        }
    }
};

  

  接下來我們看一下普通類型的ajax請求分發是如何處理的

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
                var handle, i,
                xhr = s.xhr();
                //打開socket
                //傳遞空username,Opera產生一個登陸彈出框(#2865)
                if ( s.username ) {
                    xhr.open( s.type, s.url, s.async, s.username, s.password );
                } else {
                    xhr.open( s.type, s.url, s.async );
                }

                //如果提供應用自定義字段
                if ( s.xhrFields ) {
                    for ( i in s.xhrFields ) {
                        xhr[ i ] = s.xhrFields[ i ];
                    }
                }

                // 重寫mime類型,如果需要的話
                if ( s.mimeType && xhr.overrideMimeType ) {
                    xhr.overrideMimeType( s.mimeType );
                }

                // X-Requested-With頭
                if ( !s.crossDomain && !headers["X-Requested-With"] ) {
                    headers["X-Requested-With"] = "XMLHttpRequest";
                }

                // 需要extra try/catch對跨域請求(Firefox 3中)
                try {
                    for ( i in headers ) {
                        xhr.setRequestHeader( i, headers[ i ] );
                    }
                } catch( err ) {}

                //發送請求
                //在jQuery.ajax中有try/catch處理
                xhr.send( ( s.hasContent && s.data ) || null );

                // Listener
                callback = function( _, isAbort ) {...};

                if ( !s.async ) {
                    // if we're in sync mode we fire the callback
                    callback();
                } else if ( xhr.readyState === 4 ) {
                    // (IE6 & IE7) if it's in cache and has been
                    // retrieved directly we need to fire the callback
                    setTimeout( callback );
                } else {
                    handle = ++xhrId;
                    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;
                }
            },

            abort: function() {
                if ( callback ) {
                    callback( undefined, true );
                }
            }
        };
    }
});

  邏輯是比較簡單的,就不詳細分析了。可見普通情況下使用XHR方式來處理ajax。

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM