前回からの今回までの間に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:
2531: lastModified: {},
2532:
2533: ajax: function ( s ) {
2534: var jsonp, jsre = /=\?(&|$)/g , status, data;
2535:
2536:
2537:
2538: s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));
2539:
2540:
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:
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:
2555: if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) {
2556: jsonp = "jsonp" + jsc++;
2557:
2558:
2559: if ( s.data )
2560: s.data = (s.data + "" ).replace(jsre, "=" + jsonp + "$1" );
2561: s.url = s.url.replace(jsre, "=" + jsonp + "$1" );
2562:
2563:
2564:
2565: s.dataType = "script" ;
2566:
2567:
2568: window[ jsonp ] = function (tmp){
2569: data = tmp;
2570: success();
2571: complete();
2572:
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:
2586: var ret = s.url.replace(/(\?|&)_=.*?(&|$)/ , "$1_=" + ts + "$2" );
2587:
2588: s.url = ret + ((ret == s.url) ? (s.url.match(/\?/ ) ? "&" : "?" ) + "_=" + ts : "" );
2589: }
2590:
2580行目からは、ブラウザのキャッシュを抑制するためのパラメータを付加する処理になります。2580行目によりdataTypeが"script"でキャッシュに関する指定がない場合はキャッシュ不可とします。
また、リクエストメソッドがGETでキャッシュ不可の場合は、"_=1210664936115"形式のパラメータを付加します。もし、"_="のパラメータが既に存在していれば2586行目でそれを置換し、なければ2588行目で追加します。
2591:
2592: if ( s.data && s.type.toLowerCase() == "get" ) {
2593: s.url += (s.url.match(/\?/ ) ? "&" : "?" ) + s.data;
2594:
2595:
2596: s.data = null;
2597: }
2598:
2592行目からは、リクエストメソッドがGETでdataパラメータが指定されている場合の処理で、この場合はURLパラメータにdataを追加し、dataプロパティはクリアします。
2599:
2600: if ( s.global && ! jQuery.active++ )
2601: jQuery.event.trigger( "ajaxStart" );
2602:
2601行目は、ajaxStartイベントのトリガー処理になります。globalイベントを実行するかどうかのフラグがtrueで、jQuery.activ=0つまり既に実行中のクエリーがない場合にajaxStartイベントを発生させます。
2603:
2604:
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:
2613: if ( !jsonp ) {
2614: var done = false;
2615:
2616:
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:
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:
2637:
2638: var xml = window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP" ) : new XMLHttpRequest();
2639:
2640:
2641: xml.open(s.type, s.url, s.async, s.username, s.password);
2642:
2634行目のrequestDoneはリクエストを送信済みかどうかのフラグで、2674行目でtrueに設定されます。また、Internet Explorer7からXMLHttpRequestも利用できるようになったのですが、実装に問題があるため、ActiveXObjectが利用可能であればそちらを使うようにします。2641行目で渡されてきたパラメータに従って、通信用のソケットを開きます。
2643:
2644: try {
2645:
2646: if ( s.data )
2647: xml.setRequestHeader("Content-Type" , s.contentType);
2648:
2649:
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:
2655: xml.setRequestHeader("X-Requested-With" , "XMLHttpRequest" );
2656:
2657:
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:
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:
2671: var onreadystatechange = function (isTimeout){
2672:
2673: if ( !requestDone && xml && (xml.readyState == 4 || isTimeout == "timeout" ) ) {
2674: requestDone = true;
2675:
2676:
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:
2689: try {
2690:
2691: data = jQuery.httpData( xml, s.dataType );
2692: } catch(e) {
2693: status = "parsererror" ;
2694: }
2695: }
2696:
2697:
2698: if ( status == "success" ) {
2699:
2700: var modRes;
2701: try {
2702: modRes = xml.getResponseHeader("Last-Modified" );
2703: } catch(e) {}
2704:
2705: if ( s.ifModified && modRes )
2706: jQuery.lastModified[s.url] = modRes;
2707:
2708:
2709: if ( !jsonp )
2710: success();
2711: } else
2712: jQuery.handleError(s, xml, status);
2713:
2714:
2715: complete();
2716:
2717:
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:
2725: var ival = setInterval(onreadystatechange, 13);
2726:
2727:
2728: if ( s.timeout > 0 )
2729: setTimeout(function (){
2730:
2731: if ( xml ) {
2732:
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:
2742: try {
2743: xml.send(s.data);
2744: } catch(e) {
2745: jQuery.handleError(s, xml, null, e);
2746: }
2747:
2748:
2749: if ( !s.async )
2750: onreadystatechange();
2751:
2752: function success(){
2753:
2754: if ( s.success )
2755: s.success( data, status );
2756:
2757:
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:
2764: if ( s.complete )
2765: s.complete(xml, status);
2766:
2767:
2768: if ( s.global )
2769: jQuery.event.trigger( "ajaxComplete" , [xml, s] );
2770:
2771:
2772: if ( s.global && ! --jQuery.active )
2773: jQuery.event.trigger( "ajaxStop" );
2774: }
2775:
2776:
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:
2782: if ( s.error ) s.error( xml, status, e );
2783:
2784:
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:
2790: active: 0,
2791:
2792:
2793: httpSuccess: function ( r ) {
2794: try {
2795:
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:
2804: httpNotModified: function ( xml, url ) {
2805: try {
2806: var xmlRes = xml.getResponseHeader("Last-Modified" );
2807:
2808:
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:
2824: if ( type == "script" )
2825: jQuery.globalEval( data );
2826:
2827:
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を返してこのメソッドは終了です。