jquery.jsを読み解く

第12回jQueryライブラリ(2530行目~2833行目)

前回からの今回までの間にjQuery 1.2.4および1.2.5がリリースされました。開発者のJohn Resigによると、1.2.4にはビルド上の不具合があったために、1.2.5をリリースしたとのことで、1.2.4に関しては利用しないほうが賢明です。

大きな変更点としては、width、heightやoffsetsなどの扱いを拡張するDimensionsプラグインのコアライブラリへの統合とイベントハンドリングの改良によるスピード向上が図られています。この改良により、ドラッグアンドドロップの処理が今までよりも3倍速くなっているそうです。また、夏にはjQuery 1.3のリリースも予定されていて、こちらもセレクタとDOM操作の改善によりさらなるスピードアップが期待できそうです。

ところで、この「jQuery.jsを読み解く」では、連載開始時のバージョン1.2.2を前提に進めています。しかし、現在の最新版である1.2.5と共通の部分がほとんどですので、ぜひ最新版のソースコードを読む際にも本記事を参考にしていただければと思います。それでは、2530行目からのAjax関連処理を実装している部分の解説に入っていきましょう。

jQuery.ajax()

jQuery.ajax()メソッドは、jQueryで用意されている様々なAjaxリクエストを処理するための部分です。

2530:   // Last-Modified header cache for next request
2531:   lastModified: {},
2532: 
2533:   ajax: function( s ) {
2534:     var jsonp, jsre = /=\?(&|$)/g, status, data;
2535: 
2536:     // Extend the settings, but re-extend 's' so that it can be
2537:     // checked again later (in the test suite, specifically)
2538:     s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));
2539: 
2540:     // convert data if not already a string
2541:     if ( s.data && s.processData && typeof s.data != "string" )
2542:       s.data = jQuery.param(s.data);
2543: 

2531行目では、次回のリクエストのためにLast-Modifiedヘッダを格納するための変数を用意しています。

そして、2533行目からがjQuery.ajax()メソッドの定義になります。2534行目では、変数の初期化を行っています。jsreとはJSONPのコールバック関数名を置換するための正規表現です。

2544:     // Handle JSONP Parameter Callbacks
2545:     if ( s.dataType == "jsonp" ) {
2546:       if ( s.type.toLowerCase() == "get" ) {
2547:         if ( !s.url.match(jsre) )
2548:           s.url += (s.url.match(/\?/) ? "&" : "?") + (s.jsonp || "callback") + "=?";
2549:       } else if ( !s.data || !s.data.match(jsre) )
2550:         s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
2551:       s.dataType = "json";
2552:     }
2553: 
2554:     // Build temporary JSONP function
2555:     if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) {
2556:       jsonp = "jsonp" + jsc++;
2557: 
2558:       // Replace the =? sequence both in the query string and the data
2559:       if ( s.data )
2560:         s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
2561:       s.url = s.url.replace(jsre, "=" + jsonp + "$1");
2562: 
2563:       // We need to make sure
2564:       // that a JSONP style response is executed properly
2565:       s.dataType = "script";
2566: 
2567:       // Handle JSONP-style loading
2568:       window[ jsonp ] = function(tmp){
2569:         data = tmp;
2570:         success();
2571:         complete();
2572:         // Garbage collect
2573:         window[ jsonp ] = undefined;
2574:         try{ delete window[ jsonp ]; } catch(e){}
2575:         if ( head )
2576:           head.removeChild( script );
2577:       };
2578:     }
2579: 

2545~2552行目でJSONP形式でデータを受け取るための前処理を行います。リクエストメソッドがGETで、クエリー文字列に"=?"が含まれていない場合は"callback=?"形式のパラメータを追加します。jsonpプロパティが指定されていれば、引数名としてその値を利用し、指定されていなければデフォルトのcallbackを利用します。また、GET以外のメソッドでcallback関数名の指定がなければ、dataにcallback関数の指定を追加します。そして、2551行目でdataTypeを"json"に変更します。

2555~2578行目は、dataTypeが"json"かつパラメータに"callback=?"形式の指定がある場合の処理で、callback関数名の置換と実際のコールバック関数の登録を行います。2556行目にて"json1210664936115"のようなコールバック関数名を確保します。そして、2559行目でクエリー文字列と送信データ内の"=?"をコールバック関数名に置き換えます。そして、後ほどのためにdataTypeを"script"に置き換えておきます。

2568行目でwindowオブジェクトにコールバック関数を先ほど生成した名前で登録します。関数の処理内容としては、success(),complete()の順に処理を実行していき、最後に自身を削除しています。完全に削除できるように、2573行目でwindow[jsonp]にundefinedを設定した後に、deleteを実行、head内のscriptタグを除去しています。success, completeに関しては、それぞれ2752行目、2762行目で説明します。

2580:     if ( s.dataType == "script" && s.cache == null )
2581:       s.cache = false;
2582: 
2583:     if ( s.cache === false && s.type.toLowerCase() == "get" ) {
2584:       var ts = (new Date()).getTime();
2585:       // try replacing _= if it is there
2586:       var ret = s.url.replace(/(\?|&)_=.*?(&|$)/, "$1_=" + ts + "$2");
2587:       // if nothing was replaced, add timestamp to the end
2588:       s.url = ret + ((ret == s.url) ? (s.url.match(/\?/) ? "&" : "?") + "_=" + ts : "");
2589:     }
2590: 

2580行目からは、ブラウザのキャッシュを抑制するためのパラメータを付加する処理になります。2580行目によりdataTypeが"script"でキャッシュに関する指定がない場合はキャッシュ不可とします。

また、リクエストメソッドがGETでキャッシュ不可の場合は、"_=1210664936115"形式のパラメータを付加します。もし、"_="のパラメータが既に存在していれば2586行目でそれを置換し、なければ2588行目で追加します。

2591:     // If data is available, append data to url for get requests
2592:     if ( s.data && s.type.toLowerCase() == "get" ) {
2593:       s.url += (s.url.match(/\?/) ? "&" : "?") + s.data;
2594: 
2595:       // IE likes to send both get and post data, prevent this
2596:       s.data = null;
2597:     }
2598: 

2592行目からは、リクエストメソッドがGETでdataパラメータが指定されている場合の処理で、この場合はURLパラメータにdataを追加し、dataプロパティはクリアします。

2599:     // Watch for a new set of requests
2600:     if ( s.global && ! jQuery.active++ )
2601:       jQuery.event.trigger( "ajaxStart" );
2602: 

2601行目は、ajaxStartイベントのトリガー処理になります。globalイベントを実行するかどうかのフラグがtrueで、jQuery.activ=0つまり既に実行中のクエリーがない場合にajaxStartイベントを発生させます。

2603:     // If we're requesting a remote document
2604:     // and trying to load JSON or Script with a GET
2605:     if ( (!s.url.indexOf("http") || !s.url.indexOf("//")) && ( s.dataType == "script" || s.dataType =="json" ) && s.type.toLowerCase() == "get" ) {
2606:       var head = document.getElementsByTagName("head")[0];
2607:       var script = document.createElement("script");
2608:       script.src = s.url;
2609:       if (s.scriptCharset)
2610:         script.charset = s.scriptCharset;
2611: 
2612:       // Handle Script loading
2613:       if ( !jsonp ) {
2614:         var done = false;
2615: 
2616:         // Attach handlers for all browsers
2617:         script.onload = script.onreadystatechange = function(){
2618:           if ( !done && (!this.readyState || 
2619:               this.readyState == "loaded" || this.readyState == "complete") ) {
2620:             done = true;
2621:             success();
2622:             complete();
2623:             head.removeChild( script );
2624:           }
2625:         };
2626:       }
2627: 
2628:       head.appendChild(script);
2629: 
2630:       // We handle everything using the script element injection
2631:       return undefined;
2632:     }
2633: 

2605行目からは、JSONまたはScriptタグの読み込み処理です。リクエストURLが"http"もしくは"//"で始まっていて、データ形式が"script"または"json"で、メソッドが"GET"の場合にheadタグ内に挿入する<script src="~">を準備します。また、s.scriptCharsetが指定されていれば2610行目でcharsetを設定します。2613~2626行目は、jsonpのハンドラが設定されていない場合の処理で、ロード完了時にsuccess()およびcomplete()メソッドを実行し、追加したscriptタグを除去する関数を設定します。

2628行目で作成したscriptタグを実際に追加します。

2634:     var requestDone = false;
2635: 
2636:     // Create the request object; Microsoft failed to properly
2637:     // implement the XMLHttpRequest in IE7, so we use the ActiveXObject when it is available
2638:     var xml = window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
2639: 
2640:     // Open the socket
2641:     xml.open(s.type, s.url, s.async, s.username, s.password);
2642: 

2634行目のrequestDoneはリクエストを送信済みかどうかのフラグで、2674行目でtrueに設定されます。また、Internet Explorer7からXMLHttpRequestも利用できるようになったのですが、実装に問題があるため、ActiveXObjectが利用可能であればそちらを使うようにします。2641行目で渡されてきたパラメータに従って、通信用のソケットを開きます。

2643:     // Need an extra try/catch for cross domain requests in Firefox 3
2644:     try {
2645:       // Set the correct header, if data is being sent
2646:       if ( s.data )
2647:         xml.setRequestHeader("Content-Type", s.contentType);
2648: 
2649:       // Set the If-Modified-Since header, if ifModified mode.
2650:       if ( s.ifModified )
2651:         xml.setRequestHeader("If-Modified-Since",
2652:           jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );
2653: 
2654:       // Set header so the called script knows that it's an XMLHttpRequest
2655:       xml.setRequestHeader("X-Requested-With", "XMLHttpRequest");
2656: 
2657:       // Set the Accepts header for the server, depending on the dataType
2658:       xml.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
2659:         s.accepts[ s.dataType ] + ", */*" :
2660:         s.accepts._default );
2661:     } catch(e){}
2662: 

2644行目からは、追加ヘッダの設定です。

2647行目は、s.dataが渡されている場合に、"Content-Type"ヘッダを設定します。

2651:行目は、s.ifModifiedがtrueの場合に、"If-Modified-Since"ヘッダを設定します。もし、前回リクエスト時のものがあればその日時を、なければ1970年1月1日を指定します。

2655行目は、XMLHttpRequestを使ってリクエストを送っていることをサーバに知らせるための"X-Requested-With"ヘッダを追加しています。

2658行目は、期待するdataTypeをサーバに伝えるための"Accept"ヘッダを追加しています。s.accepts[ s.dataType ]が設定されていればその値を、なければデフォルトの"*/*"が設定されます。

2663:     // Allow custom headers/mimetypes
2664:     if ( s.beforeSend )
2665:       s.beforeSend(xml);
2666:       
2667:     if ( s.global )
2668:       jQuery.event.trigger("ajaxSend", [xml, s]);
2669: 

もし、リクエストを送る前に行うbeforeSendメソッドが定義されていれば、これを実行します。

そして、2668行目にて、これまでにも度々登場しているglobalフラグがtrueであれば、ajaxSendイベントを発生させます。

2670:     // Wait for a response to come back
2671:     var onreadystatechange = function(isTimeout){
2672:       // The transfer is complete and the data is available, or the request timed out
2673:       if ( !requestDone && xml && (xml.readyState == 4 || isTimeout == "timeout") ) {
2674:         requestDone = true;
2675:         
2676:         // clear poll interval
2677:         if (ival) {
2678:           clearInterval(ival);
2679:           ival = null;
2680:         }
2681:         
2682:         status = isTimeout == "timeout" && "timeout" ||
2683:           !jQuery.httpSuccess( xml ) && "error" ||
2684:           s.ifModified && jQuery.httpNotModified( xml, s.url ) && "notmodified" ||
2685:           "success";
2686: 
2687:         if ( status == "success" ) {
2688:           // Watch for, and catch, XML document parse errors
2689:           try {
2690:             // process the data (runs the xml through httpData regardless of callback)
2691:             data = jQuery.httpData( xml, s.dataType );
2692:           } catch(e) {
2693:             status = "parsererror";
2694:           }
2695:         }
2696: 
2697:         // Make sure that the request was successful or notmodified
2698:         if ( status == "success" ) {
2699:           // Cache Last-Modified header, if ifModified mode.
2700:           var modRes;
2701:           try {
2702:             modRes = xml.getResponseHeader("Last-Modified");
2703:           } catch(e) {} // swallow exception thrown by FF if header is not available
2704:   
2705:           if ( s.ifModified && modRes )
2706:             jQuery.lastModified[s.url] = modRes;
2707: 
2708:           // JSONP handles its own success callback
2709:           if ( !jsonp )
2710:             success();  
2711:         } else
2712:           jQuery.handleError(s, xml, status);
2713: 
2714:         // Fire the complete handlers
2715:         complete();
2716: 
2717:         // Stop memory leaks
2718:         if ( s.async )
2719:           xml = null;
2720:       }
2721:     };
2722:     

2670~2721行目は、Ajaxリクエストのレスポンスを受け取る処理で、onreadystatechangeという関数を定義しています。2673行目のif文は、まだリクエスト前でかつXMLHttpRequestオブジェクトが存在していて、readyStateが"4"つまりCOMPLETEDの場合またはタイムアウトになったかどうかを判定し、該当するときのみ以降の処理が実行されます。

2674行目は、リクエストが完了したかどうかのフラグでこれをtrueに設定しています。この変数はタイムアウトの判定に利用されます。

2677行目は、Ajaxレスポンス監視タイマーのクリア処理です。2725行目で設定するタイマーが定義されていれば、これを解除します。

2682~2685行目は、status変数の設定です。少々紛らわしいのですが、isTimeout変数が"timeout"であれば"timeout"を、jQuery.httpSuccess()の判定結果がfalseであれば"error"を、s.ifModifiedが設定されていてjQuery.NotModified()の結果がtrueであれば"notmodified"を、それ以外の場合には"success"を返します。

2687~2695行目は、結果が"success"だった場合に、2815行目で定義されているjQuery.httpData()メソッドを使ってデータを取り込みます。何かエラーが発生した場合は、statusに"parseerror"を設定します。

2697行目からは、statusが"success"の場合の処理で、2720行目でLast-Modifiedヘッダを調べてmodRes変数に格納します。そして、s.ifModifiedフラグがtrueであれば、2706行目で次回の処理のためにjQueryオブジェクト内に保存しておきます。

2710行目は、JSONPリクエスト以外の場合にsuccess()メソッドをコールします。

2712行目は、statusが"success"以外の場合にはエラーとして、jQuery.handleError()をコールします。

2715行目は、リクエストが成功しても失敗しても呼び出されるcomplete()メソッドを実行しています。

最後に非同期呼び出しの場合にメモリリークを避けるために、2719行目でXMLHttpRequestインスタンスを削除しておきます。

2723:     if ( s.async ) {
2724:       // don't attach the handler to the request, just poll it instead
2725:       var ival = setInterval(onreadystatechange, 13); 
2726: 
2727:       // Timeout checker
2728:       if ( s.timeout > 0 )
2729:         setTimeout(function(){
2730:           // Check to see if the request is still happening
2731:           if ( xml ) {
2732:             // Cancel the request
2733:             xml.abort();
2734:   
2735:             if( !requestDone )
2736:               onreadystatechange( "timeout" );
2737:           }
2738:         }, s.timeout);
2739:     }
2740:       

2723~2739行目は、サーバからレスポンスが返ってくるのを監視するための処理です。s.asyncは非同期通信を行うかどうかのフラグで、デフォルトで"true"です。2725行目で、setInterval()を使って先ほどのonreadystatechange関数を13ミリ秒おきに実行します。ここで、なぜブラウザ標準のonreadystatechangeイベントを使わずにタイマーを使って監視するのかということですが、Internet Explorerで深刻なメモリーリークが発生することへの対処のようです。Dojoライブラリでも同様にタイマーによる監視を行っています。

次に2728行目でタイムアウト時間(s.timeout)が指定されていれば、s.timeout経過後にxml.abort()メソッドを使ってリクエストをキャンセルします。

2741:     // Send the data
2742:     try {
2743:       xml.send(s.data);
2744:     } catch(e) {
2745:       jQuery.handleError(s, xml, null, e);
2746:     }
2747:     
2748:     // firefox 1.5 doesn't fire statechange for sync requests
2749:     if ( !s.async )
2750:       onreadystatechange();
2751: 
2752:     function success(){
2753:       // If a local callback was specified, fire it and pass it the data
2754:       if ( s.success )
2755:         s.success( data, status );
2756: 
2757:       // Fire the global callback
2758:       if ( s.global )
2759:         jQuery.event.trigger( "ajaxSuccess", [xml, s] );
2760:     }
2761: 

2743行目で実際にリクエストを送信します。もし、何かしらのエラーが発生した場合はjQuery.handleError()メソッドが呼び出されます。

2750行目では、Firefoxで同期的なリクエストを行った際にonreadystatechange()が発生しないので、onreadystatechangeを自分で呼び出しています。

2752行目からは、Ajaxリクエストが成功した場合に実行されるsuccess()メソッドの定義です。もし、パラメータs.successが指定されていれば、その関数success()をdataおよびstatusを引数として実行します。次に2759行目では、Ajax処理全体に適用されるイベント処理を行うかどうかのフラグs.globalがtrueであれば、ajaxSuccess()イベントを発生させます。

2762:     function complete(){
2763:       // Process result
2764:       if ( s.complete )
2765:         s.complete(xml, status);
2766: 
2767:       // The request was completed
2768:       if ( s.global )
2769:         jQuery.event.trigger( "ajaxComplete", [xml, s] );
2770: 
2771:       // Handle the global AJAX counter
2772:       if ( s.global && ! --jQuery.active )
2773:         jQuery.event.trigger( "ajaxStop" );
2774:     }
2775:     
2776:     // return XMLHttpRequest to allow aborting the request etc.
2777:     return xml;
2778:   },
2779: 

2762~2778行目は、Ajaxリクエストが完了した場合に実行されるcomplete()メソッドの処理をを実行する処理になります。内容的には先ほどのsuccess()とほぼ同様で、s.completeが指定されていれば2765行目で実行します。また、s.globalがtrueであればajaxCompleteイベントを発生させます。

また、2772行目は現在実行中のAjaxリクエスト数を保持しておくカウンタjQuery.activeをカウントダウンして0になったらAjaxStopイベントを発生させます。

最後に2777行目で、XMLHttpRequestオブジェクトを返して終了です。ここで、XMLHttpRequestオブジェクトを返すのは、Ajaxリクエストを中止できるようにするためです。

jQuery.handleError()

2780:   handleError: function( s, xml, status, e ) {
2781:     // If a local callback was specified, fire it
2782:     if ( s.error ) s.error( xml, status, e );
2783: 
2784:     // Fire the global callback
2785:     if ( s.global )
2786:       jQuery.event.trigger( "ajaxError", [xml, s, e] );
2787:   },
2788: 

2780行目からは、jQuery.handleError()メソッドの定義です。Ajaxリクエストがエラーになった場合のメソッド定義です。第1引数sにerrorメソッドが定義されていれば、2782行目にてそのメソッドを実行します。また、globalプロパティがtrueであれば(デフォルトはtrue⁠⁠、2786行目でajaxErrorイベントを実行します。

jQuery.httpSuccess()

2789:   // Counter for holding the number of active queries
2790:   active: 0,
2791: 
2792:   // Determines if an XMLHttpRequest was successful or not
2793:   httpSuccess: function( r ) {
2794:     try {
2795:       // IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450
2796:       return !r.status && location.protocol == "file:" ||
2797:         ( r.status >= 200 && r.status 2798:         jQuery.browser.safari && r.status == undefined;
2799:     } catch(e){}
2800:     return false;
2801:   },
2802: 

2790行目のactiveは、実行中のajaxクエリの数を保持しておくためのカウンタです。

2793行目からは、jQuery.httpSuccess()メソッドの定義で、XMLHttpRequestが成功したかどうかを判定します。プロトコルが"file://"で始まるローカルファイル、ステータスコードが200番台、"304 Not Modified"、もしくはInternet ExplorerでPUTリクエストの結果として稀に返ってくる"1223"、ブラウザがSafariの場合で"undefined"が返ってきた場合に、このメソッドはtrueを返します。それ以外の場合や何かエラーが発生した場合は、2800行目でfalseを返します。

jQuery.httpNotModified()

2803:   // Determines if an XMLHttpRequest returns NotModified
2804:   httpNotModified: function( xml, url ) {
2805:     try {
2806:       var xmlRes = xml.getResponseHeader("Last-Modified");
2807: 
2808:       // Firefox always returns 200. check Last-Modified date
2809:       return xml.status == 304 || xmlRes == jQuery.lastModified[url] ||
2810:         jQuery.browser.safari && xml.status == undefined;
2811:     } catch(e){}
2812:     return false;
2813:   },
2814: 

2804行目からは、jQuery.httpNotModified()メソッドの定義で、XMLHttpRequestが「Not Modified」の応答を受け取ったかどうかを判定します。2806行目でLast-Modifiedヘッダの値を取得します。これはFirefoxでは常にステータスコード200が返ってくるため、比較に利用するためです。そして、2809行目でステータスコードが304か保存しておいたlastModifiedとLast-Modifiedヘッダの値が等いか、またはブラウザがSafariの場合でステータスコードがundefinedの場合にはtrueを返します。それ以外の場合、または何かしらのエラーが発生した場合はfalseを返します。

jQuery.httpData()

2815:   httpData: function( r, type ) {
2816:     var ct = r.getResponseHeader("content-type");
2817:     var xml = type == "xml" || !type && ct && ct.indexOf("xml") >= 0;
2818:     var data = xml ? r.responseXML : r.responseText;
2819: 
2820:     if ( xml && data.documentElement.tagName == "parsererror" )
2821:       throw "parsererror";
2822: 
2823:     // If the type is "script", eval it in global context
2824:     if ( type == "script" )
2825:       jQuery.globalEval( data );
2826: 
2827:     // Get the JavaScript object, if JSON is used.
2828:     if ( type == "json" )
2829:       data = eval("(" + data + ")");
2830: 
2831:     return data;
2832:   },
2833: 

2815行目からのjQuery.httpData()メソッドは、XMLHttpRequestにて受け取ったデータを処理します。2816行目で"content-type"ヘッダを取得します。2817行目では、第2引数typeが"xml"であるか"content-type"ヘッダに"xml"が含まれていればXML形式のデータであると判定します。2818行目は、XML形式であればresponseXMLを、そうでなければresponseTextを取得します。

2820行目は、レスポンスデータがXML形式の場合にパースエラーが発生したら、パースエラーを投げます。

2825行目は、レスポンスデータが"script"の場合にglobalEval()メソッドを利用してスクリプトを実行します。

2829行目は、レスポンスデータがJSON形式の場合にeval()関数を用いてデータを取り込みます。ここでdataを括弧でくくっているのは、eval()関数は引数を式として評価するためです。

最後に2831行目で、dataを返してこのメソッドは終了です。

おすすめ記事

記事・ニュース一覧