讀Zepto源碼之Ajax模塊

Ajax 模塊也是常常會用到的模塊,Ajax 模塊中包含了 jsonp 的現實,和 XMLHttpRequest 的封裝。 javascript

讀 Zepto 源碼系列文章已經放到了github上,歡迎star: reading-zeptohtml

源碼版本

本文閱讀的源碼爲 zepto1.2.0前端

ajax的事件觸發順序

zepto 針對 ajax 的發送過程,定義瞭如下幾個事件,正常狀況下的觸發順序以下:java

  1. ajaxstart : XMLHttpRequest 實例化前觸發
  2. ajaxBeforeSend: 發送 ajax 請求前觸發
  3. ajaxSend : 發送 ajax 請求時觸發
  4. ajaxSuccess / ajaxError : 請求成功/失敗時觸發
  5. ajaxComplete: 請求完成(不管成功仍是失敗)時觸發
  6. ajaxStop: 請求完成後觸發,這個事件在 ajaxComplete 後觸發。

ajax 方法的參數解釋

如今尚未講到 ajax 方法,之因此要將參數提早,是由於後面的內容,不時會用到相關的參數,因此一開始先將參數解釋清楚。git

  • typeHTTP 請求的類型;
  • url: 請求的路徑;
  • data: 請求參數;
  • processData: 是否須要將非 GET 請求的參數轉換成字符串,默認爲 true ,即默認轉換成字符串;
  • contentType: 設置 Content-Type 請求頭;
  • mineType : 覆蓋響應的 MIME 類型,能夠是 jsonjsonpscriptxmlhtml、 或者 text
  • jsonp: jsonp 請求時,攜帶回調函數名的參數名,默認爲 callback
  • jsonpCallbackjsonp 請求時,響應成功時,執行的回調函數名,默認由 zepto 管理;
  • timeout: 超時時間,默認爲 0
  • headers:設置 HTTP 請求頭;
  • async: 是否爲同步請求,默認爲 false
  • global: 是否觸發全局 ajax 事件,默認爲 true
  • context: 執行回調時(如 jsonpCallbak)時的上下文環境,默認爲 window
  • traditional: 是否使用傳統的淺層序列化方式序列化 data 參數,默認爲 false,例若有 data{p1:'test1', p2: {nested: 'test2'} ,在 traditionalfalse 時,會序列化成 p1=test1&p2[nested]=test2, 在爲 true 時,會序列化成 p1=test&p2=[object+object]
  • xhrFieldsxhr 的配置;
  • cache:是否容許瀏覽器緩存 GET 請求,默認爲 false
  • username:須要認證的 HTTP 請求的用戶名;
  • password: 須要認證的 HTTP 請求的密碼;
  • dataFilter: 對響應數據進行過濾;
  • xhrXMLHttpRequest 實例,默認用 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 用來觸發一個事件,而且若是該事件禁止瀏覽器默認事件時,返回 falsegithub

參數 context 爲上下文,eventName 爲事件名,data 爲數據。ajax

該方法內部調用了 Event 模塊的 trigger 方法,具體分析見《讀Zepto源碼之Event模塊》。正則表達式

triggerGlobal

function triggerGlobal(settings, context, eventName, data) {
  if (settings.global) return triggerAndReturn(context || document, eventName, data)
}

觸發全局事件chrome

settingsajax 配置,context 爲指定的上下文對象,eventName 爲事件名,data 爲數據。json

triggerGlobal 內部調用的是 triggerAndReturn 方法,若是有指定上下文對象,則在指定的上下文對象上觸發,不然在 document 上觸發。

ajaxStart

function ajaxStart(settings) {
  if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart')
}

觸發全局的 ajaxStart 事件。

若是 global 設置爲 true,則 $.active 的值增長1。

若是 globaltrue ,而且 $.active 在更新前的數量爲 0,則觸發全局的 ajaxStart 事件。

ajaxStop

function ajaxStop(settings) {
  if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
}

觸發全局 ajaxStop 事件。

若是 globaltrue ,則將 $.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 和當前狀態 statusxhr 做爲回調函數的參數。

若是 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 事件,錯誤的類型能夠爲 timeouterrorabortparsererror

在觸發事件前,調用配置中的 error 方法,將 xhr 實例,錯誤類型 typeerror 對象做爲回調函數的參數。

隨後調用 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/javascriptxmlTypeRE 匹配的是 text/xml 或者 application/xml, 都還比較簡單,不做過多的解釋。

Content-Type 的值的形式以下 text/html; charset=utf-8, 因此若是參數 mime 存在,則用 ; 分割,取第一項,這裏是 text/html,即爲包含類型的字符串。

接下來是針對 htmljsonscriptxml 用對應的正則進行匹配,匹配成功,返回對應的類型值,若是都不匹配,則返回 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 的爲數組,則 arraytrue, 若是爲純粹對象,則 hashtrue$.isArray$.isPlainObject 的源碼分析見《讀Zepto源碼以內部方法》。

遍歷須要序列化的對象 obj,判斷 value 的類型 type, 這個 type 後面會用到。

scope 是記錄深層嵌套時的 key 值,這個 key 值受 traditional 的影響。

若是 traditionaltrue ,則 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
}

序列化參數。

若是 processDatatrue ,而且參數 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 賦值爲空字符串。

而後將 keyvalueencodeURIComponent 編碼,用 = 號鏈接起來。

接着即是簡單的調用 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 是根據配置得出的回調函數名。若是 _callbackNamefunction ,則以執行的結果做爲回調函數名,若是 _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 爲所傳遞配置的副本。

deferreddeferred 對象。

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 屬性設置爲當前請求的地址,而後獲取節點的 protocolhost,看跟當前頁面的連接用一樣方式拼接出來的地址是否一致。

注意到這裏的 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 ,或者在 dataTypescript 或者 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.jsonpfalse, 則不向 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

xhrXMLHttpRequest 實例。

nativeSetHeaderxhr 實例上的 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 方法來重寫 responsecontent-type ,使得服務端返回的類型跟客戶端要求的類型不一致時,能夠按照指定的格式來解釋。具體能夠參見這篇文章 《你真的會使用XMLHttpRequest嗎?》。

若是有指定 contentType

或者 contentType 沒有設置爲 false ,而且 data 存在以及請求類型不爲 GET 時,設置 Content-Type 爲指定的 contentType ,在沒有指定時,設置爲 application/x-www-form-urlencoded 。因此沒有指定 contentType 時, POST 請求,默認的 Content-Typeapplication/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:'

status0 時,表示請求並無到達服務器,有幾種狀況會形成 status0 的狀況,例如網絡不通,不合法的跨域請求,防火牆攔截等。

直接用本地文件的方式打開,也會出現 status0 的狀況,可是我在 chrome 上測試,在這種狀況下只能取到 statusresponseTyperesponseText 都取不到,不清楚這個用本地文件打開時,進入成功判斷的目的何在。

處理數據
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 來判斷得到的數據類型,進而調用不一樣的方法來處理。

若是數據爲 arraybufferblob 對象時,即爲二進制數據時,resultresponse 中直接取得。

不然,用 responseText 獲取數據,而後再對數據嘗試解釋。

在解釋數據前,調用 ajaxDataFilter 對數據進行過濾。

若是數據類型爲 script ,則使用 eval 方法,執行返回的 script 內容。

這裏爲何用 (1, eval) ,而不是直接用 eval 呢,是爲了確保 eval 執行的做用域是在 window 下。具體參考:(1,eval)('this') vs eval('this') in JavaScript? 和 《Global eval. What are the options?

若是 dataTypexml ,則調用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 同樣,只開放了 urldatasuccessdataType 等幾個接口參數,默認配置了 typePOST 請求。

$.getJSON

$.getJSON = function(/* url, data, success */){
  var options = parseArguments.apply(null, arguments)
  options.dataType = 'json'
  return $.ajax(options)
}

$.getJSON$.get 差很少,比 $.get 更省了一個 dataType 的參數,這裏指定了 dataTypejson 類型。

$.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 的格式爲 請求地址 選擇符

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 用來從新調整參數,由於 datasuccess 都是可選的。

options.success = function(response){
  self.html(selector ?
            $('<div>').html(response.replace(rscript, "")).find(selector)
            : response)
  callback && callback.apply(self, arguments)
}

請求成功後,若是有 selector ,則從文檔中篩選符合的文檔插入頁面,不然,將返回的文檔所有插入頁面。

若是有配置回調函數,則執行回調。

系列文章

  1. 讀Zepto源碼之代碼結構
  2. 讀 Zepto 源碼以內部方法
  3. 讀Zepto源碼之工具函數
  4. 讀Zepto源碼之神奇的$
  5. 讀Zepto源碼之集合操做
  6. 讀Zepto源碼之集合元素查找
  7. 讀Zepto源碼之操做DOM
  8. 讀Zepto源碼之樣式操做
  9. 讀Zepto源碼之屬性操做
  10. 讀Zepto源碼之Event模塊
  11. 讀Zepto源碼之IE模塊
  12. 讀Zepto源碼之Callbacks模塊
  13. 讀Zepto源碼之Deferred模塊

參考

License

License: CC BY-NC-ND 4.0

最後,全部文章都會同步發送到微信公衆號上,歡迎關注,歡迎提意見:

做者:對角另外一面

相關文章
相關標籤/搜索