zepto源码分析·ajax模块

时间:2023-03-10 06:50:17
zepto源码分析·ajax模块

准备知识

在看ajax实现的时候,如果对ajax技术知识不是很懂的话,可以参看下ajax基础,以便读分析时不会那么迷糊

全局ajax事件

默认$.ajaxSettings设置中的global为true,因此在Ajax请求的生命周期内,这些事件将被触发:

ajaxStart:如果没有其他Ajax请求当前活跃将会被触发
ajaxBeforeSend:再发送请求前,可以被取消
ajaxSend:像 ajaxBeforeSend,但不能取消
ajaxSuccess:当返回成功时
ajaxError:当有错误时
ajaxComplete:请求已经完成后,无论请求是成功或者失败
ajaxStop:如果这是最后一个活跃着的Ajax请求,将会被触发

默认情况下,ajax事件在document对象上触发;然而如果请求的context是一个DOM节点,该事件会在此节点上触发然后再DOM中冒泡;唯一的例外是ajaxStart和ajaxStop这两个全局事件

源码实现如下

// 触发事件并返回布尔值
function triggerAndReturn(context, eventName, data) {
// 创建eventName事件对象
var event = $.Event(eventName)
// 触发对应事件
$(context).trigger(event, data)
// 判断是否调用了preventDefault()
return !event.isDefaultPrevented()
}
// 触发Ajax全局事件
function triggerGlobal(settings, context, eventName, data) {
if (settings.global) return triggerAndReturn(context || document, eventName, data)
}
// 请求活跃数
$.active = 0
// 如果没有其他Ajax请求操作,将会被触发(在$.ajax操作开始时会被内部调用)
function ajaxStart(settings) {
if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart')
}
// 如果这是最后一个活跃着的Ajax请求,将会被触发(在$.ajax操作结束时会被内部调用)
function ajaxStop(settings) {
if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
}
// 再发送请求前,可以被取消(在$.ajax操作开始时会被内部调用)
function ajaxBeforeSend(xhr, settings) {
var context = settings.context
// 如果在$.ajax中的beforeSend方法中返回了return false
// 如果在全局的ajaxBeforeSend侦听回调中返回了return false
// 那么返回return false
if (settings.beforeSend.call(context, xhr, settings) === false ||
triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false)
return false
// ajaxBeforeSend事件侦听中如果没返回return false,那么主动触发ajaxSend事件
triggerGlobal(settings, context, 'ajaxSend', [xhr, settings])
}
// 当返回成功时
function ajaxSuccess(data, xhr, settings, deferred) {
var context = settings.context, status = 'success'
// 执行$.ajax的success方法
settings.success.call(context, data, status, xhr)
// 如果存在deferred模块,执行resolveWith方法
if (deferred) deferred.resolveWith(context, [data, status, xhr])
// 触发ajaxSuccess全局事件
triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data])
// 执行ajaxComplete函数
ajaxComplete(status, xhr, settings)
}
// 当有错误时
function ajaxError(error, type, xhr, settings, deferred) {
var context = settings.context
// 执行$.ajax的error方法
settings.error.call(context, xhr, type, error)
// 如果存在deferred模块,执行resolveWith方法
if (deferred) deferred.rejectWith(context, [xhr, type, error])
// 触发ajaxError全局事件
triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type])
// 执行ajaxComplete函数
ajaxComplete(type, xhr, settings)
}
// 请求已经完成后,无论请求是成功或者失败
function ajaxComplete(status, xhr, settings) {
var context = settings.context
// 执行$.ajax的complete方法
settings.complete.call(context, xhr, status)
// 触发ajaxComplete全局事件
triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings])
// 执行ajaxStop函数
ajaxStop(settings)
}

其实具体实现很简单,就是通过定义一系列触发函数,在具体的ajax接口操作中穿插调用,利用库本身实现的$.Event模块进行dom事件触发

全局ajax方法

ajax

使用

$.ajax({
type: 'GET',
url: '/projects',
data: { name: 'Zepto.js' },
dataType: 'json',
timeout: 300,
context: $('body'),
success: function(data){},
error: function(xhr, type){}
})

实现内容如下

$.ajax = function(options) {
// 1.拷贝传入配置options
// 2.拷贝$.ajaxSettings默认配置
// 3.触发全局ajaxStart事件
// 4.判断是否请求跨域(对settings.crossDomain进行设置)
// 5.取得实际有效的url
// 6.序列化数据
// 7.判断是否为jsonp并进行处理
// 8.获取mime和xhr, 定义内部头部处理方法
// 9.配置核心头部(表明请求类型, 表明能够处理的类型, 强制响应时的mime类型, 确定发送信息至服务器时内容编码类型)
// 10.侦听onreadystatechange
// 11.处理ajaxBeforeSend的情况
// 12.建立xhr.open
// 13.设置header
// 14.超时判断和处理
// 15.执行send
}

具体源码

$.ajax = function(options){
// 拷贝传入配置
var settings = $.extend({}, options || {}),
// 如果存在Deferred模块就创建deferred对象
deferred = $.Deferred && $.Deferred(),
urlAnchor, hashIndex;
// 拷贝$.ajaxSettings默认配置
for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key]
// 触发全局ajaxStart事件
ajaxStart(settings)
// 判断是否请求跨域
if (!settings.crossDomain) {
urlAnchor = document.createElement('a')
urlAnchor.href = settings.url
// ie游览器bug——重新赋值可以才能获得host
urlAnchor.href = urlAnchor.href
settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host)
}
// 如果请求url不存在,就以当前页面地址为请求地址
if (!settings.url) settings.url = window.location.toString()
// 如果存在hash,则只取非hash的url部分
if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex)
// 序列化数据
serializeData(settings)
// 设置dataType
var dataType = settings.dataType, hasPlaceholder = /\?.+=\?/.test(settings.url)
// 如果存在?name=?占位符,就设置dataType为jsonp
if (hasPlaceholder) dataType = 'jsonp'
// 如果判断条件满足,则不允许缓存,通过添加时间戳实现
if (settings.cache === false || (
(!options || options.cache !== true) &&
('script' == dataType || 'jsonp' == dataType)
))
settings.url = appendQuery(settings.url, '_=' + Date.now()) // 如果dataType为jsonp
if ('jsonp' == dataType) {
// 如果不存在占位符
if (!hasPlaceholder)
// 如果settings.jsonp存在,则追加=?
// 如果settings.jsonp为false,则不向url中追加内容
// 否则追加callback=?
settings.url = appendQuery(settings.url,
settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?')
return $.ajaxJSONP(settings, deferred)
} // 根据dataType获取mime
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对象
xhr = settings.xhr(),
// 存储原生header设置方法
nativeSetHeader = xhr.setRequestHeader,
abortTimeout // 如果存在deferred对象,执行promise
if (deferred) deferred.promise(xhr)
// 表明请求类型
if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest')
// 表明能够处理的类型
setHeader('Accept', mime || '*/*')
// 强制响应时的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])
// 重置header设置方法
xhr.setRequestHeader = setHeader xhr.onreadystatechange = function(){
// 请求过程结束
if (xhr.readyState == 4) {
// 重置onreadystatechange为空函数
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 = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'))
// 根据responseType设置result
if (xhr.responseType == 'arraybuffer' || xhr.responseType == 'blob')
result = xhr.response
else {
result = xhr.responseText
try {
// 通过settings中dataFilter字段进行数据过滤
result = ajaxDataFilter(result, dataType, settings)
// (1,eval)确保eval执行的作用域是在window下
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 }
// 如果error有值,发出转换异常
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)
}
}
} // 如果ajaxBeforeSend侦听存在return false操作,则取消请求,发送异常
if (ajaxBeforeSend(xhr, settings) === false) {
xhr.abort()
ajaxError(null, 'abort', xhr, settings, deferred)
return xhr
} var async = 'async' in settings ? settings.async : true
// 附带username&password凭据字段参数
xhr.open(settings.type, settings.url, async, settings.username, settings.password)
// 复制其它配置到xhr实例上
if (settings.xhrFields) for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name]
// 真正设置header
for (name in headers) nativeSetHeader.apply(xhr, headers[name])
// 如果存在超时设置,执行超时处理,发送超时异常
if (settings.timeout > 0) abortTimeout = setTimeout(function(){
xhr.onreadystatechange = empty
xhr.abort()
ajaxError(null, 'timeout', xhr, settings, deferred)
}, settings.timeout)
// 避免发送空字符串
xhr.send(settings.data ? settings.data : null)
return xhr
}

相关内部方法实现

// 通过mime值得到dateType
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'
} // 对url添加Query
function appendQuery(url, query) {
if (query == '') return url
return (url + '&' + query).replace(/[&?]{1,2}/, '?')
} // 序列化数据($.param实现可看具体源码)
function serializeData(options) {
// 如果processData属性为true,并且存在的数据不为字符串,则调用$.param序列化
if (options.processData && options.data && $.type(options.data) != "string")
options.data = $.param(options.data, options.traditional)
// 如果请求为get或jsonp,则调用appendQueryj将data追加到url
if (options.data && (!options.type || options.type.toUpperCase() == 'GET' || 'jsonp' == options.dataType))
options.url = appendQuery(options.url, options.data), options.data = undefined
} // 数据过滤
function ajaxDataFilter(data, type, settings) {
if (settings.dataFilter == empty) return data
var context = settings.context
return settings.dataFilter.call(context, data, type)
}

get

$.get = function(/* url, data, success, dataType */){
return $.ajax(parseArguments.apply(null, arguments))
}

post

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

getJSON

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

拓展接口的依托方法

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
}
}

load

$.fn.load = function(url, data, success){
// 不存在zepto对象就不执行操作
if (!this.length) return this
// 获取zepto对象,url根据空格生成数组
var self = this, parts = url.split(/\s/), selector,
// 获取默认请求配置
options = parseArguments(url, data, success),
// 存储回调函数
callback = options.success
// 根据parts分理出url和选择器
if (parts.length > 1) options.url = parts[0], selector = parts[1]
// 修改success
options.success = function(response){
// 如果存在选择器,就取response里选择器的部分,否则取全部
self.html(selector ?
$('<div>').html(response.replace(rscript, "")).find(selector)
: response)
callback && callback.apply(self, arguments)
}
$.ajax(options)
return this
}

ajaxJSONP

使用

 // options就是$.ajax方法的options,主要传递`jsonpCallback:全局jsonp函数名`
$.ajaxJSONP(options);

源码

$.ajaxJSONP = function(options, deferred){
// 不存在type的时候就走正常的ajax流程,使用默认type
if (!('type' in options)) return $.ajax(options)
// 获取全局JSONP回调函数的字符串名
var _callbackName = options.jsonpCallback,
callbackName = ($.isFunction(_callbackName) ?
_callbackName() : _callbackName) || ('Zepto' + (jsonpID++)),
// 创建script标签
script = document.createElement('script'),
// 存储全局JSONP回调函数的引用
originalCallback = window[callbackName],
responseData,
// 定义取消方法
abort = function(errorType) {
$(script).triggerHandler('error', errorType || 'abort')
},
xhr = { abort: abort }, abortTimeout // 如果存在deferred,执行promise方法
if (deferred) deferred.promise(xhr) // 对script标签定义load error方法
$(script).on('load error', function(e, errorType){
// 清除定时函数,去除script标签
clearTimeout(abortTimeout)
$(script).off().remove()
// 操作成功和失败时
if (e.type == 'error' || !responseData) {
ajaxError(null, errorType || 'error', xhr, options, deferred)
} else {
ajaxSuccess(responseData[0], xhr, options, deferred)
}
// 交还全局jsonp函数引用
window[callbackName] = originalCallback
// 如果存在返回数据和全局jsonp函数
if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0])
// 重置变量
originalCallback = responseData = undefined
})
// 如果ajaxBeforeSend侦听存在return false操作,则执行取消
if (ajaxBeforeSend(xhr, options) === false) {
abort('abort')
return xhr
}
// 重置全局jsonp回调函数,用于获取数据
window[callbackName] = function(){
responseData = arguments
}
// 对script标签设置url(发送请求)
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
document.head.appendChild(script)
// 如果存在延迟设置,则定时执行取消
if (options.timeout > 0) abortTimeout = setTimeout(function(){
abort('timeout')
}, options.timeout) return xhr
}

ajaxJSONP实现的关键在于,对全局jsonp函数引用的存取和交还

一开始将原始函数引用进行存储

originalCallback = window[callbackName],

接着重置它用于在script请求后被执行以便获得返回数据

window[callbackName] = function(){
responseData = arguments
}
script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
document.head.appendChild(script)

然后在script标签的load事件触发后,执行存储的原始函数并把数据传递过去

$(script).on('load error', function(e, errorType){
// .... if (responseData && $.isFunction(originalCallback))
originalCallback(responseData[0]) // ....
})