读 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
License: CC BY-NC-ND 4.0

最后,全部文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:

做者:对角另外一面

相关文章
相关标签/搜索