读Zepto源码之Ajax模块

Ajax 模块也是经常会用到的模块,Ajax 模块中包含了 jsonp 的现实,和 XMLHttpRequest 的封装。

读 Zepto 源码系列文章已经放到了github上,欢迎star: reading-zepto

源码版本

本文阅读的源码为 zepto1.2.0

ajax的事件触发顺序

zepto 针对 ajax 的发送过程,定义了以下几个事件,正常情况下的触发顺序如下:

  1. ajaxstart : XMLHttpRequest 实例化前触发
  2. ajaxBeforeSend: 发送 ajax 请求前触发
  3. ajaxSend : 发送 ajax 请求时触发
  4. ajaxSuccess / ajaxError : 请求成功/失败时触发
  5. ajaxComplete: 请求完成(无论成功还是失败)时触发
  6. ajaxStop: 请求完成后触发,这个事件在 ajaxComplete 后触发。

ajax 方法的参数解释

现在还没有讲到 ajax 方法,之所以要将参数提前,是因为后面的内容,不时会用到相关的参数,所以一开始先将参数解释清楚。

  • 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 用来触发一个事件,并且如果该事件禁止浏览器默认事件时,返回 false

参数 context 为上下文,eventName 为事件名,data 为数据。

该方法内部调用了 Event 模块的 trigger 方法,具体分析见《读Zepto源码之Event模块》。

triggerGlobal

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

触发全局事件

settingsajax 配置,context 为指定的上下文对象,eventName 为事件名,data 为数据。

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

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

作者:对角另一面

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容