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是不能跨域請求的,因此需要使用一種特別的方式來實現跨域,其中的原理是利用
這裡有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個處理器
- *: Array[1] 針對xhr方式
- 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(); } } }; } });
此時就很明瞭吧
所以最終的實現就是通過動態載入指令碼!