一个普通的 Zepto 源码分析(二) - ajax 模块

普通的路人,普通地瞧。分析时使用的是目前最新 1.2.0 版本。

Zepto 可以由许多模块组成,默认包含的模块有 zepto 核心模块,以及 event 、 ajax 、 form 、 ie ,其中 ajax 模块是比较重要的模块之一,我们可以借助它提供的方法去做一些网络请求,还可以监听它的生命周期事件。

Zepto 基本模块之 ajax 模块

我们都已经知道 Zepto 插件的一般形式是把 Zepto 对象传入给 $ 形参,那么可以先搜索 $. 开头的代码段,从暴露的函数入手来分析整个代码结构。

代码结构与分析

删减出外形:

;(function($){
// Number of active Ajax requests
$.active = 0 $.ajaxJSONP = function(options, deferred){...} $.ajaxSettings = {...} $.ajax = function(options){...} $.get = function(/* url, data, success, dataType */){...} $.post = function(/* url, data, success, dataType */){...} $.getJSON = function(/* url, data, success */){...} $.fn.load = function(url, data, success){...} $.param = function(obj, traditional){...}
})(Zepto)

另由静态分析可知:

  1. $.get()$.post()$.getJSON()$.fn.load() 均调用了 $.ajax()parseArguments()说明 $.ajax() 才是我们主要分析的目标,后者则是处理函数参数的关键;
  2. $.param()$.ajax()$.ajaxJSONP() 均调用了 $.isFunction() ,这个倒是没有什么好纠结的,就是用了 Zepto 核心定义的一个判断传入参数是否为函数的函数;
  3. $.ajax() 操作、返回的是一个原生的 xhr 对象,调用了很多 ajax 开头的内部函数来完成生命周期的控制封装。

参数规格化与 MIME

先来看看 parseArguments() 都干了些什么:

  // handle optional data/success arguments
function parseArguments(url, data, success, dataType) {
// 参数重载
if ($.isFunction(data)) dataType = success, success = data, data = undefined
if (!$.isFunction(success)) dataType = success, success = undefined
// 返回规格化对象
return {
url: url
, data: data
, success: success
, dataType: dataType
}
}

它的参数覆盖了我们之前提到的四个调用者的参数。

在前两行我们可以看到,它做了一个顺移来完成对重载调用格式的支持。比如 $.get(url, function(data, status, xhr){ ... }) 。这个是简单判断参数是否为函数来完成的,有两个缺点,一是会重复判断 success ,二是当只传两个参数时会做冗余赋值。

那么这个函数的作用就是参数规格化。然而.. 在 Zepto 文档上并没有看到对 dataType 的说明,略坑?

我们已知 $.ajaxSettings 里有一个 accepts 属性,文档上说是根据 dataType 来请求服务器的,而代码注释里则说这是一个 Mapping ;另外根据对 $.ajax() 的静态分析,我们还有一个 mimeToDataType() ,它根据输入的 MIME 字符串来输出内部定义的 dataType :

  var scriptTypeRE = /^(?:text|application)\/javascript/i,
xmlTypeRE = /^(?:text|application)\/xml/i,
jsonType = 'application/json',
htmlType = 'text/html' $.ajaxSettings = {
// MIME types mapping
// IIS returns Javascript as "application/x-javascript"
accepts: {
script: 'text/javascript, application/javascript, application/x-javascript',
json: jsonType,
xml: 'application/xml, text/xml',
html: htmlType,
text: 'text/plain'
}
} function mimeToDataType(mime) {
if (mime) mime = mime.split(';', 2)[0]
return mime && ( mime == htmlType ? 'html' :
mime == jsonType ? 'json' :
scriptTypeRE.test(mime) ? 'script' :
xmlTypeRE.test(mime) && 'xml' ) || 'text'
}

其中 mime.split(';', 2) 限定了只能用一个分号分成两部分,但我质疑它的效果.. 显然限定为 1 是更好的。

get 与 post

接下来就可以来看 get/post 方法了:

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

嗯,没什么好分析的, apply 也是很常见的用法。但是我们确定之前是没有 type 属性的,那么可以猜测 $.ajax() 还会对 options 作进一步处理,比如合并 $.ajaxSettings 中的设置等等。

load() 函数

这是挂到原型上的,我们已知 Zepto 调用原型函数前都会把自己弄成一个类数组,也就是自己定义的集合 Collection 。

文档上说这个方法可以给一个集合的元素用 GET Ajax 加载给定 URL 的 HTML 内容,还可以同时指定一个 CSS 选择器,使其只加载符合这个选择器的内容。而指定了选择器以后,加载内容中的 script 则不会被执行。来看看是怎么做的:

  $.fn.load = function(url, data, success){
if (!this.length) return this
var self = this, parts = url.split(/\s/), selector,
options = parseArguments(url, data, success),
callback = options.success
if (parts.length > 1) options.url = parts[0], selector = parts[1]
options.success = function(response){
self.html(selector ?
$('<div>').html(response.replace(rscript, "")).find(selector)
: response)
callback && callback.apply(self, arguments)
}
$.ajax(options)
return this
}

这个 callback 操作好像挺迷的,前面多传了 success 进去,多做了一次赋值。

同样也没多少好分析的,就是给 Ajax 添加了一个成功回调,用来设置元素的内容,并代理了传入的回调。至于 .find() 是跟 jQuery 一样的实现,当在一个集合上调用时,就筛出元素。

param() 函数

这个函数的扇入扇出也是比较少的,可以先分析。那么这个方法也是一个序列化函数,可以把一个(狭义的)对象序列化成编码 URL 字符串,当然也可以接收一个数组,但只接收 serializeArray 格式的。

  var escape = encodeURIComponent

  function serialize(params, obj, traditional, scope){
var type, array = $.isArray(obj), hash = $.isPlainObject(obj)
$.each(obj, function(key, value) {
type = $.type(value)
// 关注点 4 (递归进来)
if (scope) key = traditional ? scope :
scope + '[' + (hash || type == 'object' || type == 'array' ? key : '') + ']'
// 关注点 2 (初始入口)
// handle data in serializeArray() format
if (!scope && array) params.add(value.name, value.value)
// 关注点 3
// recurse into nested objects
else if (type == "array" || (!traditional && type == "object"))
serialize(params, value, traditional, key)
else params.add(key, value)
})
} $.param = function(obj, traditional){
var params = []
// 关注点 2
params.add = function(key, value) {
if ($.isFunction(value)) value = value()
if (value == null) value = ""
this.push(escape(key) + '=' + escape(value))
}
// 关注点 1
serialize(params, obj, traditional)
return params.join('&').replace(/%20/g, '+')
}

这次就不从 serialize() 开始看了,当然是从短的开始看啦!我说的短,不是代码有多少行,而是除去赋值操作后,把字面量压成一行后等等,还能剩下多短的结构。

那么我们可以看到 $.param() 里做了一个临时数组 params 用于存放每个键值对的序列化结果,最后 join 到一起做修补替换。关键点在 add() 函数,如果 value 是一个函数,则调用并获得其返回值。但是.. 如果其返回值或者本来就是 nullundefined 应该返回空值吗?这点我不敢苟同,就算不会解析错误,空键不如干脆不要。至于改写 escape 看起来也没什么必要.. 最多就是提醒写插件的开发者.. 直接调用就好了哇..

接下来就是看起来很长的 serialize() 函数啦。初步目测是一个递归,用于处理嵌套情况。那么三个局部变量一个是拿到小写的常见类型名,后两个是布尔值,相信不陌生。文件内搜索发现只有来自 $.param() 的引用,那么可以断定第二个 if 才是初始入口,这里是处理 serializeArray 的键值对象格式。而如果是普通对象 k-v 对的值是数组或对象的话,就进入递归调用把子结构也序列化,否则直接把 k-v 对加入 params 数组中。

要注意的是,如果设置为传统的浅序列化模式,嵌套对象值会被无情抛弃成 [object Object] 也就是 %5Bobject+Object%5D 。而数组的 key 则是不带方括号的表示形式,在 Zepto 上是无论嵌套多少层数组,都会处理成同 key 而不同 value 的多个键值对,但 jQuery 更新了其实现,它是无论嵌套多少层放在同一个键值对中,用英文逗号隔开,如下:

decodeURIComponent($.param({a:1,b:[1,[2,22,[3,33,[4]]],5]},true))
// jQuery, "a=1&b=1&b=2,22,3,33,4&b=5"
// Zepto, "a=1&b=1&b=2&b=22&b=3&b=33&b=4&b=5"

至于带方括号的非传统模式实现也比较简单,每次递归更新 key 就好了。

Ajax 生命周期及事件

一共 7 个,都可以在官方文档找到说明的。其中 ajaxStartajaxStop 事件只有设置为 global: true 才会在 document 上被激发,其余则都是全局事件,在 document 或指定 DOM 节点上激发并冒泡。至于怎么捕获事件,相信熟悉的人都不陌生(好像是废话)

  // trigger a custom event and return false if it was cancelled
function triggerAndReturn(context, eventName, data) {
var event = $.Event(eventName)
$(context).trigger(event, data)
return !event.isDefaultPrevented()
} // trigger an Ajax "global" event
function triggerGlobal(settings, context, eventName, data) {
if (settings.global) return triggerAndReturn(context || document, eventName, data)
}
// 关注点 1
// Number of active Ajax requests
$.active = 0 function ajaxStart(settings) {
// 关注点 2
if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart')
}
function ajaxStop(settings) {
// 关注点 2
if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
}

维护了一个 active 变量,在第一次发起 Ajax 或最后一次结束中被检查为 0 而触发事件,若事件没有被抑制则开始冒泡。没有用设计模式,应该也没必要。

  // triggers an extra global event "ajaxBeforeSend" that's like "ajaxSend" but cancelable
function ajaxBeforeSend(xhr, settings) {
var context = settings.context
if (settings.beforeSend.call(context, xhr, settings) === false ||
triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false)
return false triggerGlobal(settings, context, 'ajaxSend', [xhr, settings])
}
function ajaxSuccess(data, xhr, settings, deferred) {
var context = settings.context, status = 'success'
settings.success.call(context, data, status, xhr)
if (deferred) deferred.resolveWith(context, [data, status, xhr])
triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data])
ajaxComplete(status, xhr, settings)
}
// type: "timeout", "error", "abort", "parsererror"
function ajaxError(error, type, xhr, settings, deferred) {
var context = settings.context
settings.error.call(context, xhr, type, error)
if (deferred) deferred.rejectWith(context, [xhr, type, error])
triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type])
ajaxComplete(type, xhr, settings)
}
// status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
function ajaxComplete(status, xhr, settings) {
var context = settings.context
settings.complete.call(context, xhr, status)
triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings])
ajaxStop(settings)
}

剩下的 5 个事件在 4 个生命时期被激发。可以看到在 ajaxBeforeSend 里允许回调或事件被抑制,这时就会返回 false 进一步取消该 Ajax 。否则就触发 ajaxSend 事件了——不过显然,这个时候其实还没有真正地 send 出去,只是先激活了事件。同时我们也能看到,无论 Ajax 请求成功还是失败,最终都触发完成事件,最后“标志性”地终止——当它是最后一个 Ajax 时就会触发 ajaxStop 事件。

此外我们还可以知道, Ajax 回调是先于事件发生的;而如果是 Promise ,那么只有当 ajaxError 时才会 reject 。

$.ajax() 函数分析

终于到了重头戏了。至于剩余的其他边角函数可以一眼扫光,用到再说吧~

其实大部分代码都用来处理 settings 了,然而还是可以大致分为几部分的。

配置项的合并

  $.ajax = function(options){
var settings = $.extend({}, options || {}),
deferred = $.Deferred && $.Deferred(),
urlAnchor, hashIndex
for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key] ajaxStart(settings)
...
}

首先是浅复制 options 参数,接着继承 $.ajaxSettings 中的属性。同样是浅复制,后者不能覆盖用户传入的 options 参数.. 不过..

    var settings = $.extend({}, $.ajaxSettings)
settings = $.extend(settings, options || {})

总感觉这是一样的,哈哈。毕竟 for...in 能检出原型上的属性,而反正 $.extend() 浅复制时内部实现也是纯 for...in ,好像没毛病。要是支持 ES5 的话直接 Object.create() 好像也.. 没毛病?

完成了设置项的初始化后,激发 ajaxStart 事件,开始做进一步的处理..

配置项的处理

  var originAnchor = document.createElement('a')
// 关注点 1
originAnchor.href = window.location.href $.ajax = function(options){
...
if (!settings.crossDomain) {
// 关注点 3
urlAnchor = document.createElement('a')
urlAnchor.href = settings.url
// cleans up URL for .href (IE only), see https://github.com/madrobby/zepto/pull/1049
urlAnchor.href = urlAnchor.href
// 关注点 2 (自动处理出的 protocol 和 host)
settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host)
}
...
}

对跨域属性的处理。这里有个特殊技巧,就是给 a 标签修改 href 属性后,浏览器会帮我们自动处理出 protocolhost 属性,这对判断是否跨域很有用,且不用调用冗长的解析库。

我们知道不跨域的标准是协议相同、主机地址/域名相同、端口号相同,而有人发现在 IE 且 80 端口下需要赋值完整地址才会把 host 解析出来,于是多了一个自赋值的 PR 。

  function appendQuery(url, query) {
if (query == '') return url
// 关注点 3
return (url + '&' + query).replace(/[&?]{1,2}/, '?')
} // serialize payload and append it to the URL for GET requests
function serializeData(options) {
// 关注点 4 (序列化 data 对象)
if (options.processData && options.data && $.type(options.data) != "string")
options.data = $.param(options.data, options.traditional)
// 关注点 5 (默认 GET 的 url 处理)
if (options.data && (!options.type || options.type.toUpperCase() == 'GET' || 'jsonp' == options.dataType))
options.url = appendQuery(options.url, options.data), options.data = undefined
} $.ajax = function(options){
...
// 关注点 1
if (!settings.url) settings.url = window.location.toString()
// 关注点 2
if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex)
serializeData(settings)
...
}

url 属性的处理。 window.location 的使用是 DOM 基础知识了,前面没用我猜是为了保持一致性?(逃

这里暂时不知道不保留 # 号后部分的作用,只知道 WHATWG 定义其为 URL-fragment ,没有很特别的说明,也许有时间要看看 Node 的解析说明。

搞定了 url 后就可以序列化数据了。根据调用关系, appendQuery() 有 4 个扇入,唯一亮点就是每次把第一个出现的 & 或 ? 替换成 ? 。我认为这个实现是基于传入 url 是 / 结尾的假设的,那么其实判断最后一个字符来决定使用 & 或 ? 应当比查找要好很多。至于 serializeData 就是两种情况,如果提供的 options.data 不是一个字符串且需要自动序列化,那么就调用之前提到的 $.param() 进行序列化,否则如果是 jsonp 或者默认 GET 则处理进 options.url 里。

    // 关注点 1
var dataType = settings.dataType, hasPlaceholder = /\?.+=\?/.test(settings.url)
if (hasPlaceholder) dataType = 'jsonp'
// 关注点 2 (要不要缓存的判断与处理)
if (settings.cache === false || (
(!options || options.cache !== true) &&
('script' == dataType || 'jsonp' == dataType)
))
settings.url = appendQuery(settings.url, '_=' + Date.now())
// 关注点 3 ( jsonp 的判断与处理)
if ('jsonp' == dataType) {
if (!hasPlaceholder)
settings.url = appendQuery(settings.url,
settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?')
return $.ajaxJSONP(settings, deferred)
}

根据 dataType 对缓存和 jsonp 的处理,也算一小段吧。不使用缓存的处理好理解,就是常见的加入时间参数。

hasPlaceholder 则是测试(贪婪匹配)最后一个键值对的值(即 url 中的 callbackName )是否为 placeholder 即 ? 符。这个实现很奇怪,已经不符合现在的 jQuery 了,现在似乎是不能只在 url 指定 =? 的,必须设置 dataType: jsonp 才行。另外先补 url 再替换似乎也有些低效。

原生 xhr 对象的 header 设置

首先是对 request header 的设置:

    var mime = settings.accepts[dataType],
headers = { }, /* 关注点 1 (暂存 header ) */
setHeader = function(name, value) { headers[name.toLowerCase()] = [name, value] },
xhr = settings.xhr(),
nativeSetHeader = xhr.setRequestHeader if (deferred) deferred.promise(xhr) if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest')
// 关注点 2
setHeader('Accept', mime || '*/*')
// 关注点 3 (注意优先级)
if (mime = settings.mimeType || mime) {
if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0]
xhr.overrideMimeType && xhr.overrideMimeType(mime)
}
//关注点 4
if (settings.contentType || (settings.contentType !== false && settings.data && settings.type.toUpperCase() != 'GET'))
setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded')
//关注点 4
if (settings.headers) for (name in settings.headers) setHeader(name, settings.headers[name])
xhr.setRequestHeader = setHeader

注意到在 v1.1.1 的 commit 记录里写到,为了支持在 beforeSend 生命期中(这个时候 xhr 对象还没 open )调用 xhr.setRequestHeader() 修改 header ,用自定义的 setHeader() 函数暂存下来,到实际要操作打开 xhr 对象时再去调用原生方法设置 header 。

注意到这一句 setHeader('Accept', mime || '*/*') , MDN 上是这么说的:

If no Accept header has been set using this, an Accept header with the */* is sent with the request when send() is called.

因此我认为可以改成 mime && setHeader('Accept', mime)

而紧接着的 if 具有一定的迷惑性,它其实是要用 accepts[dataType] 或者 mimeType 来重写响应头里的 MIME (赋值的优先级较低,其实完全可以拿出来赋值)。再下来就是针对非 GET 而又有上传数据的请求,将 Content-Type 改为 POST 格式。再下来就是存下自定义的 header 并重写方法了,可以看到自定义 header 会覆盖 Zepto 的默认值。

发送 xhr

  $.ajax = function(options){
...
xhr.onreadystatechange = function(){...}
// 关注点 1
if (ajaxBeforeSend(xhr, settings) === false) {
xhr.abort()
ajaxError(null, 'abort', xhr, settings, deferred)
return xhr
}
// 关注点 2 (打开 xhr 对象)
var async = 'async' in settings ? settings.async : true
xhr.open(settings.type, settings.url, async, settings.username, settings.password) if (settings.xhrFields) for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name] for (name in headers) nativeSetHeader.apply(xhr, headers[name])
// 关注点 3 (超时的后续处理)
if (settings.timeout > 0) abortTimeout = setTimeout(function(){
xhr.onreadystatechange = empty
xhr.abort()
ajaxError(null, 'timeout', xhr, settings, deferred)
}, settings.timeout)
// 关注点 4
// avoid sending empty string (#319)
xhr.send(settings.data ? settings.data : null)
return xhr
}

先不管 onreadystatechange 回调,里面只有一个完成状态的判断。

这里终于跑到了第二个生命期,准备工作已经做好,触发可以被取消的 ajaxBeforeSend 事件,接着就是打开 xhr 了。这里有一个点是超时的处理,把 onreadystatechange 回调设置为空我认为是一个收尾工作,比如 $.ajax() 返回的 xhr 对象也可以重新打开,这时候显然不希望还是原来的回调。另外不使用原生超时事件的原因应该是 Android 4.4 的浏览器还不支持。

最后 xhr.send() 注释了对 #319 的修补。这个 issue 的大意是当在 Chrome 上 POST 的数据为空字符串时(经过上面的处理,传入的 data 变为了 undefined ),会触发一个 CORS 错误。应该是 11 年 Chrome 上的 BUG ,现在我无法复现了。

onreadystatechange() 回调

;(function($){
var blankRE = /^\s*$/
...
xhr.onreadystatechange = function(){
if (xhr.readyState == 4) {
xhr.onreadystatechange = empty
clearTimeout(abortTimeout)
var result, error = false
// 关注点 1 (正常状态码的判断)
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) {
dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'))
// 关注点 2 (响应数据流)
if (xhr.responseType == 'arraybuffer' || xhr.responseType == 'blob')
result = xhr.response
else {
result = xhr.responseText
// 关注点 3 ( evel() 的间接调用与数据类型判断)
try {
// http://perfectionkills.com/global-eval-what-are-the-options/
// sanitize response accordingly if data filter callback provided
result = ajaxDataFilter(result, dataType, settings)
if (dataType == 'script') (1,eval)(result)
else if (dataType == 'xml') result = xhr.responseXML
else if (dataType == 'json') result = blankRE.test(result) ? null : $.parseJSON(result)
} catch (e) { error = e } if (error) return ajaxError(error, 'parsererror', xhr, settings, deferred)
} ajaxSuccess(result, xhr, settings, deferred)
} else {
ajaxError(xhr.statusText || null, xhr.status ? 'error' : 'abort', xhr, settings, deferred)
}
}
}
...
}

对于本地文件(即 file: 协议头)浏览器的状态码会是 0 。而如果是浏览器取消了 xhr 请求则触发 abort 类型的 ajaxError 事件,如宿主机的网络连接变化 / 中断了等。

有意思的点是 responseType 属性。两种类型我都没见过,据 MDN 是用于二进制数据传输, 由 .response 返回相应的对象。

一个奇怪的技巧是如代码注释所示,使用间接调用的形式 (1,eval)() 来避免污染外层作用域。再吐个槽,对 dataType 的赋值可以放进 try 块里的。

$.ajaxJSONP() 函数

接下来看下最后一个函数和 $.ajax() 相比有哪些不同。在文档上标为废弃,实际上是不建议直接使用。而在上面的代码我们也看到 $.ajaxJSONP()$.ajax() 中的调用是发生在事件 ajaxStart 事件之后、配置项合并完成后、设置 header 之前的。

;(function($){
var jsonpID = +new Date()
... $.ajaxJSONP = function(options, deferred){
if (!('type' in options)) return $.ajax(options)
// 关注点 2 (回调函数名的处理)
var _callbackName = options.jsonpCallback,
callbackName = ($.isFunction(_callbackName) ?
_callbackName() : _callbackName) || ('Zepto' + (jsonpID++)),
script = document.createElement('script'),
originalCallback = window[callbackName],
responseData,
abort = function(errorType) {
$(script).triggerHandler('error', errorType || 'abort')
}, /* 关注点 1 (只有取消方法的 xhr 对象) */
xhr = { abort: abort }, abortTimeout if (deferred) deferred.promise(xhr) $(script).on('load error', function(e, errorType){...}) if (ajaxBeforeSend(xhr, options) === false) {
abort('abort')
return xhr
}
// 关注点 3 (对自动回调的代理包装,先拿到数据)
window[callbackName] = function(){
responseData = arguments
}
// 关注点 4 (最后一个 xxx=? 的替换)
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
document.head.appendChild(script) if (options.timeout > 0) abortTimeout = setTimeout(function(){
abort('timeout')
}, options.timeout) return xhr
}
...
}

这里的 xhr 对象就不是真正的 XMLHttpRequest 实例了,而是一个只带 abort() 方法的 mock 对象。

首先关注 callbackName ,允许字符串,也允许由一个函数返回,或由 Zepto 指定一个值。根据 Zepto 的 make 脚本,几个插件是简单连接到一起的,可以认为这个 jsonpID 的初始值大概是加载执行 Zepto 的时间,有一定的随机效果(时间一般不可逆),且每次自增(原单位是毫秒)保证了每次都会拿到最新版本而不是缓存。

另外有一个 originalCallback 的处理,挂钩我们原来的回调函数,先对响应数据做空值判断,再传回来。当然如果我们没有回调函数,那么就只能在 ajaxSuccess 中得到数据了,因为 Zepto 生成的“函数名”本质上只是个字符串而已。

    $(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)
}
// 关注点 1 (自动生成的回调名只是字符串)
window[callbackName] = originalCallback
if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0])
// 关注点 2 (收尾)
originalCallback = responseData = undefined
})

abort() 函数会触发一次 error 事件,而 $(script).off().remove() 不论如何都会移除之前创建的 script 元素(当然也先解绑了回调函数本身)。

系列相关

一个普通的 Zepto 源码分析(一) - ie 与 form 模块

一个普通的 Zepto 源码分析(二) - ajax 模块

一个普通的 Zepto 源码分析(三) - event 模块

本文基于 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 发布,欢迎引用、转载或演绎,但是必须保留本文的署名 BlackStorm 以及本文链接 http://www.cnblogs.com/BlackStorm/p/Zepto-Analysing-For-Ajax-Module.html ,且未经许可不能用于商业目的。如有疑问或授权协商请 与我联系

一个普通的 Zepto 源码分析(二) - ajax 模块的更多相关文章

  1. 一个普通的 Zepto 源码分析(三) - event 模块

    一个普通的 Zepto 源码分析(三) - event 模块 普通的路人,普通地瞧.分析时使用的是目前最新 1.2.0 版本. Zepto 可以由许多模块组成,默认包含的模块有 zepto 核心模块, ...

  2. 一个普通的 Zepto 源码分析(一) - ie 与 form 模块

    一个普通的 Zepto 源码分析(一) - ie 与 form 模块 普通的路人,普通地瞧.分析时使用的是目前最新 1.2.0 版本. Zepto 可以由许多模块组成,默认包含的模块有 zepto 核 ...

  3. jQuery 源码分析(二) 入口模块

    jQuery返回的对象本质上是一个JavaScript对象,而入口模块则可以保存对应的节点的引用,然后供其它模块操作 我们创建jQuery对象时可以给jQuery传递各种不同的选择器,如下: fals ...

  4. [Abp 源码分析]二、模块系统

    0.简介 整个 Abp 框架由各个模块组成,基本上可以看做一个程序集一个模块,不排除一个程序集有多个模块的可能性.可以看看他官方的这些扩展库: 可以看到每个项目文件下面都会有一个 xxxModule ...

  5. Zepto源码分析(二)奇淫技巧总结

    Zepto源码分析(一)核心代码分析 Zepto源码分析(二)奇淫技巧总结 目录 * 前言 * 短路操作符 * 参数重载(参数个数重载) * 参数重载(参数类型重载) * CSS操作 * 获取属性值的 ...

  6. Zepto源码分析(一)核心代码分析

    本文只分析核心的部分代码,并且在这部分代码有删减,但是不影响代码的正常运行. 目录 * 用闭包封装Zepto * 开始处理细节 * 正式处理数据(获取选择器选择的DOM) * 正式处理数据(添加DOM ...

  7. Fresco 源码分析(二) Fresco客户端与服务端交互(1) 解决遗留的Q1问题

    4.2 Fresco客户端与服务端的交互(一) 解决Q1问题 从这篇博客开始,我们开始讨论客户端与服务端是如何交互的,这个交互的入口,我们从Q1问题入手(博客按照这样的问题入手,是因为当时我也是从这里 ...

  8. zepto源码分析系列

    如果你也开发移动端web,如果你也用zepto,应该值得你看看.有问题请留言. Zepto源码分析-架构 Zepto源码分析-zepto(DOM)模块 Zepto源码分析-callbacks模块 Ze ...

  9. 框架-springmvc源码分析(二)

    框架-springmvc源码分析(二) 参考: http://www.cnblogs.com/leftthen/p/5207787.html http://www.cnblogs.com/leftth ...

随机推荐

  1. SVN仓库迁移到Git遇到的两个问题和解决办法

    OS: CentOS 7.0 准备: git svn git-svn sudo yum install git sudo yum install subversion sudo yum install ...

  2. php的修改

    修改数据: (1)如果不是有外键的表,那么修改就正常的进行修改页面和修改处理页面就可以,但是有了外键的表,在进行遍历时就要写相应的条件了. 例如,有张表中有性别,但是进行创建表时是用的0或1来表示的, ...

  3. 使用asp.net mvc部分视图渲染html

    为了提升用户体验,一般我们采用ajax加载数据然后根据数据渲染html,渲染html可以使用前端渲染和服务器端渲染. 前端渲染 使用前端模版引擎或MVC框架,例如underscore.js的templ ...

  4. Python3中的模块

    模块使用哪种语言实现并不重要,因为所有的模块导入与使用的方式都相同. 1.常用模块导入格式: import importable1,importable2,... import importable ...

  5. Android源码博文集锦1

    Android精选源码 功能齐全的实用Android视频播放器 实现滑动到底部,加载更多RecyclerViewDemo 使用Kotlin语言编写Android MVP案例KotlinMVPDemo ...

  6. 恐怖的ifdown eth0;0

    下午闲的蛋疼,随手给测试机配了个浮动地址eth0:0. ping了下OK,内网访问没问题. 准备收手的时候,瞄了一眼ifcfg-eth0:0的配置,发现广播地址BROADCAST写成了BOADCAST ...

  7. SerializableObj

    package JBJADV003; import java.io.*; public class SerializableObj { /** * @param args * @throws IOEx ...

  8. Java自学手记——泛型

    泛型在集合中的应用 泛型在集合经常能看到,有两个好处:1.把运行时出现 的问题提前至了编译时:2.避免了无谓的强制类型转换. 用法:两边泛型的类型必须相同,可允许一边不写,只是为了兼容性,并不推荐. ...

  9. mac版破解office

    下载地址:http://ereach-public.oss-cn-shanghai.aliyuncs.com/office%202016%20for%20mac.dmg 解压密码:www.ifunma ...

  10. es6的模块化编程

    es6的模块化编程 基本用法 es6 中新增了两个命令 export 和 import , export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能. 一个模块就是一个独 ...