1. 程式人生 > >jQuery原始碼分析系列(35) : Ajax

jQuery原始碼分析系列(35) : Ajax

ajax的核心是通過XmlHttpRequest獲取非本頁內容,而jsonp的核心則是動態新增<script>標籤來呼叫伺服器提供的js指令碼

json核心就是:允許使用者傳遞一個callback引數給服務端,然後服務端返回資料時會將這個callback引數作為函式名來包裹住JSON資料,這樣客戶端就可以隨意定製自己的函式來自動處理返回資料了。

jquery ext dojo這類庫的實現手段其實大同小異

在同源策略下,在某個伺服器下的頁面是無法獲取到該伺服器以外的資料的,但img、iframe、script等標籤是個例外,這些標籤可以通過src屬性請求到其他伺服器上的資料。

利用script標籤的開放策略,我們可以實現跨域請求資料,當然,也需要服務端的配合。

先看一段jQuery處理jsonp的情況

通過傳送php請求

客戶端

$.ajax({
    async: false, // 同步載入資料,即等到ajax執行完畢再接著執行下面的語句
    url: 'http://192.168.1.114/yii/demos/test.php', //不同的域
    type: 'GET', // jsonp模式只有GET是合法的
    data: {
        'action': 'aaron'
    }, // 預傳參的陣列
    dataType: 'jsonp', // 資料型別
    jsonp: 'backfunc', // 指定回撥函式名,與伺服器端接收的一致,並回傳回來
success: function(json) { console.log(json); } })

php服務端

<?php
$act = trim($_GET['action']);

if($act == 'aaron' ){
    echo trim($_GET['backfunc']).'('. json_encode(array('status'=>1,'info'=>'OK')) .')';  
}
?>

一般的ajax是不能跨域請求的,因此需要使用一種特別的方式來實現跨域,其中的原理是利用

<script> 元素的這個開放策略

這裡有2個重要的引數

  • jsonpCallback

為jsonp請求指定一個回撥函式名。這個值將用來取代jQuery自動生成的隨機函式名。這主要用來讓jQuery生成一個獨特的函式名,這樣管理請求更容易,也能方便地提供回撥函式和錯誤處理。你也可以在想讓瀏覽器快取GET請求的時候,指定這個回撥函式名。從jQuery 1.5開始,你也可以使用一個函式作為該引數設定,在這種情況下,該函式的返回值就是jsonpCallback的結果。

  • jsonp

在一個jsonp請求中重寫回調函式的名字。這個值用來替代在"callback=?"這種GET或POST請求中URL引數裡的"callback"部分,比如{jsonp:'onJsonPLoad'}會導致將"onJsonPLoad=?"傳給伺服器。在jQuery 1.5,,設定jsonp選項為false,阻止了jQuery從加入"?callback"字串的URL或試圖使用"=?"轉換。在這種情況下,你也應該明確設定jsonpCallback設定。例如, { jsonp: false, jsonpCallback: "callbackName" }、

當我們正常地請求一個JSON資料的時候,服務端返回的是一串JSON型別的資料,而我們使用JSONP模式來請求資料的時候

服務端返回的是一段可執行的JavaScript程式碼

所以我們可見伺服器程式碼最後一行

$_GET['backfunc']).'('. json_encode(array('status'=>1,'info'=>'OK')) .')

就是執行的 backfunc方法,然後把資料通過回撥的方式傳遞過去

OK,就是整個流程就是:

客戶端傳送一個請求,規定一個可執行的函式名(這裡就是jQuery做了封裝的處理,自動幫你生成回撥函式並把資料取出來供success屬性方法來呼叫,不是傳遞的一個回撥控制代碼),服務端接受了這個backfunc函式名,然後把資料通過實參的形式傳送出去

jQuery的實現:

通過ajax請求不同域的實現,底層不是靠XmlHttpRequest而是script,所以不要被這個方法給迷惑了

在ajax請求中型別如果是type是get post,其實內部都只會用get,因為其跨域的原理就是用的動態載入script的src,所以我們只能把引數通過url的方式傳遞

比如

$.ajax({
    url: 'http://192.168.1.114/yii/demos/test.php', //不同的域
    type: 'GET', // jsonp模式只有GET是合法的
    data: {
        'action': 'aaron'
    }, // 預傳參的陣列
    dataType: 'jsonp', // 資料型別
    jsonp: 'backfunc', // 指定回撥函式名,與伺服器端接收的一致,並回傳回來
})

其實jquery內部會轉化成

然後動態載入

然後php方就會執行backfunc(傳遞引數);

所以流程就會分二步:

1:針對jsonp的預處理,主要是轉化拼接這些引數,然後處理快取,因為jsonp的方式也是靠載入script所以要關閉瀏覽器快取

inspectPrefiltersOrTransports中,當作了jsonp的預處理後,還要在執行inspect(dataTypeOrTransport);的遞迴,就是為了關閉這個快取機制

var dataTypeOrTransport = prefilterOrFactory(options, originalOptions, jqXHR);
            /**
             * 針對jonsp處理
             */
            if (typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[dataTypeOrTransport]) {
                //增加cache設定標記
                //不需要快取
                //dataTypes: Array[2]
                // 0: "script"
                // 1: "json"
                options.dataTypes.unshift(dataTypeOrTransport);
                inspect(dataTypeOrTransport);
                return false;
            } else if (seekingTransport) {
                return !(selected = dataTypeOrTransport);
            }

具體的預處理的程式碼

// Detect, normalize options and install callbacks for jsonp requests
// 向前置過濾器物件中新增特定型別的過濾器
// 新增的過濾器將格式化引數,並且為jsonp請求增加callbacks
jQuery.ajaxPrefilter("json jsonp", function(s, originalSettings, jqXHR) {

    var callbackName,
        overwritten,
        responseContainer,
        // 如果是表單提交,則需要檢查資料
        jsonProp = s.jsonp !== false && (rjsonp.test(s.url) ?
            "url" :
            typeof s.data === "string" 
            && !(s.contentType || "").indexOf("application/x-www-form-urlencoded") 
            && rjsonp.test(s.data) && "data"
        );

    // Handle iff the expected data type is "jsonp" or we have a parameter to set
    // 這個方法只處理jsonp,如果json的url或data有jsonp的特徵,會被當成jsonp處理
    if (jsonProp || s.dataTypes[0] === "jsonp") {

        // Get callback name, remembering preexisting value associated with it
        // s.jsonpCallback時函式,則執行函式用返回值做為回撥函式名
        callbackName = s.jsonpCallback = jQuery.isFunction(s.jsonpCallback) ?
            s.jsonpCallback() :
            s.jsonpCallback;

        // Insert callback into url or form data
        // 插入回撥url或表單資料
        // "test.php?symbol=IBM&callback=jQuery20309245402452070266_1402451299022"
        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;
        }

        // Use data converter to retrieve json after script execution
        s.converters["script json"] = function() {
            if (!responseContainer) {
                jQuery.error(callbackName + " was not called");
            }
            return responseContainer[0];
        };

        // force json dataType
        // 強制跟換型別
        s.dataTypes[0] = "json";

        // Install callback
        // 增加一個全域性的臨時函式
        overwritten = window[callbackName];
        window[callbackName] = function() {
            responseContainer = arguments;
        };

        // Clean-up function (fires after converters)
        // 在程式碼執行完畢後清理這個全部函式
        jqXHR.always(function() {
            // Restore preexisting value
            window[callbackName] = overwritten;

            // Save back as free
            if (s[callbackName]) {
                // make sure that re-using the options doesn't screw things around
                s.jsonpCallback = originalSettings.jsonpCallback;

                // save the callback name for future use
                oldCallbacks.push(callbackName);
            }

            // Call if it was a function and we have a response
            if (responseContainer && jQuery.isFunction(overwritten)) {
                overwritten(responseContainer[0]);
            }

            responseContainer = overwritten = undefined;
        });

        // Delegate to script
        return "script";
    }
});

 jquery會在window物件中載入一個全域性的函式,當代碼插入時函式執行,執行完畢後就會被移除。同時jquery還對非跨域的請求進行了優化,如果這個請求是在同一個域名下那麼他就會像正常的Ajax請求一樣工作。

分發器執行程式碼:

當我們所有的引數都轉化好了,此時會經過請求傳送器用來處理髮送的具體

為什麼會叫做分發器,因為傳送的請求目標

ajax因為參雜了jsonp的處理,所以實際上的請求不是通過 xhr.send(XmlHttpRequest)傳送的

而是通過get方式的指令碼載入的

所以

transports物件在初始化構件的時候,會生成2個處理器

  1. *: Array[1]        針對xhr方式
  2. script: Array[1]  針對script,jsonp方式

所以

transport = inspectPrefiltersOrTransports(transports, s, options, jqXHR);

那麼得到的transport就會根據當前的處理的型別,來選擇採用哪種傳送器(*、script)

針對script的請求器

jQuery.ajaxTransport("script", function(s) {
    // This transport only deals with cross domain requests
    if (s.crossDomain) {
        var script, callback;
        return {
            send: function(_, complete) {
                script = jQuery("<script>").prop({
                    async: true,
                    charset: s.scriptCharset,
                    //"http://192.168.1.114/yii/demos/test.php?backfunc=jQuery20308569577629677951_1402642881663&action=aaron&_=1402642881664"
                    src: s.url
                }).on(
                    "load error",
                    callback = function(evt) {
                        script.remove();
                        callback = null;
                        if (evt) {
                            complete(evt.type === "error" ? 404 : 200, evt.type);
                        }
                    }
                );
                document.head.appendChild(script[0]);
            },
            abort: function() {
                if (callback) {
                    callback();
                }
            }
        };
    }
});

此時就很明瞭吧

所以最終的實現就是通過動態載入指令碼!