源码分析:(用于真正处理执行ajaxPrefilters和ajaxTransport的逻辑代码,参考)
function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
var inspected = {},
seekingTransport = ( structure === transports );
function inspect( dataType ) {
var selected;
inspected[ dataType ] = true;//这种数据类型已经检查过了
jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
if ( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
options.dataTypes.unshift( dataTypeOrTransport );
inspect( dataTypeOrTransport );
return false;
} else if ( seekingTransport ) {
return !( selected = dataTypeOrTransport );
}
});
return selected;//返回的select有send,abort等方法!
}
return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
}
注意:
(1)上面用的是jQuery.each方法,如果有return false那么直接跳出循环。
(2)同时我们一开始是检测options.dataTypes[0],也就是执行我们传入的dataTypes[0]的回调函数集合,如果我们明确传入了dataType那么这里就不会是"*"否则默认是"*"!这种"*"表示通过ajaxTransport和ajaxPrefilters添加函数的时候没有明确指定数据类型,那么数据类型就被设置为"*"!表示任何数据类型都会进行过滤
(3)总之,如果我们明确指定了dataType那么我们就检测dataType[0],如果检测dataType[0]时候返回的值转换为false那么我们还会继续执行所有的"*"也就是通用过滤器!最终返回一个对象,这个对象就是select就是执行通过ajaxTransport和ajaxPrefilters添加的函数执行的返回值,该对象有send等方法,也是ajax请求真正起作用的地方!获取这个对象就可以发送请求了!
note:inspectPrefiltersOrTransports方法在ajax方法中被执行了两次,第一次执行所有的ajaxPrefilters第二次执行所有的ajaxTransport!但是这里面很显然有一次执行的时候判断返回值是否是string,如果是string同时调用结果也是在ajaxprefilters中,而不是ajaxTransport中,而且这种数据类型还没有被检查过!那么把这种类型放入到dataTypes中,而且放在dataTypes的最前面,然后继续对这种类型检查!那么这个判断有什么用呢?看下面代码:
jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
var callbackName, overwritten, responseContainer,
jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
"url" :
typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data"
);
// Handle iff the expected data type is "jsonp" or we have a parameter to set
if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
// Get callback name, remembering preexisting value associated with it
callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
s.jsonpCallback() :
s.jsonpCallback;
// Insert callback into url or form data
if ( jsonProp ) {
s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
} else if ( s.jsonp !== false ) {
s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
}
// Use data converter to retrieve json after script execution
s.converters["script json"] = function() {
if ( !responseContainer ) {
jQuery.error( callbackName + " was not called" );
}
return responseContainer[ 0 ];
};
// force json dataType
s.dataTypes[ 0 ] = "json";//把dataTypes[0]设置为json,也就是返回值是json!
// Install callback
overwritten = window[ callbackName ];
window[ callbackName ] = function() {
responseContainer = arguments;
};
// Clean-up function (fires after converters)
jqXHR.always(function() {
// Restore preexisting value
window[ callbackName ] = overwritten;
// Save back as free
if ( s[ callbackName ] ) {
// make sure that re-using the options doesn't screw things around
s.jsonpCallback = originalSettings.jsonpCallback;
// save the callback name for future use
oldCallbacks.push( callbackName );
}
// Call if it was a function and we have a response
if ( responseContainer && jQuery.isFunction( overwritten ) ) {
overwritten( responseContainer[ 0 ] );
}
responseContainer = overwritten = undefined;
});
// Delegate to script
return "script";
}
});
note:对于json或者jsonp这种数据的预处理发生了URL的重构,同时最后返回了“script”用于inspectPrefiltersOrTransports函数。所以在用户的dataType设置为"json"或者"jsonp"的时候我们会进行URL重构,同时重构结束以后会在函数inspectPrefiltersOrTransports里继续对script标签进行预先处理,也就说当对json或者jsonp处理完毕以后就会处理transport或者prefilters里面的script集合中的函数,要记住在prefilters或者transport里面以script为键名放置的其实是一个数组。
我们现在看看jQuery通过ajaxTransport方法添加的一个通用的回调函数:
jQuery.ajaxTransport(function( options ) {
// Cross domain only allowed if supported through XMLHttpRequest
if ( !options.crossDomain || support.cors ) {
var callback;
return {
send: function( headers, complete ) {
var i,
xhr = options.xhr(),
id = ++xhrId;
// Open the socket
xhr.open( options.type, options.url, options.async, options.username, options.password );
// Apply custom fields if provided
if ( options.xhrFields ) {
for ( i in options.xhrFields ) {
xhr[ i ] = options.xhrFields[ i ];
}
}
// Override mime type if needed
if ( options.mimeType && xhr.overrideMimeType ) {
xhr.overrideMimeType( options.mimeType );
}
// X-Requested-With header
// For cross-domain requests, seeing as conditions for a preflight are
// akin to a jigsaw puzzle, we simply never set it to be sure.
// (it can always be set on a per-request basis or even using ajaxSetup)
// For same-domain requests, won't change header if already provided.
if ( !options.crossDomain && !headers["X-Requested-With"] ) {
headers["X-Requested-With"] = "XMLHttpRequest";
}
// Set headers
for ( i in headers ) {
// Support: IE<9
// IE's ActiveXObject throws a 'Type Mismatch' exception when setting
// request header to a null-value.
//
// To keep consistent with other XHR implementations, cast the value
// to string and ignore `undefined`.
if ( headers[ i ] !== undefined ) {
xhr.setRequestHeader( i, headers[ i ] + "" );
}
}
// Do send the request
// This may raise an exception which is actually
// handled in jQuery.ajax (so no try/catch here)
xhr.send( ( options.hasContent && options.data ) || null );
// Listener
callback = function( _, isAbort ) {
var status, statusText, responses;
// Was never called and is aborted or complete
if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
// Clean up
delete xhrCallbacks[ id ];
callback = undefined;
xhr.onreadystatechange = jQuery.noop;
//在abort方法里面调用callback(undefined,true)这里就会把回调函数什么都置空,如果完成了readyState=4也会置空回调函数!
// Abort manually if needed
if ( isAbort ) {//执行if如果调用了abort方法!
if ( xhr.readyState !== 4 ) {
xhr.abort();
}
} else {
responses = {};
status = xhr.status;
// Support: IE<10
// Accessing binary-data responseText throws an exception
// (#11426)
if ( typeof xhr.responseText === "string" ) {
responses.text = xhr.responseText;
}
// 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 && options.isLocal && !options.crossDomain ) {
status = responses.text ? 200 : 404;
// IE - #1450: sometimes returns 1223 when it should be 204
} else if ( status === 1223 ) {
status = 204;
}
}
}
// Call complete if needed
if ( responses ) {//status是状态码,statusText就是状态信息,responses就是服务器返回内容
complete( status, statusText, responses, xhr.getAllResponseHeaders() );
}
};
if ( !options.async ) {
// if we're in sync mode we fire the callback
callback();
} else if ( xhr.readyState === 4 ) {
// (IE6 & IE7) if it's in cache and has been
// retrieved directly we need to fire the callback
setTimeout( callback );
} else {
// Add to the list of active xhr callbacks回调函数callback第一个参数是event对象!
xhr.onreadystatechange = xhrCallbacks[ id ] = callback;
}
},
abort: function() {
if ( callback ) {
callback( undefined, true );
}
}
};
}
});
note:这个通用的回调函数的使用返回就是上面说过的"*",对于任何类型都会处理。当在inspectPrefiltersOrTransports函数中执行的时候就会返回select,是一个对象,该对象有send方法用于真正发送请求!函数执行的时候传入的参数是options,originalOptions,jqXHR对象!同时要调用send方法的时候,我们会传入两个参数,通过setRequestHeader方法传入的HTTP头和调用成功时候的回调函数。调用时候的过程为:
(1)通过open方法打开socket流,并且附加用户传入的xhrFields头部信息。xhrFields是一个具有多个"字段名称-字段值"对的对象,用于对本地XHR对象进行设置。一对「文件名-文件值」在本机设置XHR对象。例如,如果需要,你可以用它来为跨域请求设置XHR对象的withCredentials
属性为true
。
(2)为XHR对象设置用户自己提供的mimeType类型,并且把X-Requested-With放入headers集合中,最终把用户提供的headers选项通过setRequestHeader赋值到XHR对象上。headers默认值:{}
。以对象形式指定附加的请求头信息。请求头X-Requested-With: XMLHttpRequest
将始终被添加,当然你也可以在此处修改默认的XMLHttpRequest值。headers
中的值可以覆盖beforeSend
回调函数中设置的请求头(意即beforeSend先被调用)。headers选项是通过调用xhr对象的setRequestHeader方法来完成的!
(3)设置完XHR的头部以后就调用send方法了,如果不是get/head请求那么把用户提供的数据一同发送出去,get/head方法已经把参数附加到URL后面!
我们通过阅读源码看出,调用transport.send( requestHeaders, done );方法的时候是在获取到transport方法以后,然后传入的回调函数是done方法(见下面源码)!
(4)如果async是false,表示不是异步,那么直接调用上面代码中的回调函数callback。如果用户指定了异步,同时readyState已经是4,表示已经完成了请求了。(4 - (完成)响应内容解析完成,可以在客户端调用了 )那么这时候也直接调用callback(因为在IE6、7中会缓存,我们要手动调用)。如果不是上面两种情况,例如"用户指定了async为true,同时readyState也不是4,那么我们直接把这个callback回调函数绑定到xhr的onreadystatechange事件中"。我们要注意上面代码中有xhrCallbacks[id],用处在那里呢?我们看看定义:var xhrId = 0,xhrCallbacks = {},
同时在ajaxTransport中每调用一次send方法id = ++xhrId;xhr.onreadystatechange = xhrCallbacks[ id ] = callback;这表示:每次调用send方法都会把这个函数放在xhrCallbacks对象中保存起来,其中键名是表示第几次调用send方法!键值就是回调函数!
(5)我们看看在回调函数callback中干了什么?第一步:如果请求已经完成或者已经取消那么我们xhrCallbacks中的相应的回调删除,同时把callback清空为undefined,已经onreadystatechange设置为空函数。如果isAbort为true,同时readyState不是4,表示没有完成,那么手动调用abort方法!如果isAbort不是true,那么表示已经完成了,这时候获取xhr对象的status,responseText,statusText,getAllResponseHeaders,同时把xhr对象的responseText封装到response的text属性上面,最后调用我们上面调用send方法传入的回调函数。done( status, statusText, responses, xhr.getAllResponseHeaders() );
总之:
(1)ajaxTransport只是返回一个具有send,abort等方法的对象,调用这个对象的send方法就相当于真正调用了ajax请求!请求完成以后会把所有的信息传入到send调用的时候指定的回调函数中,其中包括status, statusText, responses, xhr.getAllResponseHeaders() 进而完成回调!
(2)我这里要特别强调一点:当通过jQuery发送ajax实现跨域请求的时候,这时候是不会发送X-Requested-With头的,即使你明确通过heads添加你也会发现不会发送这个HTTP头!哪怕只有端口号不同,协议相同,域名相同也不会发送!其实从代码中也是很容易知道的: !options.crossDomain && !headers["X-Requested-With"]如果是跨域那么crossDomain肯定是true,即使自己不设置jquery也会给你设置,那么这if就不会执行,也就是不会设置 ["X-Requested-With"]头!
done方法源码:
注意:如果是通过上面这个通用的ajaxTransport获取到的对象调用的send方法,那么最后返回的response格式为{text:xhr.responseText}。当然,如果是其它的数据类型可能直接通过自己特有的ajaxTransport完成数据发送和回调了,而不会通过通用的ajaxTransport。这个逻辑在inspectPrefiltersOrTransports中很容易就能看到!因为先处理dataType[0]然后才处理dataType["*"]!
function done( status, nativeStatusText, responses, headers ) {
var isSuccess, success, error, response, modified,
statusText = nativeStatusText;
// Called once
if ( state === 2 ) {
return;
}
// State is "done" now
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;
// Determine if successful
isSuccess = status >= 200 && status < 300 || status === 304;
// Get response data
if ( responses ) {
response = ajaxHandleResponses( s, jqXHR, responses );
}
// Convert no matter what (that way responseXXX fields are always set)
response = ajaxConvert( s, response, jqXHR, isSuccess );
// If successful, handle type chaining
if ( isSuccess ) {
// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
if ( s.ifModified ) {
modified = jqXHR.getResponseHeader("Last-Modified");
if ( modified ) {
jQuery.lastModified[ cacheURL ] = modified;
}
modified = jqXHR.getResponseHeader("etag");
if ( modified ) {
jQuery.etag[ cacheURL ] = modified;
}
}
// if no content
if ( status === 204 || s.type === "HEAD" ) {
statusText = "nocontent";
// if not modified
} else if ( status === 304 ) {
statusText = "notmodified";
// If we have data, let's convert it
} else {
statusText = response.state;
success = response.data;
error = response.error;
isSuccess = !error;
}
} else {
// We extract error from statusText
// then normalize statusText and status for non-aborts
error = statusText;
if ( status || !statusText ) {
statusText = "error";
if ( status < 0 ) {
status = 0;
}
}
}
// Set data for the fake xhr object
jqXHR.status = status;
jqXHR.statusText = ( nativeStatusText || statusText ) + "";
// Success/Error
if ( isSuccess ) {//Deferred中封装了三个Callbacks对象done,fail,progress如果resolveWith那么done中函数全部调用!
deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
} else {
deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
}
// Status-dependent callbacks
jqXHR.statusCode( statusCode );
statusCode = undefined;
if ( fireGlobals ) {
globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
[ jqXHR, s, isSuccess ? success : error ] );
}
// Complete
completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
if ( fireGlobals ) {
globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
// Handle the global AJAX counter
if ( !( --jQuery.active ) ) {
jQuery.event.trigger("ajaxStop");
}
}
}
return jqXHR;
}
note:done方法到底在干嘛。
(1)他作为ajax的回调函数处理,他首先会把返回的数据如通用ajaxTransport返回的responses={text:xhr.responseText}封装到jqXHR对象上去,形成jqXHR["responseText"]=xhr.responseText!于是在回调函数里面我们就可以通过jqXHR获取到服务器端返回的数据!
(2)把处理后的数据返回来,如通过通用ajaxTransport返回的responses["text"],也就是得到服务器返回的真正的数据,这个数据是"string"!
(3)ajaxConverter在干嘛?因为第二步返回的数据是string,但是用户通过dataType指定了自己需要的数据类型,ajaxConverter就是把我们获取到的string类型数据转换为用户通过dataType指定的数据类型!
status >= 200 && status < 300 || status === 304;表示请求成功了!
我们首先弄懂ajax中的resolveWith等逻辑,见下面的测试用例:
var deferred=jQuery.Deferred();
var jqXHR={};
function f1()
{
alert("f1");
}
function f2()
{
alert("f2");
}
function f3()
{
alert("f3");
}
//jqXHR具有了promise所有的属性和方法,同时为返回的这个
//增强的jqXHR对象对应的成功回调数组添加了两个回调函数f1,f2
deferred.promise( jqXHR ).done(f1).done(f2);
//jqXHR对象的success方法相当于jqXHR的done方法
//说明通过jqXHR通过success方法添加进去的函数在
//Deferred调用resolve时候也会调用!
jqXHR.success = jqXHR.done;
//通过success方法添加一个回调函数
jqXHR.success(f3);
//弹出[f1,f2,f3]
deferred.resolve();
//代码片段1:
//for ( i in { success: 1, error: 1, complete: 1 } ) {
// jqXHR[ i ]( s[ i ] );
// }
//代码片段2:
//deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
//源码中这一句代码的作用:让通过Deferred对象中的成功回调函数全部执行,通过代码片段1
//可以知道:通过jqXHR.success(s[i])把我们自己设置的成功回调函数全部放在Deferred
//对象对应done所对于的Callback中,所以当调用deferred对象的resolveWith时候
//我们自己传送的success方法就会被执行!
//代码片段3:
//var completeDeferred = jQuery.Callbacks("once memory")
// deferred.promise( jqXHR ).complete = completeDeferred.add;
//这也就是说:我们通过complete方法添加的函数放在了completeDeferred中
//而completeDeferred对应于一个Callback对象,添加的函数数组如何被调用呢?
//completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
//所以Callback对象的调用还是通过原始的fireWith,因为这里是Callback没有done方法等!
通过上面测试代码片段你应该理解下面几个部分:
指定请求完成(无论成功或失败)后需要执行的回调函数。该函数还有两个参数:一个是jqXHR
对象,一个是表示请求状态的字符串('success'、 'notmodified'、 'error'、 'timeout'、 'abort'或'parsererror')。这是一个Ajax事件。从jQuery 1.5开始,该属性值可以是数组形式的多个函数,每个函数都将被回调执行。
指定请求成功后执行的回调函数。该函数有3个参数:请求返回的数据、响应状态字符串、jqXHR
对象。从jQuery 1.5开始,该属性值可以是数组形式的多个函数,每个函数都将被回调执行。
指定请求失败时执行的回调函数。该函数有3个参数:jqXHR对象、 请求状态字符串(null、 'timeout'、 'error'、 'abort'和'parsererror')、 错误信息字符串(响应状态的文本描述部分,例如'Not Found'或'Internal Server Error')。这是一个Ajax事件。跨域脚本和跨域JSONP请求不会调用该函数。从jQuery 1.5开始,该属性值可以是数组形式的多个函数,每个函数都将被回调执行。
上面的success,error,complete都是添加到jqXHR对象相应的属性当中。success对应于jqXHR的done方法对应的回调数组,通过resolveWith调用;error对应于jqXHR的fail对应的回调数组,通过rejectWith调用;而complete是通过保存在Callbacks里面而不是Deferred里面,他是不管成功与否都是会调用的,他的调用是通过fireWith这种方法完成的!(可以参考我的关于Deferred和Callbacks对应的源码分析部分),我把这一部分源码附带上:deferred = jQuery.Deferred(),
completeDeferred = jQuery.Callbacks("once memory")
//complete通过保存在Callbacks中实现
deferred.promise( jqXHR ).complete = completeDeferred.add;
//success对应于done
jqXHR.success = jqXHR.done;
//error对应于fail
jqXHR.error = jqXHR.fail;
//resolveWith调用通过done方法或者success添加的回调函数
deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
//rejectWith调用通过fail或者error添加的回调函数
deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
//compelte添加的函数是通过fireWith实现,因为他是Callbacks而不是Deferred对象!
completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
上面的代码中,window怎么有callbackName呢,看下面的代码,这是jQuery为我们自动生成的一个函数:
jQuery.ajaxSetup({
jsonp: "callback",
jsonpCallback: function() {
var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
this[ callback ] = true;
return callback;
}
});
note:这一句代码使得最终的options对象具有了两个属性,一个是jsonp,一个是jsonpCallback,这是jQuery为我们内置的两个属性,可以通过jQuery.ajaxSettings.jsonpCallback打印看到函数签名!如果打印的时候在后面加上一个括号表示函数调用,这时候就会返回一个函数名,函数名如
jQuery111107973217158578336_1446447729232这样的字符串!如果我们发送jsonp请求的时候没有指定这两个参数那么结果就是如下的URL:http://localhost:8080/qinl/a.action?callback=jQuery111107973217158578336_1446447729232,服务器端通过通过获取到callback后这个jQuery111107973217158578336_1446447729232函数名,然后返回的字符串为jQuery111107973217158578336_1446447729232("hello, I am back!"),返回到客户端以后就相当于直接调用了这个函数!于是responseContainer就相当于回调时候的实参,这个实参是服务器端发送过来的数据!
s.converters["script json"] = function() {
if ( !responseContainer ) {
jQuery.error( callbackName + " was not called" );
}
return responseContainer[ 0 ];
};
note:这一段代码的作用就是在最终的options的converters中添加了一段相应格式的处理函数,我们先看看converters里面放的是什么:
converters: {
// Convert anything to text
"* text": String,
// Text to html (true = no transformation)
"text html": true,
// Evaluate text as a json expression
"text json": jQuery.parseJSON,
// Parse text as xml
"text xml": jQuery.parseXML
}
note:现在说说上面那段代码的作用,他相当与告诉jQuery,如果用户传入的参数是dataType="script json"那么我们就用后面这个函数处理。我们可以看到对于"text json"用了jQuery.parseJSON从而把服务器端的返回数据转化为JSON。那么传入的dataType="script json"会怎么处理呢?如果服务器端没有返回数据,那么jQuery就抛出一个异常,如果jQuery返回了数据那么就直接把服务器返回的数据返回!
这就是对"script json"的处理逻辑!这个函数会在ajaxConvert函数中被调用!
重写JSONP请求的回调函数名称。该值用于替代"url?callback=?"中的"callback"部分。服务器用request.getParameter获取到!服务器返回之为jsonpCallback("服务器要传递给浏览器的数组")。
为JSONP请求指定一个回调函数名。这个值将用来取代jQuery自动生成的随机函数名。
从jQuery 1.5开始,你也可以指定一个函数来返回所需的函数名称。