普通的路人,普通地瞧。簡單粗暴地分析了 Zepto 的 Ajax 模塊,分析時使用的是目前最新 1.2.0 版本。 ...
一個普通的 Zepto 源碼分析(二) - ajax 模塊
普通的路人,普通地瞧。分析時使用的是目前最新 1.2.0 版本。
Zepto 可以由許多模塊組成,預設包含的模塊有 zepto 核心模塊,以及 event 、 ajax 、 form 、 ie ,其中 ajax 模塊是比較重要的模塊之一,我們可以藉助它提供的方法去做一些網路請求,還可以監聽它的生命周期事件。
Zepto 基本模塊之 ajax 模塊
我們都已經知道 Zepto 插件的一般形式是把 Zepto
對象傳入給 $
形參,那麼可以先搜索 $.
開頭的代碼段,從暴露的函數入手來分析整個代碼結構。
代碼結構與分析
刪減出外形:
;(function($){
// Number of active Ajax requests
$.active = 0
$.ajaxJSONP = function(options, deferred){...}
$.ajaxSettings = {...}
$.ajax = function(options){...}
$.get = function(/* url, data, success, dataType */){...}
$.post = function(/* url, data, success, dataType */){...}
$.getJSON = function(/* url, data, success */){...}
$.fn.load = function(url, data, success){...}
$.param = function(obj, traditional){...}
})(Zepto)
另由靜態分析可知:
$.get()
、$.post()
、$.getJSON()
、$.fn.load()
均調用了$.ajax()
和parseArguments()
,說明$.ajax()
才是我們主要分析的目標,後者則是處理函數參數的關鍵;
$.param()
、$.ajax()
、$.ajaxJSONP()
均調用了$.isFunction()
,這個倒是沒有什麼好糾結的,就是用了 Zepto 核心定義的一個判斷傳入參數是否為函數的函數;
$.ajax()
操作、返回的是一個原生的 xhr 對象,調用了很多 ajax 開頭的內部函數來完成生命周期的控制封裝。
參數規格化與 MIME
先來看看 parseArguments()
都幹了些什麼:
// handle optional data/success arguments
function parseArguments(url, data, success, dataType) {
// 參數重載
if ($.isFunction(data)) dataType = success, success = data, data = undefined
if (!$.isFunction(success)) dataType = success, success = undefined
// 返回規格化對象
return {
url: url
, data: data
, success: success
, dataType: dataType
}
}
它的參數覆蓋了我們之前提到的四個調用者的參數。
在前兩行我們可以看到,它做了一個順移來完成對重載調用格式的支持。比如 $.get(url, function(data, status, xhr){ ... })
。這個是簡單判斷參數是否為函數來完成的,有兩個缺點,一是會重覆判斷 success
,二是當只傳兩個參數時會做冗餘賦值。
那麼這個函數的作用就是參數規格化。然而.. 在 Zepto 文檔上並沒有看到對 dataType
的說明,略坑?
我們已知 $.ajaxSettings
里有一個 accepts
屬性,文檔上說是根據 dataType
來請求伺服器的,而代碼註釋里則說這是一個 Mapping ;另外根據對 $.ajax()
的靜態分析,我們還有一個 mimeToDataType()
,它根據輸入的 MIME 字元串來輸出內部定義的 dataType :
var scriptTypeRE = /^(?:text|application)\/javascript/i,
xmlTypeRE = /^(?:text|application)\/xml/i,
jsonType = 'application/json',
htmlType = 'text/html'
$.ajaxSettings = {
// MIME types mapping
// IIS returns Javascript as "application/x-javascript"
accepts: {
script: 'text/javascript, application/javascript, application/x-javascript',
json: jsonType,
xml: 'application/xml, text/xml',
html: htmlType,
text: 'text/plain'
}
}
function mimeToDataType(mime) {
if (mime) mime = mime.split(';', 2)[0]
return mime && ( mime == htmlType ? 'html' :
mime == jsonType ? 'json' :
scriptTypeRE.test(mime) ? 'script' :
xmlTypeRE.test(mime) && 'xml' ) || 'text'
}
其中 mime.split(';', 2)
限定了只能用一個分號分成兩部分,但我質疑它的效果.. 顯然限定為 1 是更好的。
get 與 post
接下來就可以來看 get/post 方法了:
$.get = function(/* url, data, success, dataType */){
return $.ajax(parseArguments.apply(null, arguments))
}
$.post = function(/* url, data, success, dataType */){
var options = parseArguments.apply(null, arguments)
options.type = 'POST'
return $.ajax(options)
}
$.getJSON = function(/* url, data, success */){
var options = parseArguments.apply(null, arguments)
options.dataType = 'json'
return $.ajax(options)
}
嗯,沒什麼好分析的, apply
也是很常見的用法。但是我們確定之前是沒有 type
屬性的,那麼可以猜測 $.ajax()
還會對 options
作進一步處理,比如合併 $.ajaxSettings
中的設置等等。
load()
函數
這是掛到原型上的,我們已知 Zepto 調用原型函數前都會把自己弄成一個類數組,也就是自己定義的集合 Collection 。
文檔上說這個方法可以給一個集合的元素用 GET Ajax 載入給定 URL 的 HTML 內容,還可以同時指定一個 CSS 選擇器,使其只載入符合這個選擇器的內容。而指定了選擇器以後,載入內容中的 script 則不會被執行。來看看是怎麼做的:
$.fn.load = function(url, data, success){
if (!this.length) return this
var self = this, parts = url.split(/\s/), selector,
options = parseArguments(url, data, success),
callback = options.success
if (parts.length > 1) options.url = parts[0], selector = parts[1]
options.success = function(response){
self.html(selector ?
$('<div>').html(response.replace(rscript, "")).find(selector)
: response)
callback && callback.apply(self, arguments)
}
$.ajax(options)
return this
}
這個 callback
操作好像挺迷的,前面多傳了 success
進去,多做了一次賦值。
同樣也沒多少好分析的,就是給 Ajax 添加了一個成功回調,用來設置元素的內容,並代理了傳入的回調。至於 .find()
是跟 jQuery 一樣的實現,當在一個集合上調用時,就篩出元素。
param()
函數
這個函數的扇入扇出也是比較少的,可以先分析。那麼這個方法也是一個序列化函數,可以把一個(狹義的)對象序列化成編碼 URL 字元串,當然也可以接收一個數組,但只接收 serializeArray
格式的。
var escape = encodeURIComponent
function serialize(params, obj, traditional, scope){
var type, array = $.isArray(obj), hash = $.isPlainObject(obj)
$.each(obj, function(key, value) {
type = $.type(value)
// 關註點 4 (遞歸進來)
if (scope) key = traditional ? scope :
scope + '[' + (hash || type == 'object' || type == 'array' ? key : '') + ']'
// 關註點 2 (初始入口)
// handle data in serializeArray() format
if (!scope && array) params.add(value.name, value.value)
// 關註點 3
// recurse into nested objects
else if (type == "array" || (!traditional && type == "object"))
serialize(params, value, traditional, key)
else params.add(key, value)
})
}
$.param = function(obj, traditional){
var params = []
// 關註點 2
params.add = function(key, value) {
if ($.isFunction(value)) value = value()
if (value == null) value = ""
this.push(escape(key) + '=' + escape(value))
}
// 關註點 1
serialize(params, obj, traditional)
return params.join('&').replace(/%20/g, '+')
}
這次就不從 serialize()
開始看了,當然是從短的開始看啦!我說的短,不是代碼有多少行,而是除去賦值操作後,把字面量壓成一行後等等,還能剩下多短的結構。
那麼我們可以看到 $.param()
里做了一個臨時數組 params
用於存放每個鍵值對的序列化結果,最後 join 到一起做修補替換。關鍵點在 add()
函數,如果 value
是一個函數,則調用並獲得其返回值。但是.. 如果其返回值或者本來就是 null
或 undefined
應該返回空值嗎?這點我不敢苟同,就算不會解析錯誤,空鍵不如乾脆不要。至於改寫 escape
看起來也沒什麼必要.. 最多就是提醒寫插件的開發者.. 直接調用就好了哇..
接下來就是看起來很長的 serialize()
函數啦。初步目測是一個遞歸,用於處理嵌套情況。那麼三個局部變數一個是拿到小寫的常見類型名,後兩個是布爾值,相信不陌生。文件內搜索發現只有來自 $.param()
的引用,那麼可以斷定第二個 if 才是初始入口,這裡是處理 serializeArray
的鍵值對象格式。而如果是普通對象 k-v 對的值是數組或對象的話,就進入遞歸調用把子結構也序列化,否則直接把 k-v 對加入 params
數組中。
要註意的是,如果設置為傳統的淺序列化模式,嵌套對象值會被無情拋棄成 [object Object]
也就是 %5Bobject+Object%5D
。而數組的 key 則是不帶方括弧的表示形式,在 Zepto 上是無論嵌套多少層數組,都會處理成同 key 而不同 value 的多個鍵值對,但 jQuery 更新了其實現,它是無論嵌套多少層放在同一個鍵值對中,用英文逗號隔開,如下:
decodeURIComponent($.param({a:1,b:[1,[2,22,[3,33,[4]]],5]},true))
// jQuery, "a=1&b=1&b=2,22,3,33,4&b=5"
// Zepto, "a=1&b=1&b=2&b=22&b=3&b=33&b=4&b=5"
至於帶方括弧的非傳統模式實現也比較簡單,每次遞歸更新 key 就好了。
Ajax 生命周期及事件
一共 7 個,都可以在官方文檔找到說明的。其中 ajaxStart
和 ajaxStop
事件只有設置為 global: true
才會在 document 上被激發,其餘則都是全局事件,在 document 或指定 DOM 節點上激發並冒泡。至於怎麼捕獲事件,相信熟悉的人都不陌生(好像是廢話)
// trigger a custom event and return false if it was cancelled
function triggerAndReturn(context, eventName, data) {
var event = $.Event(eventName)
$(context).trigger(event, data)
return !event.isDefaultPrevented()
}
// trigger an Ajax "global" event
function triggerGlobal(settings, context, eventName, data) {
if (settings.global) return triggerAndReturn(context || document, eventName, data)
}
// 關註點 1
// Number of active Ajax requests
$.active = 0
function ajaxStart(settings) {
// 關註點 2
if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart')
}
function ajaxStop(settings) {
// 關註點 2
if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
}
維護了一個 active
變數,在第一次發起 Ajax 或最後一次結束中被檢查為 0 而觸發事件,若事件沒有被抑制則開始冒泡。沒有用設計模式,應該也沒必要。
// triggers an extra global event "ajaxBeforeSend" that's like "ajaxSend" but cancelable
function ajaxBeforeSend(xhr, settings) {
var context = settings.context
if (settings.beforeSend.call(context, xhr, settings) === false ||
triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false)
return false
triggerGlobal(settings, context, 'ajaxSend', [xhr, settings])
}
function ajaxSuccess(data, xhr, settings, deferred) {
var context = settings.context, status = 'success'
settings.success.call(context, data, status, xhr)
if (deferred) deferred.resolveWith(context, [data, status, xhr])
triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data])
ajaxComplete(status, xhr, settings)
}
// type: "timeout", "error", "abort", "parsererror"
function ajaxError(error, type, xhr, settings, deferred) {
var context = settings.context
settings.error.call(context, xhr, type, error)
if (deferred) deferred.rejectWith(context, [xhr, type, error])
triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type])
ajaxComplete(type, xhr, settings)
}
// status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
function ajaxComplete(status, xhr, settings) {
var context = settings.context
settings.complete.call(context, xhr, status)
triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings])
ajaxStop(settings)
}
剩下的 5 個事件在 4 個生命時期被激發。可以看到在 ajaxBeforeSend
里允許回調或事件被抑制,這時就會返回 false 進一步取消該 Ajax 。否則就觸發 ajaxSend
事件了——不過顯然,這個時候其實還沒有真正地 send 出去,只是先激活了事件。同時我們也能看到,無論 Ajax 請求成功還是失敗,最終都觸發完成事件,最後“標誌性”地終止——當它是最後一個 Ajax 時就會觸發 ajaxStop
事件。
此外我們還可以知道, Ajax 回調是先於事件發生的;而如果是 Promise ,那麼只有當 ajaxError
時才會 reject 。
$.ajax()
函數分析
終於到了重頭戲了。至於剩餘的其他邊角函數可以一眼掃光,用到再說吧~
其實大部分代碼都用來處理 settings
了,然而還是可以大致分為幾部分的。
配置項的合併
$.ajax = function(options){
var settings = $.extend({}, options || {}),
deferred = $.Deferred && $.Deferred(),
urlAnchor, hashIndex
for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key]
ajaxStart(settings)
...
}
首先是淺複製 options
參數,接著繼承 $.ajaxSettings
中的屬性。同樣是淺複製,後者不能覆蓋用戶傳入的 options
參數.. 不過..
var settings = $.extend({}, $.ajaxSettings)
settings = $.extend(settings, options || {})
總感覺這是一樣的,哈哈。畢竟 for...in 能檢出原型上的屬性,而反正 $.extend()
淺複製時內部實現也是純 for...in ,好像沒毛病。要是支持 ES5 的話直接 Object.create()
好像也.. 沒毛病?
完成了設置項的初始化後,激發 ajaxStart
事件,開始做進一步的處理..
配置項的處理
var originAnchor = document.createElement('a')
// 關註點 1
originAnchor.href = window.location.href
$.ajax = function(options){
...
if (!settings.crossDomain) {
// 關註點 3
urlAnchor = document.createElement('a')
urlAnchor.href = settings.url
// cleans up URL for .href (IE only), see https://github.com/madrobby/zepto/pull/1049
urlAnchor.href = urlAnchor.href
// 關註點 2 (自動處理出的 protocol 和 host)
settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host)
}
...
}
對跨域屬性的處理。這裡有個特殊技巧,就是給 a
標簽修改 href
屬性後,瀏覽器會幫我們自動處理出 protocol
和 host
屬性,這對判斷是否跨域很有用,且不用調用冗長的解析庫。
我們知道不跨域的標準是協議相同、主機地址/功能變數名稱相同、埠號相同,而有人發現在 IE 且 80 埠下需要賦值完整地址才會把 host
解析出來,於是多了一個自賦值的 PR 。
function appendQuery(url, query) {
if (query == '') return url
// 關註點 3
return (url + '&' + query).replace(/[&?]{1,2}/, '?')
}
// serialize payload and append it to the URL for GET requests
function serializeData(options) {
// 關註點 4 (序列化 data 對象)
if (options.processData && options.data && $.type(options.data) != "string")
options.data = $.param(options.data, options.traditional)
// 關註點 5 (預設 GET 的 url 處理)
if (options.data && (!options.type || options.type.toUpperCase() == 'GET' || 'jsonp' == options.dataType))
options.url = appendQuery(options.url, options.data), options.data = undefined
}
$.ajax = function(options){
...
// 關註點 1
if (!settings.url) settings.url = window.location.toString()
// 關註點 2
if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex)
serializeData(settings)
...
}
對 url
屬性的處理。 window.location
的使用是 DOM 基礎知識了,前面沒用我猜是為了保持一致性?(逃
這裡暫時不知道不保留 # 號後部分的作用,只知道 WHATWG 定義其為 URL-fragment ,沒有很特別的說明,也許有時間要看看 Node 的解析說明。
搞定了 url 後就可以序列化數據了。根據調用關係, appendQuery()
有 4 個扇入,唯一亮點就是每次把第一個出現的 & 或 ? 替換成 ? 。我認為這個實現是基於傳入 url
是 / 結尾的假設的,那麼其實判斷最後一個字元來決定使用 & 或 ? 應當比查找要好很多。至於 serializeData
就是兩種情況,如果提供的 options.data
不是一個字元串且需要自動序列化,那麼就調用之前提到的 $.param()
進行序列化,否則如果是 jsonp 或者預設 GET 則處理進 options.url
里。
// 關註點 1
var dataType = settings.dataType, hasPlaceholder = /\?.+=\?/.test(settings.url)
if (hasPlaceholder) dataType = 'jsonp'
// 關註點 2 (要不要緩存的判斷與處理)
if (settings.cache === false || (
(!options || options.cache !== true) &&
('script' == dataType || 'jsonp' == dataType)
))
settings.url = appendQuery(settings.url, '_=' + Date.now())
// 關註點 3 ( jsonp 的判斷與處理)
if ('jsonp' == dataType) {
if (!hasPlaceholder)
settings.url = appendQuery(settings.url,
settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?')
return $.ajaxJSONP(settings, deferred)
}
根據 dataType
對緩存和 jsonp 的處理,也算一小段吧。不使用緩存的處理好理解,就是常見的加入時間參數。
而 hasPlaceholder
則是測試(貪婪匹配)最後一個鍵值對的值(即 url 中的 callbackName )是否為 placeholder 即 ? 符。這個實現很奇怪,已經不符合現在的 jQuery 了,現在似乎是不能只在 url 指定 =?
的,必須設置 dataType: jsonp
才行。另外先補 url 再替換似乎也有些低效。
原生 xhr 對象的 header 設置
首先是對 request header 的設置:
var mime = settings.accepts[dataType],
headers = { }, /* 關註點 1 (暫存 header ) */
setHeader = function(name, value) { headers[name.toLowerCase()] = [name, value] },
xhr = settings.xhr(),
nativeSetHeader = xhr.setRequestHeader
if (deferred) deferred.promise(xhr)
if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest')
// 關註點 2
setHeader('Accept', mime || '*/*')
// 關註點 3 (註意優先順序)
if (mime = settings.mimeType || mime) {
if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0]
xhr.overrideMimeType && xhr.overrideMimeType(mime)
}
//關註點 4
if (settings.contentType || (settings.contentType !== false && settings.data && settings.type.toUpperCase() != 'GET'))
setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded')
//關註點 4
if (settings.headers) for (name in settings.headers) setHeader(name, settings.headers[name])
xhr.setRequestHeader = setHeader
註意到在 v1.1.1 的 commit 記錄里寫到,為了支持在 beforeSend
生命期中(這個時候 xhr 對象還沒 open )調用 xhr.setRequestHeader()
修改 header ,用自定義的 setHeader()
函數暫存下來,到實際要操作打開 xhr 對象時再去調用原生方法設置 header 。
註意到這一句 setHeader('Accept', mime || '*/*')
, MDN 上是這麼說的:
If no Accept header has been set using this, an Accept header with the */* is sent with the request when send() is called.
因此我認為可以改成 mime && setHeader('Accept', mime)
。
而緊接著的 if 具有一定的迷惑性,它其實是要用 accepts[dataType]
或者 mimeType
來重寫響應頭裡的 MIME (賦值的優先順序較低,其實完全可以拿出來賦值)。再下來就是針對非 GET 而又有上傳數據的請求,將 Content-Type 改為 POST 格式。再下來就是存下自定義的 header 並重寫方法了,可以看到自定義 header 會覆蓋 Zepto 的預設值。
發送 xhr
$.ajax = function(options){
...
xhr.onreadystatechange = function(){...}
// 關註點 1
if (ajaxBeforeSend(xhr, settings) === false) {
xhr.abort()
ajaxError(null, 'abort', xhr, settings, deferred)
return xhr
}
// 關註點 2 (打開 xhr 對象)
var async = 'async' in settings ? settings.async : true
xhr.open(settings.type, settings.url, async, settings.username, settings.password)
if (settings.xhrFields) for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name]
for (name in headers) nativeSetHeader.apply(xhr, headers[name])
// 關註點 3 (超時的後續處理)
if (settings.timeout > 0) abortTimeout = setTimeout(function(){
xhr.onreadystatechange = empty
xhr.abort()
ajaxError(null, 'timeout', xhr, settings, deferred)
}, settings.timeout)
// 關註點 4
// avoid sending empty string (#319)
xhr.send(settings.data ? settings.data : null)
return xhr
}
先不管 onreadystatechange
回調,裡面只有一個完成狀態的判斷。
這裡終於跑到了第二個生命期,準備工作已經做好,觸發可以被取消的 ajaxBeforeSend
事件,接著就是打開 xhr 了。這裡有一個點是超時的處理,把 onreadystatechange
回調設置為空我認為是一個收尾工作,比如 $.ajax()
返回的 xhr 對象也可以重新打開,這時候顯然不希望還是原來的回調。另外不使用原生超時事件的原因應該是 Android 4.4 的瀏覽器還不支持。
最後 xhr.send()
註釋了對 #319 的修補。這個 issue 的大意是當在 Chrome 上 POST 的數據為空字元串時(經過上面的處理,傳入的 data
變為了 undefined
),會觸發一個 CORS 錯誤。應該是 11 年 Chrome 上的 BUG ,現在我無法復現了。
onreadystatechange()
回調
;(function($){
var blankRE = /^\s*$/
...
xhr.onreadystatechange = function(){
if (xhr.readyState == 4) {
xhr.onreadystatechange = empty
clearTimeout(abortTimeout)
var result, error = false
// 關註點 1 (正常狀態碼的判斷)
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) {
dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'))
// 關註點 2 (響應數據流)
if (xhr.responseType == 'arraybuffer' || xhr.responseType == 'blob')
result = xhr.response
else {
result = xhr.responseText
// 關註點 3 ( evel() 的間接調用與數據類型判斷)
try {
// http://perfectionkills.com/global-eval-what-are-the-options/
// sanitize response accordingly if data filter callback provided
result = ajaxDataFilter(result, dataType, settings)
if (dataType == 'script') (1,eval)(result)
else if (dataType == 'xml') result = xhr.responseXML
else if (dataType == 'json') result = blankRE.test(result) ? null : $.parseJSON(result)
} catch (e) { error = e }
if (error) return ajaxError(error, 'parsererror', xhr, settings, deferred)
}
ajaxSuccess(result, xhr, settings, deferred)
} else {
ajaxError(xhr.statusText || null, xhr.status ? 'error' : 'abort', xhr, settings, deferred)
}
}
}
...
}
對於本地文件(即 file:
協議頭)瀏覽器的狀態碼會是 0 。而如果是瀏覽器取消了 xhr 請求則觸發 abort 類型的 ajaxError
事件,如宿主機的網路連接變化 / 中斷了等。
有意思的點是 responseType
屬性。兩種類型我都沒見過,據 MDN 是用於二進位數據傳輸, 由 .response
返回相應的對象。
一個奇怪的技巧是如代碼註釋所示,使用間接調用的形式 (1,eval)()
來避免污染外層作用域。再吐個槽,對 dataType
的賦值可以放進 try 塊里的。
$.ajaxJSONP()
函數
接下來看下最後一個函數和 $.ajax()
相比有哪些不同。在文檔上標為廢棄,實際上是不建議直接使用。而在上面的代碼我們也看到 $.ajaxJSONP()
在 $.ajax()
中的調用是發生在事件 ajaxStart
事件之後、配置項合併完成後、設置 header 之前的。
;(function($){
var jsonpID = +new Date()
...
$.ajaxJSONP = function(options, deferred){
if (!('type' in options)) return $.ajax(options)
// 關註點 2 (回調函數名的處理)
var _callbackName = options.jsonpCallback,
callbackName = ($.isFunction(_callbackName) ?
_callbackName() : _callbackName) || ('Zepto' + (jsonpID++)),
script = document.createElement('script'),
originalCallback = window[callbackName],
responseData,
abort = function(errorType) {
$(script).triggerHandler('error', errorType || 'abort')
}, /* 關註點 1 (只有取消方法的 xhr 對象) */
xhr = { abort: abort }, abortTimeout
if (deferred) deferred.promise(xhr)
$(script).on('load error', function(e, errorType){...})
if (ajaxBeforeSend(xhr, options) === false) {
abort('abort')
return xhr
}
// 關註點 3 (對自動回調的代理包裝,先拿到數據)
window[callbackName] = function(){
responseData = arguments
}
// 關註點 4 (最後一個 xxx=? 的替換)
script.src = options.