源码分析:(用于真正处理执行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开始,你也可以指定一个函数来返回所需的函数名称。