jquery.jsを読み解く

第4回jQueryライブラリ(770行目~1093行目)

jQuery.jsを読み解くの連載も今回で第4回となりました。

本題に入る前に少々豆知識です。jQueryは軽量なライブラリだと言われていますが、実際のところはどうなのでしょうか。ソースコードの行数をprototype.jsと比較してみると、prototype.jsの3,277行に対して、jQuery 1.2.2は3,383行です。ちょっと意外ですが、実はjQueryのほうが行数でみると多かったりします。行数が多いからといって高機能とはいえませんが、ライブラリを選択する際の参考にしてみてください。

それでは、さっそく続きのコードを見ていきましょう。

swap()

0770: // A method for quickly swapping in/out CSS properties to get correct calculations
0771: swap: function( elem, options, callback ) {
0772:   var old = {};
0773:   // Remember the old values, and insert the new ones
0774:   for ( var name in options ) {
0775:     old[ name ] = elem.style[ name ];
0776:     elem.style[ name ] = options[ name ];
0777:   }
0778:
0779:   callback.call( elem );
0780:
0781:   // Revert the old values
0782:   for ( var name in options )
0783:     elem.style[ name ] = old[ name ];
0784: },
0785:

jQuery.swapメソッドは、要素の大きさなどの属性を取得する際に、一時的に値を変更するための内部処理用メソッドです。次に説明するcssメソッドから利用されます。動作としては、774行目のfor文で古い値を保存しておいてから、引数optionsで渡された値に変更します。そして、779行目で引数として指定されたcallbackメソッドを実行します。最後に782行目のfor文で、属性を保存しておいた値に戻して完了です。

css()

0786: css: function( elem, name, force ) {
0787:   if ( name == "width" || name == "height" ) {
0788:     var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ];
0789:   
0790:     function getWH() {
0791:       val = name == "width" ? elem.offsetWidth : elem.offsetHeight;
0792:       var padding = 0, border = 0;
0793:       jQuery.each( which, function() {
0794:         padding += parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
0795:         border += parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
0796:       });
0797:       val -= Math.round(padding + border);
0798:     }
0799:   
0800:     if ( jQuery(elem).is(":visible") )
0801:       getWH();
0802:     else
0803:       jQuery.swap( elem, props, getWH );
0804:     
0805:     return Math.max(0, val);
0806:   }
0807:   
0808:   return jQuery.curCSS( elem, name, force );
0809: },
0810:

jQuery.cssメソッドは、選択された要素のスタイル属性値を取得するためのメソッドです。787行目にあるように"width"と"height"の値を取得したい場合だけ少し挙動が異なります。この2つの値を取得する場合で、対象となる要素がvisibility:"hidden", display:"none"など不可視状態の場合、先ほど説明したjQuery.swap()を呼び出して一時的にvisibility:"hidden", display:"block"の状態を作り出しています。こうしておいてから、790行目の関数getWH()で値を算出します。

また、"width"と"height"以外の値を取得する場合は、次に説明するjQuery.curCSS()を実行します(808行目⁠⁠。

curCSS()

jQuery.curCSSメソッドは、CSSの属性値を取得します。Internet ExplorerやFirefox、Operaといったブラウザごとの解釈の違いに対する対策が随所に見られます。cssのクロスブラウザに対する処理をまとめている部分となります。

0811: curCSS: function( elem, name, force ) {
0812:   var ret;
0813:
0814:   // A helper method for determining if an element's values are broken
0815:   function color( elem ) {
0816:     if ( !jQuery.browser.safari )
0817:       return false;
0818:
0819:     var ret = document.defaultView.getComputedStyle( elem, null );
0820:     return !ret || ret.getPropertyValue("color") == "";
0821:   }
0822:
0823:   // We need to handle opacity special in IE
0824:   if ( name == "opacity" && jQuery.browser.msie ) {
0825:     ret = jQuery.attr( elem.style, "opacity" );
0826:
0827:     return ret == "" ?
0828:       "1" :
0829:       ret;
0830:   }
0831:   // Opera sometimes will give the wrong display answer, this fixes it, see #2037
0832:   if ( jQuery.browser.opera && name == "display" ) {
0833:     var save = elem.style.display;
0834:     elem.style.display = "block";
0835:     elem.style.display = save;
0836:   }
0837:   
0838:   // Make sure we're using the right name for getting the float value
0839:   if ( name.match( /float/i ) )
0840:     name = styleFloat;
0841:

815行目は、Safariブラウザでcolorの値がうまく取得できない時にtrueを返す関数です。後で処理を切替える判定条件として利用されます。

824行目は、Internet Explorerでopacityを取得した際の挙動を他のブラウザと合わせるための処理です。

832行目は、Operaがときどき間違ったdisplay値を返す問題への対応です。不思議なことにdisplay値を設定し直すことで解消するようです。

838行目は、float値を取得する際にIEだけプロパティ名が異なるための処理です。styleFloatは1206行目で定義されていて、IEの場合はstyleFloatを利用します。

0842:   if ( !force && elem.style && elem.style[ name ] )
0843:     ret = elem.style[ name ];
0844:

forceがfalseか指定されていない場合は、そのまま取得した結果を返します。

0845:   else if ( document.defaultView && document.defaultView.getComputedStyle ) {
0846:
0847:     // Only "float" is needed here
0848:     if ( name.match( /float/i ) )
0849:       name = "float";
0850:
0851:     name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase();
0852:
0853:     var getComputedStyle = document.defaultView.getComputedStyle( elem, null );
0854:
0855:     if ( getComputedStyle && !color( elem ) )
0856:       ret = getComputedStyle.getPropertyValue( name );
0857:
0858:     // If the element isn't reporting its values properly in Safari
0859:     // then some display: none elements are involved
0860:     else {
0861:       var swap = [], stack = [];
0862:
0863:       // Locate all of the parent display: none elements
0864:       for ( var a = elem; a && color(a); a = a.parentNode )
0865:         stack.unshift(a);
0866:
0867:       // Go through and make them visible, but in reverse
0868:       // (It would be better if we knew the exact display type that they had)
0869:       for ( var i = 0; i 0870:         if ( color( stack[ i ] ) ) {
0871:           swap[ i ] = stack[ i ].style.display;
0872:           stack[ i ].style.display = "block";
0873:         }
0874:
0875:       // Since we flip the display style, we have to handle that
0876:       // one special, otherwise get the value
0877:       ret = name == "display" && swap[ stack.length - 1 ] != null ?
0878:         "none" :
0879:         ( getComputedStyle && getComputedStyle.getPropertyValue( name ) ) || "";
0880:
0881:       // Finally, revert the display styles back
0882:       for ( var i = 0; i 0883:         if ( swap[ i ] != null )
0884:           stack[ i ].style.display = swap[ i ];
0885:     }
0886:
0887:     // We should always get a number back from opacity
0888:     if ( name == "opacity" && ret == "" )
0889:       ret = "1";
0890:

IEにはdocument.defaultView.getComputedStyleがないため、ここはIE以外で行われる処理になります。848~851行目は、属性名の正規化を行います。例えば、floatが含まれるものをfloatに統一したり、"paddingTop"を正規表現で"padding-top"にしたりします。次に855行目ですが、getComputedStyleが、利用可能で先ほどのcolor()の結果がfalseならば(Safariの問題が発生しなければ⁠⁠、getPropertyValueを使って値を取得します。

860行目以降はSafariが適切な値を返さない場合の処理で、親要素でdisplay:hiddenなものを一度display属性値をblockに設定してから値を取得しています。

0891:   } else if ( elem.currentStyle ) {
0892:     var camelCase = name.replace(/\-(\w)/g, function(all, letter){
0893:       return letter.toUpperCase();
0894:     });
0895:
0896:     ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
0897:

891行目からはIEの場合の処理です。

892行目は先ほどの属性名の変更とは逆で、"padding-top"を"paddingTop"形式に変更します。そして、currentStyleを使って属性値を取得します。

0898:     // From the awesome hack by Dean Edwards
0899:     // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
0900:
0901:     // If we're not dealing with a regular pixel number
0902:     // but a number that has a weird ending, we need to convert it to pixels
0903:     if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
0904:       // Remember the original values
0905:       var style = elem.style.left, runtimeStyle = elem.runtimeStyle.left;
0906:
0907:       // Put in the new values to get a computed value out
0908:       elem.runtimeStyle.left = elem.currentStyle.left;
0909:       elem.style.left = ret || 0;
0910:       ret = elem.style.pixelLeft + "px";
0911:
0912:       // Revert the changed values
0913:       elem.style.left = style;
0914:       elem.runtimeStyle.left = runtimeStyle;
0915:     }
0916:   }
0917:
0918:   return ret;
0919: },
0920: 

903行目以降は、ピクセル指定以外の数値をピクセル単位で取得する処理です。IEのみでJavaScriptからruntimeStyle属性を上書き可能な性質を利用して、値をleftに設定してpixelLeftを取得することでピクセル値を算出します。

clean()

0921: clean: function( elems, context ) {
0922:   var ret = [];
0923:   context = context || document;
0924:   // !context.createElement fails in IE with an error but returns typeof 'object'
0925:   if (typeof context.createElement == 'undefined') 
0926:     context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
0927:
0928:   jQuery.each(elems, function(i, elem){
0929:     if ( !elem )
0930:       return;
0931:
0932:     if ( elem.constructor == Number )
0933:       elem = elem.toString();
0934:     
0935:     // Convert html string into DOM nodes
0936:     if ( typeof elem == "string" ) {
0937:       // Fix "XHTML"-style tags in all browsers
0938:       elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
0939:         return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
0940:           all :
0941:           front + "></" + tag + ">";
0942:       });
0943:
0944:       // Trim whitespace, otherwise indexOf won't work as expected
0945:       var tags = jQuery.trim( elem ).toLowerCase(), div = context.createElement("div");
0946:
0947:       var wrap =
0948:         // option or optgroup
0949:         !tags.indexOf("<opt") &&
0950:         [ 1, "<select multiple='multiple'>", "</select>" ] ||
0951:         
0952:         !tags.indexOf("<leg") &&
0953:         [ 1, "<fieldset>", "</fieldset>" ] ||
0954:         
0955:         tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
0956:         [ 1, "<table>", "</table>" ] ||
0957:         
0958:         !tags.indexOf("<tr") &&
0959:         [ 2, "<table><tbody>", "</tbody></table>" ] ||
0960:         
0961:          // <thead> matched above
0962:         (!tags.indexOf("<td") || !tags.indexOf("<th")) &&
0963:         [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||
0964:         
0965:         !tags.indexOf("<col") &&
0966:         [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||
0967:
0968:         // IE can't serialize <link> and <script> tags normally
0969:         jQuery.browser.msie &&
0970:         [ 1, "div<div>", "</div>" ] ||
0971:         
0972:         [ 0, "", "" ];
0973:
0974:       // Go to html and back, then peel off extra wrappers
0975:       div.innerHTML = wrap[1] + elem + wrap[2];
0976:       
0977:       // Move to the right depth
0978:       while ( wrap[0]-- )
0979:         div = div.lastChild;
0980:       
0981:       // Remove IE's autoinserted <tbody> from table fragments
0982:       if ( jQuery.browser.msie ) {
0983:         
0984:         // String was a <table>, *may* have spurious 
0985:         var tbody = !tags.indexOf("<table") && tags.indexOf("<tbody") < 0 ?
0986:           div.firstChild && div.firstChild.childNodes :
0987:           
0988:           // String was a bare <thead> or <tfoot>
0989:           wrap[1] == "<table>" && tags.indexOf("<tbody") < 0 ?
0990:             div.childNodes :
0991:             [];
0992:       
0993:         for ( var j = tbody.length - 1; j >= 0 ; --j )
0994:           if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length )
0995:             tbody[ j ].parentNode.removeChild( tbody[ j ] );
0996:         
0997:         // IE completely kills leading whitespace when innerHTML is used  
0998:         if ( /^\s/.test( elem ) )  
0999:           div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild );
1000:       
1001:       }
1002:       
1003:       elem = jQuery.makeArray( div.childNodes );
1004:     }
1005:
1006:     if ( elem.length === 0 && (!jQuery.nodeName( elem, "form" ) && !jQuery.nodeName( elem, "select" )) )
1007:       return;
1008:
1009:     if ( elem[0] == undefined || jQuery.nodeName( elem, "form" ) || elem.options )
1010:       ret.push( elem );
1011:
1012:     else
1013:       ret = jQuery.merge( ret, elem );
1014:
1015:   });
1016:
1017:   return ret;
1018: },
1019: 


jQuery.cleanメソッドは、内部でHTMLを利用しやすいように加工するためのメソッドです。

925行目では、contextにdocument以外が渡された場合、Internet ExplorerではcreateElementが失敗するため、contextの値を再設定しています。そして、928行目以降で渡されたすべての要素に対して、以下の処理を適用していきます。

  • 937~942行目:空要素タグを分割
  • 944~979行目:特定のタグの場合に適切なタグで囲む
  • 981~1004行目:Internet Exploerで自動的に挿入されるtbodyタグを取り除く、innerHTMLで先頭の空白文字列を保存
  • 1006~1013行目:formまたはselectがリターン用配列への格納

attr()

1020: attr: function( elem, name, value ) {
1021:   // don't set attributes on text and comment nodes
1022:   if (!elem || elem.nodeType == 3 || elem.nodeType == 8)
1023:     return undefined;
1024:
1025:   var fix = jQuery.isXMLDoc( elem ) ?
1026:     {} :
1027:     jQuery.props;
1028:
1029:   // Safari mis-reports the default selected property of a hidden option
1030:   // Accessing the parent's selectedIndex property fixes it
1031:   if ( name == "selected" && jQuery.browser.safari )
1032:     elem.parentNode.selectedIndex;
1033:   
1034:   // Certain attributes only work when accessed via the old DOM 0 way
1035:   if ( fix[ name ] ) {
1036:     if ( value != undefined )
1037:       elem[ fix[ name ] ] = value;
1038:
1039:     return elem[ fix[ name ] ];
1040:
1041:   } else if ( jQuery.browser.msie && name == "style" )
1042:     return jQuery.attr( elem.style, "cssText", value );
1043:
1044:   else if ( value == undefined && jQuery.browser.msie && jQuery.nodeName( elem, "form" ) && (name == "action" || name == "method") )
1045:     return elem.getAttributeNode( name ).nodeValue;
1046:
1047:   // IE elem.getAttribute passes even for style
1048:   else if ( elem.tagName ) {
1049:
1050:     if ( value != undefined ) {
1051:       // We can't allow the type property to be changed (since it causes problems in IE)
1052:       if ( name == "type" && jQuery.nodeName( elem, "input" ) && elem.parentNode )
1053:         throw "type property can't be changed";
1054:
1055:       // convert the value to a string (all browsers do this but IE) see #1070
1056:       elem.setAttribute( name, "" + value );
1057:     }
1058:
1059:     if ( jQuery.browser.msie && /href|src/.test( name ) && !jQuery.isXMLDoc( elem ) ) 
1060:       return elem.getAttribute( name, 2 );
1061:
1062:     return elem.getAttribute( name );
1063:
1064:   // elem is actually elem.style ... set the style
1065:   } else {
1066:     // IE actually uses filters for opacity
1067:     if ( name == "opacity" && jQuery.browser.msie ) {
1068:       if ( value != undefined ) {
1069:         // IE has trouble with opacity if it does not have layout
1070:         // Force it by setting the zoom level
1071:         elem.zoom = 1; 
1072: 
1073:         // Set the alpha filter to set the opacity
1074:         elem.filter = (elem.filter || "").replace( /alpha\([^)]*\)/, "" ) +
1075:           (parseFloat( value ).toString() == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")");
1076:       }
1077: 
1078:       return elem.filter && elem.filter.indexOf("opacity=") >= 0 ?
1079:         (parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100).toString() :
1080:         "";
1081:     }
1082:
1083:     name = name.replace(/-([a-z])/ig, function(all, letter){
1084:       return letter.toUpperCase();
1085:     });
1086:
1087:     if ( value != undefined )
1088:       elem[ name ] = value;
1089:
1090:     return elem[ name ];
1091:   }
1092: },
1093: 

jQuery.attrメソッドは、属性値を扱うためのメソッドです。

1021~1023行目は、nodeTypeがテキストまたはコメントの場合は属性値は存在しないためundefinedを返します。

1029~1032行目は、Safariでselected値を取得しようとした時に発生する不具合を回避するため、親要素のselectedIndex値にアクセスしています。

1034~1040行目は、jQuery.propsで定義されている属性(1214行目)についてはDOM0に従ってアクセスします。

1041~1042行目は、IEからstyle属性にアクセスしようとしている場合は、jQuery.attr( elem.style, "cssText", value )として自分自身を呼び出します。

1044~1045行目は、IEからformのactionまたはmethodを取得しようとしている場合の処理です。

1047~1062行目は、上記以外の場合で、値を設定したり取得する場合の処理。input要素のtypeを変更しようとした場合には例外を発生させます。

1065~1081行目は、IEでopacityを扱う場合の処理です。

1083~1090行目は、属性名をキャメライズして、value引数が指定されていれば値を設定し、属性名値を返します。

以上でjQueryのコード全体の三分の一が終了しました。だいぶ全体像が見えてきたのではないかと思います。今回はクロスブラウザのためのバッドノウハウ的な部分が多く、泥臭い印象があったかもしれませんが、また次回をお楽しみに。

おすすめ記事

記事・ニュース一覧