Ajax
模塊也是經常會用到的模塊,Ajax
模塊中包含了 jsonp
的現實,和 XMLHttpRequest
的封裝。
讀 Zepto 源碼系列文章已經放到了github上,歡迎star: reading-zepto
源碼版本
本文閱讀的源碼爲 zepto1.2.0
ajax的事件觸發順序
zepto
針對 ajax
的發送過程,定義了以下幾個事件,正常情況下的觸發順序如下:
ajaxstart
:XMLHttpRequest
實例化前觸發ajaxBeforeSend
: 發送ajax
請求前觸發ajaxSend
: 發送ajax
請求時觸發ajaxSuccess
/ajaxError
: 請求成功/失敗時觸發ajaxComplete
: 請求完成(無論成功還是失敗)時觸發ajaxStop
: 請求完成後觸發,這個事件在ajaxComplete
後觸發。
ajax 方法的參數解釋
現在還沒有講到 ajax
方法,之所以要將參數提前,是因爲後面的內容,不時會用到相關的參數,所以一開始先將參數解釋清楚。
type
:HTTP
請求的類型;url
: 請求的路徑;data
: 請求參數;processData
: 是否需要將非GET
請求的參數轉換成字符串,默認爲true
,即默認轉換成字符串;contentType
: 設置Content-Type
請求頭;mineType
: 覆蓋響應的MIME
類型,可以是json
、jsonp
、script
、xml
、html
、 或者text
;jsonp
:jsonp
請求時,攜帶回調函數名的參數名,默認爲callback
;jsonpCallback
:jsonp
請求時,響應成功時,執行的回調函數名,默認由zepto
管理;timeout
: 超時時間,默認爲0
;headers
:設置HTTP
請求頭;async
: 是否爲同步請求,默認爲false
;global
: 是否觸發全局ajax
事件,默認爲true
;context
: 執行回調時(如jsonpCallbak
)時的上下文環境,默認爲window
。traditional
: 是否使用傳統的淺層序列化方式序列化data
參數,默認爲false
,例如有data
爲{p1:'test1', p2: {nested: 'test2'}
,在traditional
爲false
時,會序列化成p1=test1&p2[nested]=test2
, 在爲true
時,會序列化成p1=test&p2=[object+object]
;xhrFields
:xhr
的配置;cache
:是否允許瀏覽器緩存GET
請求,默認爲false
;username
:需要認證的HTTP
請求的用戶名;password
: 需要認證的HTTP
請求的密碼;dataFilter
: 對響應數據進行過濾;xhr
:XMLHttpRequest
實例,默認用new XMLHttpRequest()
生成;accepts
:從服務器請求的MIME
類型;beforeSend
: 請求發出前調用的函數;success
: 請求成功後調用的函數;error
: 請求出錯時調用的函數;complete
: 請求完成時調用的函數,無論請求是失敗還是成功。
內部方法
triggerAndReturn
function triggerAndReturn(context, eventName, data) {
var event = $.Event(eventName)
$(context).trigger(event, data)
return !event.isDefaultPrevented()
}
triggerAndReturn
用來觸發一個事件,並且如果該事件禁止瀏覽器默認事件時,返回 false
。
參數 context
爲上下文,eventName
爲事件名,data
爲數據。
該方法內部調用了 Event
模塊的 trigger
方法,具體分析見《讀Zepto源碼之Event模塊》。
triggerGlobal
function triggerGlobal(settings, context, eventName, data) {
if (settings.global) return triggerAndReturn(context || document, eventName, data)
}
觸發全局事件
settings
爲 ajax
配置,context
爲指定的上下文對象,eventName
爲事件名,data
爲數據。
triggerGlobal
內部調用的是 triggerAndReturn
方法,如果有指定上下文對象,則在指定的上下文對象上觸發,否則在 document
上觸發。
ajaxStart
function ajaxStart(settings) {
if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart')
}
觸發全局的 ajaxStart
事件。
如果 global
設置爲 true
,則 $.active
的值增加1。
如果 global
爲 true
,並且 $.active
在更新前的數量爲 0
,則觸發全局的 ajaxStart
事件。
ajaxStop
function ajaxStop(settings) {
if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
}
觸發全局 ajaxStop
事件。
如果 global
爲 true
,則將 $.active
的數量減少 1
。如果 $.active
的數量減少至 0
,即沒有在執行中的 ajax
請求時,觸發全局的 ajaxStop
事件。
ajaxBeforeSend
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])
}
ajaxBeforeSend
方法,觸發 ajaxBeforeSend
事件和 ajaxSend
事件。
這兩個事件很相似,只不過 ajaxBeforedSend
事件可以通過外界的配置來取消事件的觸發。
在觸發 ajaxBeforeSend
事件之前,會調用配置中的 beforeSend
方法,如果 befoeSend
方法返回的爲 false
時,則取消觸發 ajaxBeforeSend
事件,並且會取消後續 ajax
請求的發送,後面會講到。
否則觸發 ajaxBeforeSend
事件,並且將 xhr
事件,和配置 settings
作爲事件攜帶的數據。
注意這裏很巧妙地使用了 ||
進行斷路。
如果 beforeSend
返回的爲 false
或者觸發ajaxBeforeSend
事件的方法 triggerGlobal
返回的爲 false
,也即取消了瀏覽器的默認行爲,則 ajaxBeforeSend
方法返回 false
,中止後續的執行。
否則在觸發完 ajaxBeforeSend
事件後,觸發 ajaxSend
事件。
ajaxComplete
function ajaxComplete(status, xhr, settings) {
var context = settings.context
settings.complete.call(context, xhr, status)
triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings])
ajaxStop(settings)
}
觸發 ajaxComplete
事件。
在觸發 ajaxComplete
事件前,調用配置中的 complete
方法,將 xhr
實例和當前的狀態 state
作爲回調函數的參數。在觸發完 ajaxComplete
事件後,調用 ajaxStop
方法,觸發 ajaxStop
事件。
ajaxSuccess
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)
}
觸發 ajaxSucess
方法。
在觸發 ajaxSuccess
事件前,先調用配置中的 success
方法,將 ajax
返回的數據 data
和當前狀態 status
及 xhr
作爲回調函數的參數。
如果 deferred
存在,則調用 resoveWith
的方法,因爲 deferred
對象,因此在使用 ajax
的時候,可以使用 promise
風格的調用。關於 deferred
,見 《讀Zepto源碼之Deferred模塊》的分析。
在觸發完 ajaxSuccess
事件後,繼續調用 ajaxComplete
方法,觸發 ajaxComplete
事件。
ajaxError
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)
}
觸發 ajaxError
事件,錯誤的類型可以爲 timeout
、error
、 abort
、 parsererror
。
在觸發事件前,調用配置中的 error
方法,將 xhr
實例,錯誤類型 type
和 error
對象作爲回調函數的參數。
隨後調用 ajaxComplete
方法,觸發 ajaxComplete
事件。因此,ajaxComplete
事件無論成功還是失敗都會觸發。
empty
function empty() {}
空函數,用來作爲回調函數配置的初始值。這樣的好處是在執行回調函數時,不需要每次都判斷回調函數是否存在。
ajaxDataFilter
function ajaxDataFilter(data, type, settings) {
if (settings.dataFilter == empty) return data
var context = settings.context
return settings.dataFilter.call(context, data, type)
}
主要用來過濾請求成功後的響應數據。
如果配置中的 dataFilter
屬性爲初始值 empty
,則將原始數據返回。
如果有配置 dataFilter
,則調用配置的回調方法,將數據 data
和數據類型 type
作爲回調的參數,再將執行的結果返回。
mimeToDataType
var htmlType = 'text/html',
jsonType = 'application/json',
scriptTypeRE = /^(?:text|application)\/javascript/i,
xmlTypeRE = /^(?:text|application)\/xml/i,
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'
}
返回 dataType
的類型。
先看看這個函數中使用到的幾個正則表達式,scriptTypeRE
匹配的是 text/javascript
或者 application/javascript
, xmlTypeRE
匹配的是 text/xml
或者 application/xml
, 都還比較簡單,不作過多的解釋。
Content-Type
的值的形式如下 text/html; charset=utf-8
, 所以如果參數 mime
存在,則用 ;
分割,取第一項,這裏是 text/html
,即爲包含類型的字符串。
接下來是針對 html
、json
、 script
和 xml
用對應的正則進行匹配,匹配成功,返回對應的類型值,如果都不匹配,則返回 text
。
appendQuery
function appendQuery(url, query) {
if (query == '') return url
return (url + '&' + query).replace(/[&?]{1,2}/, '?')
}
向 url
追加參數。
如果 query
爲空,則將原 url
返回。
如果 query
不爲空,則用 &
拼接 query
。
最後調用 replace
,將 &&
、 ?&
,&?
或 ??
替換成 ?
。
拼接出來的 url
的形式如 url?key=value&key2=value
parseArguments
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
}
}
這個方法是用來格式化參數的,Ajax
模塊定義了一些便捷的調用方法,這些調用方法不需要傳遞 option
,某些必填值已經採用了默認傳遞的方式,這些方法中有些參數是可以不需要傳遞的,這個方法就是來用判讀那些參數有傳遞,那些沒有傳遞,然後再將參數拼接成 ajax
所需要的 options
對象。
serialize
function serialize(params, obj, traditional, scope){
var type, array = $.isArray(obj), hash = $.isPlainObject(obj)
$.each(obj, function(key, value) {
type = $.type(value)
if (scope) key = traditional ? scope :
scope + '[' + (hash || type == 'object' || type == 'array' ? key : '') + ']'
// handle data in serializeArray() format
if (!scope && array) params.add(value.name, value.value)
// recurse into nested objects
else if (type == "array" || (!traditional && type == "object"))
serialize(params, value, traditional, key)
else params.add(key, value)
})
}
序列化參數。
要了解這個函數,需要了解 traditional
參數的作用,這個參數表示是否開啓以傳統的淺層序列化方式來進行序列化,具體的示例見上文參數解釋部分。
如果參數 obj
的爲數組,則 array
爲 true
, 如果爲純粹對象,則 hash
爲 true
。 $.isArray
和 $.isPlainObject
的源碼分析見《讀Zepto源碼之內部方法》。
遍歷需要序列化的對象 obj
,判斷 value
的類型 type
, 這個 type
後面會用到。
scope
是記錄深層嵌套時的 key
值,這個 key
值受 traditional
的影響。
如果 traditional
爲 true
,則 key
爲原始的 scope
值,即對象第一層的 key
值。
否則,用 []
拼接當前循環中的 key
,最終的 key
值會是這種形式 scope[key][key2]...
如果 obj
爲數組,並且 scope
不存在,即爲第一層,直接調用 params.add
方法,這個方法後面會分析到。
否則如果 value
的類型爲數組或者非傳統序列化方式下爲對象,則遞歸調用 serialize
方法,用來處理 key
。
其他情況調用 params.add
方法。
serializeData
function serializeData(options) {
if (options.processData && options.data && $.type(options.data) != "string")
options.data = $.param(options.data, options.traditional)
if (options.data && (!options.type || options.type.toUpperCase() == 'GET' || 'jsonp' == options.dataType))
options.url = appendQuery(options.url, options.data), options.data = undefined
}
序列化參數。
如果 processData
爲 true
,並且參數 data
不爲字符串,則調用 $.params
方法序列化參數。 $.params
方法後面會講到。
如果爲 GET
請求或者爲 jsonp
,則調用 appendQuery
,將參數拼接到請求地址後面。
對外接口
$.active
$.active = 0
正在請求的 ajax
數量,初始時爲 0
。
$.ajaxSettings
$.ajaxSettings = {
// Default type of request
type: 'GET',
// Callback that is executed before request
beforeSend: empty,
// Callback that is executed if the request succeeds
success: empty,
// Callback that is executed the the server drops error
error: empty,
// Callback that is executed on request complete (both: error and success)
complete: empty,
// The context for the callbacks
context: null,
// Whether to trigger "global" Ajax events
global: true,
// Transport
xhr: function () {
return new window.XMLHttpRequest()
},
// 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'
},
// Whether the request is to another domain
crossDomain: false,
// Default timeout
timeout: 0,
// Whether data should be serialized to string
processData: true,
// Whether the browser should be allowed to cache GET responses
cache: true,
//Used to handle the raw response data of XMLHttpRequest.
//This is a pre-filtering function to sanitize the response.
//The sanitized response should be returned
dataFilter: empty
}
ajax
默認配置,這些是 zepto
的默認值,在使用時,可以更改成自己需要的配置。
$.param
var escape = encodeURIComponent
$.param = function(obj, traditional){
var params = []
params.add = function(key, value) {
if ($.isFunction(value)) value = value()
if (value == null) value = ""
this.push(escape(key) + '=' + escape(value))
}
serialize(params, obj, traditional)
return params.join('&').replace(/%20/g, '+')
}
param
方法用來序列化參數,內部調用的是 serialize
方法,並且在容器 params
上定義了一個 add
方法,供 serialize
調用。
add
方法比較簡單,首先判斷值 value
是否爲 function
,如果是,則通過調用函數來取值,如果爲 null
或者 undefined
,則 value
賦值爲空字符串。
然後將 key
和 value
用 encodeURIComponent
編碼,用 =
號連接起來。
接着便是簡單的調用 serialize
方法。
最後將容器中的數據用 &
連接起來,並且將空格替換成 +
號。
$.ajaxJSONP
var jsonpID = +new Date()
$.ajaxJSONP = function(options, deferred){
if (!('type' in options)) return $.ajax(options)
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')
},
xhr = { abort: abort }, abortTimeout
if (deferred) deferred.promise(xhr)
$(script).on('load error', function(e, errorType){
clearTimeout(abortTimeout)
$(script).off().remove()
if (e.type == 'error' || !responseData) {
ajaxError(null, errorType || 'error', xhr, options, deferred)
} else {
ajaxSuccess(responseData[0], xhr, options, deferred)
}
window[callbackName] = originalCallback
if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0])
originalCallback = responseData = undefined
})
if (ajaxBeforeSend(xhr, options) === false) {
abort('abort')
return xhr
}
window[callbackName] = function(){
responseData = arguments
}
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
document.head.appendChild(script)
if (options.timeout > 0) abortTimeout = setTimeout(function(){
abort('timeout')
}, options.timeout)
return xhr
}
在分析源碼之前,先了解一下 jsonp
的原理。
jsonp
實現跨域其實是利用了 script
可以請求跨域資源的特點,所以實現 jsonp
的基本步驟就是向頁面動態插入一個 script
標籤,在請求地址上帶上需要傳遞的參數,後端再將數據返回,前端調用回調函數進行解釋。
所以 jsonp
本質上是一個 GET
請求,因爲鏈接的長度有限制,因此請求所攜帶的參數的長度也會有限制。
一些變量的定義
if (!('type' in options)) return $.ajax(options)
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')
},
xhr = { abort: abort }, abortTimeout
if (deferred) deferred.promise(xhr)
如果配置中的請求類型沒有定義,則直接調用 $.ajax
方法,這個方法是整個模塊的核心,後面會講到。 jsonp
請求的 type
必須爲 jsonp
。
私有變量用來臨時存放配置中的 jsonpCallback
,即 jsonp
請求成功後執行的回調函數名,該配置可以爲 function
類型。
callbackName
是根據配置得出的回調函數名。如果 _callbackName
爲 function
,則以執行的結果作爲回調函數名,如果 _callbackName
沒有配置,則用 Zepto
+ 時間戳
作爲回調函數名,時間戳初始化後,採用自增的方式來實現函數名的唯一性。
script
用來保存創建的 script
節點。
originalCallback
用來儲存原始的回調函數。
responseData
爲響應的數據。
abort
函數用來中止 jsonp
請求,實質上是觸發了 error
事件。
xhr
對象只有 abort
方法,如果存在 deferred
對象,則調用 promise
方法在 xhr
對象的基礎上生成一個 promise
對象。
abortTimeout
用來指定超時時間。
beforeSend
if (ajaxBeforeSend(xhr, options) === false) {
abort('abort')
return xhr
}
在發送 jsonp
請求前,會調用 ajaxBeforeSend
方法,如果返回的爲 false
,則中止 jsonp
請求的發送。
發送請求
window[callbackName] = function(){
responseData = arguments
}
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
document.head.appendChild(script)
發送請求前,重寫了 window[callbackName]
函數,將 arguments
賦值給 responseData
, 這個函數會在後端返回的 js
代碼中執行,這樣 responseData
就可以獲取得到數據了。
接下來,將 url
的=?
佔位符,替換成回調函數名,最後將 script
插入到頁面中,發送請求。
請求超時
if (options.timeout > 0) abortTimeout = setTimeout(function(){
abort('timeout')
}, options.timeout)
如果有設置超時時間,則在請求超時時,觸發錯誤事件。
請求成功或失敗
$(script).on('load error', function(e, errorType){
clearTimeout(abortTimeout)
$(script).off().remove()
if (e.type == 'error' || !responseData) {
ajaxError(null, errorType || 'error', xhr, options, deferred)
} else {
ajaxSuccess(responseData[0], xhr, options, deferred)
}
window[callbackName] = originalCallback
if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0])
originalCallback = responseData = undefined
})
在請求成功或者失敗時,先清除請求超時定時器,避免觸發超時錯誤,再將插入頁面的 script
從頁面上刪除,因爲數據已經獲取到,不再需要這個 script
了。注意在刪除 script
前,調用了 off
方法,將 script
上的事件都移除了。
如果請求出錯,則調用 ajaxError
方法。
如果請求成功,則調用 ajaxSuccess
方法。
之前我們把 window[callbackName]
重寫掉了,目的是爲了獲取到數據,現在再重新將原來的回調函數賦值回去,在獲取到數據後,如果 originalCallback
有定義,並且爲函數,則將數據作爲參數傳遞進去,執行。
最後將數據和臨時函數 originalCallback
清理。
$.ajax
$.ajax
方法是整個模塊的核心,代碼太長,就不全部貼在這裏了,下面一部分一部分來分析。
處理默認配置
var settings = $.extend({}, options || {}),
deferred = $.Deferred && $.Deferred(),
urlAnchor, hashIndex
for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key]
ajaxStart(settings)
settings
爲所傳遞配置的副本。
deferred
爲 deferred
對象。
urlAnchor
爲瀏覽器解釋的路徑,會用來判斷是否跨域,後面會講到。
hashIndex
爲路徑中 hash
的索引。
用 for ... in
去遍歷 $.ajaxSettings
,作爲配置的默認值。
配置處理完畢後,調用 ajaxStart
函數,觸發 ajaxStart
事件。
判斷是否跨域
originAnchor = document.createElement('a')
originAnchor.href = window.location.href
if (!settings.crossDomain) {
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
settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host)
}
如果跨域 crossDomain
沒有設置,則需要檢測請求的地址是否跨域。
originAnchor
是當前頁面鏈接,整體思路是創建一個 a
節點,將 href
屬性設置爲當前請求的地址,然後獲取節點的 protocol
和 host
,看跟當前頁面的鏈接用同樣方式拼接出來的地址是否一致。
注意到這裏的 urlAnchor
進行了兩次賦值,這是因爲 ie
默認不會對鏈接 a
添加端口號,但是會對 window.location.href
添加端口號,如果端口號爲 80
時,會出現不一致的情況。具體見:pr#1049
處理請求地址
if (!settings.url) settings.url = window.location.toString()
if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex)
serializeData(settings)
如果沒有配置 url
,則用當前頁面的地址作爲請求地址。
如果請求的地址帶有 hash
, 則將 hash
去掉,因爲 hash
並不會傳遞給後端。
然後調用 serializeData
方法來序列化請求參數 data
。
處理緩存
var dataType = settings.dataType, hasPlaceholder = /\?.+=\?/.test(settings.url)
if (hasPlaceholder) dataType = 'jsonp'
if (settings.cache === false || (
(!options || options.cache !== true) &&
('script' == dataType || 'jsonp' == dataType)
))
settings.url = appendQuery(settings.url, '_=' + Date.now())
hasPlaceholder
的正則匹配規則跟上面分析到 jsonp
的替換 callbackName
的正則一樣,約定以這樣的方式來替換 url
中的 callbackName
。因此,也可以用這樣的正則來判斷是否爲 jsonp
。
如果 cache
的配置爲 false
,或者在 dataType
爲 script
或者 jsonp
的情況下, cache
沒有設置爲 true
時,表示不需要緩存,清除瀏覽器緩存的方式也很簡單,就是往請求地址的後面加上一個時間戳,這樣每次請求的地址都不一樣,瀏覽器自然就沒有緩存了。
處理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
時,會對 url
進行一些處理。
如果還沒有 ?=
佔位符,則向 url
中追加佔位符。
如果 settings.jsonp
存在,則追加 settings.jsonp
+ =?
。
如果 settings.jsonp
爲 false
, 則不向 url
中追加東西。
否則默認追加 callback=?
。
url
拼接完畢後,調用 $.ajaxJSONP
方法,發送 jsonp
請求。
一些變量
var mime = settings.accepts[dataType],
headers = { },
setHeader = function(name, value) { headers[name.toLowerCase()] = [name, value] },
protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol,
xhr = settings.xhr(),
nativeSetHeader = xhr.setRequestHeader,
abortTimeout
if (deferred) deferred.promise(xhr)
mime
獲取數據的 mime
類型。
headers
爲請求頭。
setHeader
爲設置請求頭的方法,其實是往 headers
上增加對應的 key
value
值。
protocol
爲協議,匹配一個或多個以字母、數字或者 -
開頭,並且後面爲 ://
的字符串。優先從配置的 url
中獲取,如果沒有配置 url
,則取 window.location.protocol
。
xhr
爲 XMLHttpRequest
實例。
nativeSetHeader
爲 xhr
實例上的 setRequestHeader
方法。
abortTimeout
爲超時定時器的 id
。
如果 deferred
對象存在,則調用 promise
方法,以 xhr
爲基礎生成一個 promise
。
設置請求頭
if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest')
setHeader('Accept', mime || '*/*')
if (mime = settings.mimeType || mime) {
if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0]
xhr.overrideMimeType && xhr.overrideMimeType(mime)
}
if (settings.contentType || (settings.contentType !== false && settings.data && settings.type.toUpperCase() != 'GET'))
setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded')
if (settings.headers) for (name in settings.headers) setHeader(name, settings.headers[name])
xhr.setRequestHeader = setHeader
如果不是跨域請求時,設置請求頭 X-Requested-With
的值爲 XMLHttpRequest
。這個請求頭的作用是告訴服務端,這個請求爲 ajax
請求。
setHeader('Accept', mime || '*/*')
用來設置客戶端接受的資源類型。
當 mime
存在時,調用 overrideMimeType
方法來重寫 response
的 content-type
,使得服務端返回的類型跟客戶端要求的類型不一致時,可以按照指定的格式來解釋。具體可以參見這篇文章 《你真的會使用XMLHttpRequest嗎?》。
如果有指定 contentType
,
或者 contentType
沒有設置爲 false
,並且 data
存在以及請求類型不爲 GET
時,設置 Content-Type
爲指定的 contentType
,在沒有指定時,設置爲 application/x-www-form-urlencoded
。所以沒有指定 contentType
時, POST
請求,默認的 Content-Type
爲 application/x-www-form-urlencoded
。
如果有配置 headers
,則遍歷 headers
配置,分別調用 setHeader
方法配置。
before send
if (ajaxBeforeSend(xhr, settings) === false) {
xhr.abort()
ajaxError(null, 'abort', xhr, settings, deferred)
return xhr
}
調用 ajaxBeforeSend
方法,如果返回的爲 false
,則中止 ajax
請求。
同步和異步請求的處理
var async = 'async' in settings ? settings.async : true
xhr.open(settings.type, settings.url, async, settings.username, settings.password)
如果有配置 async
,則採用配置中的值,否則,默認發送的是異步請求。
接着調用 open
方法,創建一個請求。
創建請求後的配置
if (settings.xhrFields) for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name]
for (name in headers) nativeSetHeader.apply(xhr, headers[name])
如果有配置 xhrFields
,則遍歷,設置對應的 xhr
屬性。
再遍歷上面配置的 headers
對象,調用 setRequestHeader
方法,設置請求頭,注意這裏的請求頭必須要在 open
之後,在 send
之前設置。
發送請求
xhr.send(settings.data ? settings.data : null)
發送請求很簡單,調用 xhr.send
方法,將配置中的數據傳入即可。
請求響應成功後的處理
xhr.onreadystatechange = function(){
if (xhr.readyState == 4) {
xhr.onreadystatechange = empty
clearTimeout(abortTimeout)
var result, error = false
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) {
dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'))
if (xhr.responseType == 'arraybuffer' || xhr.responseType == 'blob')
result = xhr.response
else {
result = xhr.responseText
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)
}
}
}
readyState
readyState
有以下5種狀態,狀態切換時,會響應 onreadystatechange
的回調。
0 | xhr 實例已經創建,但是還沒有調用 open 方法。 |
---|---|
1 | 已經調用 open 方法 |
2 | 請求已經發送,可以獲取響應頭和狀態 status |
3 | 下載中,部分響應數據已經可以使用 |
4 | 請求完成 |
具體見 MDN:XMLHttpRequest.readyState
清理工作
xhr.onreadystatechange = empty
clearTimeout(abortTimeout)
當 readyState
變爲 4
時,表示請求完成(無論成功還是失敗),這時需要將 onreadystatechange
重新賦值爲 empty
函數,清除超時響應定時器,避免定時器超時的任務執行。
成功狀態判斷
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) {
...
}
這裏判斷的是 http
狀態碼,狀態碼的含義可以參考 HTTP response status codes。
解釋一下最後這個條件 xhr.status == 0 && protocol == 'file:'
。
status
爲 0
時,表示請求並沒有到達服務器,有幾種情況會造成 status
爲 0
的情況,例如網絡不通,不合法的跨域請求,防火牆攔截等。
直接用本地文件的方式打開,也會出現 status
爲 0
的情況,但是我在 chrome
上測試,在這種情況下只能取到 status
, responseType
和 responseText
都取不到,不清楚這個用本地文件打開時,進入成功判斷的目的何在。
處理數據
blankRE = /^\s*$/,
dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'))
if (xhr.responseType == 'arraybuffer' || xhr.responseType == 'blob')
result = xhr.response
else {
result = xhr.responseText
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)
首先獲取 dataType
,後面會根據 dataType
來判斷獲得的數據類型,進而調用不同的方法來處理。
如果數據爲 arraybuffer
或 blob
對象時,即爲二進制數據時,result
從 response
中直接取得。
否則,用 responseText
獲取數據,然後再對數據嘗試解釋。
在解釋數據前,調用 ajaxDataFilter
對數據進行過濾。
如果數據類型爲 script
,則使用 eval
方法,執行返回的 script
內容。
這裏爲什麼用 (1, eval)
,而不是直接用 eval
呢,是爲了確保 eval
執行的作用域是在 window
下。具體參考:(1,eval)(‘this’) vs eval(‘this’) in JavaScript? 和 《Global eval. What are the options?》
如果 dataType
爲 xml
,則調用responseXML
方法
如果爲 json
,返回的內容爲空時,結果返回 null
,如果不爲空,調用 $.parseJSON
方法,格式化爲 json
格式。相關分析見《讀zepto源碼之工具函數》
如果解釋出錯了,則調用 ajaxError
方法,觸發 ajaxError
事件,事件類型爲 parseerror
。
如果都成功了,則調用 ajaxSuccess
方法,執行成功回調。
響應出錯
ajaxError(xhr.statusText || null, xhr.status ? 'error' : 'abort', xhr, settings, deferred)
如果 status
不在成功的範圍內,則調用 ajaxError
方法,觸發 ajaxError
事件。
響應超時
if (settings.timeout > 0) abortTimeout = setTimeout(function(){
xhr.onreadystatechange = empty
xhr.abort()
ajaxError(null, 'timeout', xhr, settings, deferred)
}, settings.timeout)
如果有設置超時時間,則設置一個定時器,超時時,首先要將 onreadystatechange
的回調設置爲空函數 empty
,避免超時響應執行完畢後,請求完成,再次執行成功回調。
然後調用 xhr.abort
方法,取消請求的發送,並且調用 ajaxError
方法,觸發 ajaxError
事件。
$.get
$.get = function(/* url, data, success, dataType */){
return $.ajax(parseArguments.apply(null, arguments))
}
$.get
是 $.ajax
GET
請求的便捷方法,內部調用了 $.ajax
,不需要指定請求類型。
$.post
$.post = function(/* url, data, success, dataType */){
var options = parseArguments.apply(null, arguments)
options.type = 'POST'
return $.ajax(options)
}
$.post
是 $.ajax
POST
請求的便捷方法,跟 $.get
一樣,只開放了 url
、data
、success
和 dataType
等幾個接口參數,默認配置了 type
爲 POST
請求。
$.getJSON
$.getJSON = function(/* url, data, success */){
var options = parseArguments.apply(null, arguments)
options.dataType = 'json'
return $.ajax(options)
}
$.getJSON
跟 $.get
差不多,比 $.get
更省了一個 dataType
的參數,這裏指定了 dataType
爲 json
類型。
$.fn.load
$.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
}
load
方法是用 ajax
的方式,請求一個 html
文件,並將請求的文件插入到頁面中。
url
可以指定選擇符,選擇符用空格分割,如果有指定選擇符,則只將匹配選擇符的文檔插入到頁面中。url
的格式爲 請求地址 選擇符
。
javascript
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]
parts
是用空格分割後的結果,如果有選擇符,則 length
會大於 1
,數組的第一項爲請求地址,第二項爲選擇符。
調用 parseArguments
用來重新調整參數,因爲 data
和 success
都是可選的。
options.success = function(response){
self.html(selector ?
$('<div>').html(response.replace(rscript, "")).find(selector)
: response)
callback && callback.apply(self, arguments)
}
請求成功後,如果有 selector
,則從文檔中篩選符合的文檔插入頁面,否則,將返回的文檔全部插入頁面。
如果有配置回調函數,則執行回調。
系列文章
- 讀Zepto源碼之代碼結構
- 讀 Zepto 源碼之內部方法
- 讀Zepto源碼之工具函數
- 讀Zepto源碼之神奇的$
- 讀Zepto源碼之集合操作
- 讀Zepto源碼之集合元素查找
- 讀Zepto源碼之操作DOM
- 讀Zepto源碼之樣式操作
- 讀Zepto源碼之屬性操作
- 讀Zepto源碼之Event模塊
- 讀Zepto源碼之IE模塊
- 讀Zepto源碼之Callbacks模塊
- 讀Zepto源碼之Deferred模塊
參考
- Zepto源碼分析-ajax模塊
- 讀zepto源碼(3) ajax
- 你真的會使用XMLHttpRequest嗎?
- 原來你是這樣的 jsonp(原理與具體實現細節)
- 一個普通的 Zepto 源碼分析(二) - ajax 模塊
- MDN:XMLHttpRequest
- fetch.spec.whatwg.org
- HTTP status code 0 - what does this mean for fetch, or XMLHttpRequest?
- (1,eval)(‘this’) vs eval(‘this’) in JavaScript?
- Global eval. What are the options?
License
最後,所有文章都會同步發送到微信公衆號上,歡迎關注,歡迎提意見:
作者:對角另一面