关于ajax
,jQuery
做了封装,并且考虑了很多浏览器兼容问题,以及跨域问题。当然,这种异步操作离不开我们之前分析的延迟对象。所以这一次,主要是对ajax
模块中对于延迟对象的应用进行分析。
$.ajax函数
先来分析$.ajax()
函数。
在这里,我必须贴一张图来展示下ajax
源码的整体框架。此图引用于高云(jQuery
技术内幕第13章)
上图清楚得描述了整个ajax
过程中的一系列流程,同时是按照源码顺序列出来的。根据上图,我们也能大体还原出来一个ajax
源码框架。
通常,我们调ajax
时,比较常见的就是 $.ajax({ url: 'XX.php', success: function(){ } })
这种形式, 但也有$.ajax('XX.php').done(function(){ }).fail(function(){ })
这种形式。所以,不难想象,$.ajax()返回的也是一个延迟对象。即上图上面最后返回的jqXHR
对象。而这个jqXHR
对象又类似于我们原生XMLHttpRequest
对象,需要有自己的一些方法和属性,因此,可以利用延迟对象的promise(arguments)
方法来扩展延迟对象。
function( url, options)
{
//创建延迟对象
var deferred = $.Deferred();
//创建一个回调对象,用于最终完成时,触发完成回调函数。
var completeDeferred = $.Callbacks('once memory')
jqXHR = {
//拥有一些XMLHttpRequest的属性和方法,比如readyState、 setRequestHeader等等。
readyState: 0,
setRequestHeader: function(){
}
}
deferred.promise(jqXHR);
//此时jqXHR是一个延迟对象,并且拥有自己特有的属性和方法
//将我们常用的success、error、complete这些回调方法分别与延迟对象的回调列表对应起来。
jqXHR.success = jqXHR.done;
jqXHR.error = jqXHR.fail;
jqXHR.complete = completeDeferred.add;
return jqXHR;
}
上面这部分框架,只是创建jqXHR对象,并将其与延迟对象的各个列表联系起来,并最终返回这个延迟对象的过程。那么,问题来了,到底随着请求的产生,发送及响应,是如何触发回调函数的?
我们再回过头看上面的图,最后当transport.send( requestHeaders, done );
执行之后,这个函数主要执行了xhr.send();
这个函数,然后监听readyStateChange事件,由xhr.responseText
或者xhr.responseXML
判断,有响应数据时,便会触发send
中的回调函数done
。
因为transport.send
针对了script
和 其他数据类型两种情况,我们这里只讨论后一种情况。其transport.send
中的部分源码如下:
//创建XMLHttpRequest对象
//打开socket连接:xhr.open()
if ( s.username ) {
xhr.open( s.type, s.url, s.async, s.username, s.password );
} else {
xhr.open( s.type, s.url, s.async );
}
//设置HTTP请求头信息
//接着是发送请求:xhr.send(),如果是post方法,有参数
xhr.send( ( s.hasContent && s.data ) || null );
//下面的思想,跟我们用原生XMLHttpRequest对象实现aJax是一样的。也是通过监听readyState是否变化的事件从而触发回调函数
//如果是同步ajax请求或者状态已经完成,那么手动执行回调函数
if ( !s.async || xhr.readyState === 4 ) {
callback();
} else {
//如果是异步,并且状态还未变成已完成,那么通过监听readystatechange事件来触发回调函数
handle = ++xhrId;
if ( xhrOnUnloadAbort ) {
// 这里主要是在IE浏览器中
if ( !xhrCallbacks ) {
xhrCallbacks = {};
//因为在IE浏览器中,关闭浏览器时,仍然保持着连接,所以需要手动将所有的ajax请求回调函数取消。
jQuery( window ).unload( xhrOnUnloadAbort );
}
xhrCallbacks[ handle ] = callback;
}
//不是IE浏览器时,监听readystatechange事件
xhr.onreadystatechange = callback;
}
那么回调函数callback 是干嘛的?
//isAbort表示是否取消请求
function( _, isAbort ) {
var status,
statusText,
responseHeaders,
responses,
xml;
// Firefox throws exceptions when accessing properties
// of an xhr when a network error occured
// http://helpful.knobsdials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
try {
// Was never called and is aborted or complete
if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
// Only called once
callback = undefined;
// Do not keep as active anymore
if ( handle ) {
xhr.onreadystatechange = jQuery.noop;
if ( xhrOnUnloadAbort ) {
delete xhrCallbacks[ handle ];
}
}
// If it's an abort
if ( isAbort ) {
// Abort it manually if needed
if ( xhr.readyState !== 4 ) {
xhr.abort();
}
} else {
status = xhr.status;
responseHeaders = xhr.getAllResponseHeaders();
responses = {};
xml = xhr.responseXML;
// Construct response list
if ( xml && xml.documentElement /* #4958 */ ) {
responses.xml = xml;
}
// When requesting binary data, IE6-9 will throw an exception
// on any attempt to access responseText (#11426)
try {
responses.text = xhr.responseText;
} catch( _ ) {
}
// Firefox throws an exception when accessing
// statusText for faulty cross-domain requests
try {
statusText = xhr.statusText;
} catch( e ) {
// We normalize with Webkit giving an empty statusText
statusText = "";
}
// Filter status for non standard behaviors
// If the request is local and we have data: assume a success
// (success with no data won't get notified, that's the best we
// can do given current implementations)
if ( !status && s.isLocal && !s.crossDomain ) {
status = responses.text ? 200 : 404;
// IE - #1450: sometimes returns 1223 when it should be 204
} else if ( status === 1223 ) {
status = 204;
}
}
}
} catch( firefoxAccessException ) {
if ( !isAbort ) {
complete( -1, firefoxAccessException );
}
}
//如果得到响应数据,触发complete函数,这个函数是外部传过来的函数,也就是我们外部的done回调函数
if ( responses ) {
complete( status, statusText, responses, responseHeaders );
}
}
而done
函数中,主要根据status
来判断响应是否成功,如果成功,触发jqXHR的成功回调列表中的回调;如果失败,则触发jqXHR的失败回调列表中的回调,除了这些处理,还会触发complete
回调函数,同时会根据情况触发各种ajax
全局函数。
function done( status, nativeStatusText, responses, headers ) {
//如果state为2,说明这个回调已经执行过了,不会再执行第二次了
if ( state === 2 ) {
return;
}
state = 2;
// Clear timeout if it exists
if ( timeoutTimer ) {
clearTimeout( timeoutTimer );
}
// Dereference transport for early garbage collection
// (no matter how long the jqXHR object will be used)
transport = undefined;
// Cache response headers
responseHeadersString = headers || "";
// Set readyState
jqXHR.readyState = status > 0 ? 4 : 0;
var isSuccess,
success,
error,
statusText = nativeStatusText,
response = responses ? ajaxHandleResponses( s, jqXHR, responses ) : undefined,
lastModified,
etag;
// 下面这部分代码主要是根据status状态,来判断是否请求成功。
//请求成功时,isSuccess标志就被设为true
if ( status >= 200 && status < 300 || status === 304 ) {
// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
if ( s.ifModified ) {
if ( ( lastModified = jqXHR.getResponseHeader( "Last-Modified" ) ) ) {
jQuery.lastModified[ ifModifiedKey ] = lastModified;
}
if ( ( etag = jqXHR.getResponseHeader( "Etag" ) ) ) {
jQuery.etag[ ifModifiedKey ] = etag;
}
}
// If not modified
if ( status === 304 ) {
statusText = "notmodified";
isSuccess = true;
//If we have data
} else {
try {
success = ajaxConvert( s, response );
statusText = "success";
isSuccess = true;
} catch(e) {
// We have a parsererror
statusText = "parsererror";
error = e;
}
}
} else {
// We extract error from statusText
// then normalize statusText and status for non-aborts
error = statusText;
if ( !statusText || status ) {
statusText = "error";
if ( status < 0 ) {
status = 0;
}
}
}
// Set data for the fake xhr object
jqXHR.status = status;
jqXHR.statusText = "" + ( nativeStatusText || statusText );
//由上面得到的请求成功与否的状态isSuccess
//请求成功,触发deferred.resolveWith(),使该延迟对象处于resolve状态,执行成功回调列表里的回调函数
//请求失败,触发deferred.rejectWith(),使该延迟对象处于reject状态,执行失败回调列表里的回调函数
if ( isSuccess ) {
deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
} else {
deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
}
//Status-dependent callbacks
jqXHR.statusCode( statusCode );
statusCode = undefined;
if ( fireGlobals ) {
//触发ajax全局事件,ajaxSuccess或者 ajaxError事件
globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ),
[ jqXHR, s, isSuccess ? success : error ] );
}
//执行completeDeferred回调对象的fireWith()方法,执行回调列表中的回调函数
completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
if ( fireGlobals ) {
globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
// Handle the global AJAX counter
if ( !( --jQuery.active ) ) {
jQuery.event.trigger( "ajaxStop" );
}
}
}
上面done
函数主要是通过触发延迟对象的resolveWith
或rejectWith
方法来执行我们添加的success: function(){ }
或 error: function(){ }
方法,并且通过触发completeDeferred
回调对象的fireWith()
方法来执行我们添加的complete: function(){ }
方法,那么我们在参数中添加的方法是如何加到回调列表中去的?
jQuery中是这么做的:
for ( i in { success: 1, error: 1, complete: 1 } ) {
jqXHR[ i ]( s[ i ] );
}
通过对{ success: 1, error: 1, complete: 1 }
遍历,分别执行jqXHR.success
, jqXHR.error
, jqXHR.complete
方法。这三个方法在前面也讲过了,分别对应到deferred
对象和回调对象的done
, fail
和 add
方法。所以这个操作就是将s.success
, s.error
, s.complete
添加到对应回调列表中的。那么s.success
, s.error
, s.complete
分别是什么呢?
其中
//option是我们传入到ajax中的配置选项对象
s = jQuery.ajaxSetup( {}, options )
//s就是 我们传的配置选项与jQuery源码中默认的配置的组合,里面有我们传入的url,没传url的话默认就是自身的href(这是在jQuery源码默认的配置选项中设置的); 还有我们传入的success回调函数, error回调函数以及complete函数。因此,上面说的s.success, s.error和s.complete其实就是我们传入的回调函数。
这样在done
函数中,触发resolveWith
或者rejectWith
,就会执行对应列表中的回调了,也就是我们传的回调函数。complete
也是同样的道理。
这样,就实现了ajax在请求的过程中,根据响应的成功与否,触发不同的回调列表中的函数,以此来实现异步请求的目的。这个过程很复杂,且比较绕,并且期间处理了很多很多情况,比如设置头部,处理跨域,这些都还没深入,只是从异步队列角度来分析,如果就实现了异步,旨在对异步队列应用有更深的理解。