惟有于技术中才能明得真相

jQuery源代码阅读(六):Ajax

jQuery的Ajax实现封装了不同浏览器的内部实现,为外部提供了一个统一的接口,使得编写Ajax程序不再那么困难。jQuery的Ajax也支持多种格式,txt, json, jsonp, xml,以及灵活的事件机制。我们就来看看jQuery是如何封装原始的XmlHttpRequest的。

jQuery中的所有Ajax调用都是通过jQuery.ajax函数来完成的,get, post, getJSON, getScript都只是简单地将调用转发给ajax函数。因此我们就从ajax函数入手。在潜入复杂的实现之前,先考虑一下自己实现ajax会怎样做是非常有益的。显然,我们做首先创建XmlHttpRequest,然后发送异步请求,在onreadystate回调中根据请求的结果再调用自定义的回调函数(如onsuccess, oncomplte, onerror)。jQuery的实现当然要比比复杂得多,因为它不仅要处理异步请求,也要处理同步请求,对其不同格式的处理过程也不一样,但经过这番思考之后,至少心里有个底,然后在复杂的jQuery实现里不致迷失方向。先来看ajax函数的前部分:

3397     ajax: function( s ) {
3398         // Extend the settings, but re-extend 's' so that it can be
3399         // checked again later (in the test suite, specifically)
3400         s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));
3401
3402         var jsonp, jsre = /=\?(&|$)/g, status, data,
3403             type = s.type.toUpperCase();
3404
3405         // convert data if not already a string
3406         if ( s.data && s.processData && typeof s.data !== "string" )
3407             s.data = jQuery.param(s.data);
3408
3409         // Handle JSONP Parameter Callbacks
3410         if ( s.dataType == "jsonp" ) {
3411             if ( type == "GET" ) {
3412                 if ( !s.url.match(jsre) )
3413                     s.url += (s.url.match(/\?/) ? "&" : "?") + (s.jsonp || "callback") + "=?";
3414             } else if ( !s.data || !s.data.match(jsre) )
3415                 s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
3416             s.dataType = "json";
3417         }
3418
3419         // Build temporary JSONP function
3420         if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) {
3421             jsonp = "jsonp" + jsc++;
3422
3423             // Replace the =? sequence both in the query string and the data
3424             if ( s.data )
3425                 s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
3426             s.url = s.url.replace(jsre, "=" + jsonp + "$1");
3427
3428             // We need to make sure
3429             // that a JSONP style response is executed properly
3430             s.dataType = "script";
3431
3432             // Handle JSONP-style loading
3433             window[ jsonp ] = function(tmp){
3434                 data = tmp;
3435                 success();
3436                 complete();
3437                 // Garbage collect
3438                 window[ jsonp ] = undefined;
3439                 try{ delete window[ jsonp ]; } catch(e){}
3440                 if ( head )
3441                     head.removeChild( script );
3442             };
3443         }


可以看到ajax函数只接受一个参数s,它包含所有请求所需的信息,如请求方法(s.type),请求的url(s.url),请求的数据(s.data),请求返回的数据类型(s.dataType),回调函数(s.success, s.error, s.complete)等,该方法返回XmlHttpRequest对象。前面的部分主要用于处理jsonp协议,我们知道由于Ajax请求由于安全限制,不能发送跨域请求,jsonp可以解除这个限制,它并不使用XmlHttpRequest去发送请求,而是使用动态创建script元素的技术,并将它的src的元素设置成请求的url,这就要求远程返回的是JavaScript代码,而不能仅仅是数据。jsonp是这样处理的,服务器端返回的数据仍然使用json协议,但它会将数据放在一个函数调用中,函数名称由客户端提供。举个例子,例如要向远程某个地址请求股票信息,请求的url可能为http://www.example.com/getStock?ticker=1234&callback=processStock,这个服务器端可能就会返回processStock({"price": 1234}),processStock是我们在请求传递的callback参数,它是在本地定义的一个JavaScript函数,括号内部是个json对象,它表示请求股票的价格。关于jsonp更详尽的解释可以参见这篇文章,我们这里需要明白的是jsonp的请求并不是通过XmlHttpRequest发送的,而是通过动态创建script元素来发送请求,script的请求也是这样做的。在jQuery中发送jsonp请求只需要在url参数中包含"=?"就可以了,例如对于上面的例子,使用jQuery调用就是这样的:

$.ajax({url: "http://www.example.com/getStock?ticker=1234&callback=?", ....})

第3402行的正则表达式进行的正是这样的判断。第3406-3407行对s.data按URL编码进行编码,{name1: "value1", name2: "value2"}会编码成"name1=value1&name2=value2"。第3401-3417行当请求数据类型为jsonp,但url不包含"=?"时,给s.url或s.data添加s.jsonp=?参数,当s.jsonp没有指定时为callback。第3420-3433行仍然处理的是jsonp数据请求,第3421-3426行将请求url中的?替换成在jQuery定义的一个函数名称,它由"jsonp"再加下一个递增的数字组成。第3433-3442行定义了这个函数,这个函数会在远程脚本加载完成时被调用,在这个函数内部调用了success()和complete(),而它们又会调用参数s中传递的函数函数。注意3430将请求数据类型改成了script,这是因为jsonp请求和script请求采用相同的方式发送请求。

3445         if ( s.dataType == "script" && s.cache == null )
3446             s.cache = false;
3447
3448         if ( s.cache === false && type == "GET" ) {
3449             var ts = now();
3450             // try replacing _= if it is there
3451             var ret = s.url.replace(/(\?|&)_=.*?(&|$)/, "$1_=" + ts + "$2");
3452             // if nothing was replaced, add timestamp to the end
3453             s.url = ret + ((ret == s.url) ? (s.url.match(/\?/) ? "&" : "?") + "_=" + ts : "");
3454         }
3455
3456         // If data is available, append data to url for get requests
3457         if ( s.data && type == "GET" ) {
3458             s.url += (s.url.match(/\?/) ? "&" : "?") + s.data;
3459
3460             // IE likes to send both get and post data, prevent this
3461             s.data = null;
3462         }
3463
3464         // Watch for a new set of requests
3465         if ( s.global && ! jQuery.active++ )
3466             jQuery.event.trigger( "ajaxStart" );

上面的代码主要是构造GET请求的URL,缓存禁用时要给url最后加上一个时间参数(第3448-3454行),这样可以避免浏览器对请求的缓存,另外data中的参数也会加到url参数中去(第3457-3461行)。第3465-3466行触发全局事件ajaxStart,注意ajaxStart并不是每次ajax请求都会触发,它只会在第一次调用ajax方法触发。

3468         // Matches an absolute URL, and saves the domain
3469         var parts = /^(\w+:)?\/\/([^\/?#]+)/.exec( s.url );
3470
3471         // If we're requesting a remote document
3472         // and trying to load JSON or Script with a GET
3473         if ( s.dataType == "script" && type == "GET" && parts
3474             && ( parts[1] && parts[1] != location.protocol || parts[2] != location.host )){
3475
3476             var head = document.getElementsByTagName("head")[0];
3477             var script = document.createElement("script");
3478             script.src = s.url;
3479             if (s.scriptCharset)
3480                 script.charset = s.scriptCharset;
3481
3482             // Handle Script loading
3483             if ( !jsonp ) {
3484                 var done = false;
3485
3486                 // Attach handlers for all browsers
3487                 script.onload = script.onreadystatechange = function(){
3488                     if ( !done && (!this.readyState ||
3489                             this.readyState == "loaded" || this.readyState == "complete") ) {
3490                         done = true;
3491                         success();
3492                         complete();
3493
3494                         // Handle memory leak in IE
3495                         script.onload = script.onreadystatechange = null;
3496                         head.removeChild( script );
3497                     }
3498                 };
3499             }
3500
3501             head.appendChild(script);
3502
3503             // We handle everything using the script element injection
3504             return undefined;
3505         }


上面的代码是处理script格式的请求,别忘了,jsonp格式的请求在前面已经将格式设置为script了,因为jsonp格式的请求也是在这里处理的。第3469行的正则表达式取出url的协议和主机名。虽然这段代码比较长,但实现应该是很直观的,第3476-3480行创建script元素,并将它的src设置为s.url,第3501行将它添加到head元素中。对于script格式(非jsonp)的请求设置回调函数,在该回调函数中调用success()和complete()函数,它们调用用户定义的回调函数(第3483-3499行)。第3504,返回undefined,对jsonp和script格式的请求结束,由于它们没有使用XmlHttpRequest,因此返回undefined。

3507         var requestDone = false;
3508
3509         // Create the request object
3510         var xhr = s.xhr();
3511
3512         // Open the socket
3513         // Passing null username, generates a login popup on Opera (#2865)
3514         if( s.username )
3515             xhr.open(type, s.url, s.async, s.username, s.password);
3516         else
3517             xhr.open(type, s.url, s.async);
3518
3519         // Need an extra try/catch for cross domain requests in Firefox 3
3520         try {
3521             // Set the correct header, if data is being sent
3522             if ( s.data )
3523                 xhr.setRequestHeader("Content-Type", s.contentType);
3524
3525             // Set the If-Modified-Since header, if ifModified mode.
3526             if ( s.ifModified )
3527                 xhr.setRequestHeader("If-Modified-Since",
3528                     jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );
3529
3530             // Set header so the called script knows that it's an XMLHttpRequest
3531             xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
3532
3533             // Set the Accepts header for the server, depending on the dataType
3534             xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
3535                 s.accepts[ s.dataType ] + ", */*" :
3536                 s.accepts._default );
3537         } catch(e){}
3538
3539         // Allow custom headers/mimetypes and early abort
3540         if ( s.beforeSend && s.beforeSend(xhr, s) === false ) {
3541             // Handle the global AJAX counter
3542             if ( s.global && ! --jQuery.active )
3543                 jQuery.event.trigger( "ajaxStop" );
3544             // close opended socket
3545             xhr.abort();
3546             return false;
3547         }
3548
3549         if ( s.global )
3550             jQuery.event.trigger("ajaxSend", [xhr, s]);


上面的代码创建XmlHttpRequest(第3510行),打开连接(第3514-3517行),设置请求参数(第3520-3537行),触发全局事件(第3540-3550行),所有的全局事件仅在s.globa执行为true(这是默认值)时才会触发。用户可以定义beforeSend回调函数,如果它返回false,则不会发送请求(第3540-3547行)。

3552         // Wait for a response to come back
3553         var onreadystatechange = function(isTimeout){
3554             // The request was aborted, clear the interval and decrement jQuery.active
3555             if (xhr.readyState == 0) {
3556                 if (ival) {
3557                     // clear poll interval
3558                     clearInterval(ival);
3559                     ival = null;
3560                     // Handle the global AJAX counter
3561                     if ( s.global && ! --jQuery.active )
3562                         jQuery.event.trigger( "ajaxStop" );
3563                 }
3564             // The transfer is complete and the data is available, or the request timed out
3565             } else if ( !requestDone && xhr && (xhr.readyState == 4 || isTimeout == "timeout") ) {
3566                 requestDone = true;
3567
3568                 // clear poll interval
3569                 if (ival) {
3570                     clearInterval(ival);
3571                     ival = null;
3572                 }
3573
3574                 status = isTimeout == "timeout" ? "timeout" :
3575                     !jQuery.httpSuccess( xhr ) ? "error" :
3576                     s.ifModified && jQuery.httpNotModified( xhr, s.url ) ? "notmodified" :
3577                     "success";
3578
3579                 if ( status == "success" ) {
3580                     // Watch for, and catch, XML document parse errors
3581                     try {
3582                         // process the data (runs the xml through httpData regardless of callback)
3583                         data = jQuery.httpData( xhr, s.dataType, s );
3584                     } catch(e) {
3585                         status = "parsererror";
3586                     }
3587                 }
3588
3589                 // Make sure that the request was successful or notmodified
3590                 if ( status == "success" ) {
3591                     // Cache Last-Modified header, if ifModified mode.
3592                     var modRes;
3593                     try {
3594                         modRes = xhr.getResponseHeader("Last-Modified");
3595                     } catch(e) {} // swallow exception thrown by FF if header is not available
3596
3597                     if ( s.ifModified && modRes )
3598                         jQuery.lastModified[s.url] = modRes;
3599
3600                     // JSONP handles its own success callback
3601                     if ( !jsonp )
3602                         success();
3603                 } else
3604                     jQuery.handleError(s, xhr, status);
3605
3606                 // Fire the complete handlers
3607                 complete();
3608
3609                 if ( isTimeout )
3610                     xhr.abort();
3611
3612                 // Stop memory leaks
3613                 if ( s.async )
3614                     xhr = null;
3615             }
3616         };

3618         if ( s.async ) {
3619             // don't attach the handler to the request, just poll it instead
3620             var ival = setInterval(onreadystatechange, 13);
3621
3622             // Timeout checker
3623             if ( s.timeout > 0 )
3624                 setTimeout(function(){
3625                     // Check to see if the request is still happening
3626                     if ( xhr && !requestDone )
3627                         onreadystatechange( "timeout" );
3628                 }, s.timeout);
3629         }


定义onreadystate函数,但是要注意的是它并没有设置成xhr的onreadystatechange事件,相反,它使用setInterval每隔13毫秒轮询一次(第3619行),这是与我们通常使用的ajax实现不一样,我想jQuery这样做,大概是为了灵活性,这使得很容易处理请求超时(第3623-3628行),但超时后,会调用onreadystatechange( "timeout"),因此在onreadystatechange若检测到参数值为'timeout'时就表示请求已经超时,需要取消。

第3555-3563行处理请求放弃的情况,第3565-3615行处理请求完成或请求超时的情况。第3569-3572清除轮询,第3574-3577判断请求的状态,或者timeout或者error或者notmodified或者success。第3579-3604行的处理应该是很直观,有错就处理错误,成功就调用成功的函数。不管成功与否,最后都要调用complete()函数(第3607行),如果请求超时了还要放弃请求(第3609-1610行)。

3632         try {
3633             xhr.send(s.data);
3634         } catch(e) {
3635             jQuery.handleError(s, xhr, null, e);
3636         }
3637
3638         // firefox 1.5 doesn't fire statechange for sync requests
3639         if ( !s.async )
3640             onreadystatechange();
3641
3642         function success(){
3643             // If a local callback was specified, fire it and pass it the data
3644             if ( s.success )
3645                 s.success( data, status );
3646
3647             // Fire the global callback
3648             if ( s.global )
3649                 jQuery.event.trigger( "ajaxSuccess", [xhr, s] );
3650         }
3651
3652         function complete(){
3653             // Process result
3654             if ( s.complete )
3655                 s.complete(xhr, status);
3656
3657             // The request was completed
3658             if ( s.global )
3659                 jQuery.event.trigger( "ajaxComplete", [xhr, s] );
3660
3661             // Handle the global AJAX counter
3662             if ( s.global && ! --jQuery.active )
3663                 jQuery.event.trigger( "ajaxStop" );
3664         }
3665
3666         // return XMLHttpRequest to allow aborting the request etc.
3667         return xhr;
3668     },



t第3632-3636行发送请求,同步调用时手动触发onreadystatechange(第3639-3640行),3642-3650定义了成功时的处理,调用用户设置的成功回调函数,并触发全局事件。第3652-3664行定义了完成时处理,调用用户设置的完成回调函数,并触发全局事件。最后返回XmlHttpRequest实例xhr(第3667行)。

以上就是ajax函数的全部实现,也是jQuery的ajax实现之精华所在,其它的ajax函数都要调用这个函数,就不再讲了。


0 评论: