Kanasan.JS jQuery コードリーディング #2 に参加したので復習とコードの解説をする

2010/09/26(日) に京都で Kanasan.JS jQuery コードリーディング #2 が行われた(告知ページ)。みんなで jQueryソースコードを読む勉強会で、僕は初めての参加。事前に前回分も読んでおいたので準備万端、気合いを入れて参加した。

で、今回の内容なんだけど、642行目から1205行目まで読み終えた。全部で6240行なので全体の5分の1ぐらい終わったかな。今回は普段外部から使うものじゃなくて、jQuery 内部で使う関数のところが多かった。あとは jQuery マニアしか使わないようなメソッドとか。そんなわけで使い方がイメージしにくいところもあったので、その中でわかりにくかったとこを復習して挙げておく。

あと、今回読んだ jQuery のバージョンは 1.4.2。

前提

こんな感じでメソッドを追加してるところがある。

jQuery.extend({
  queue: // ... queue の実装
});
jQuery.fn.extend({
  queue: // ... queue の実装
});

1つ目の jQuery.extend の方は jQuery のグローバルな関数というか jQuery のクラスメソッドを定義してる。そいつは $.queue() みたいな感じで呼び出せる。

2つ目の jQuery.fn.extend の方は jQuery オブジェクトのインスタンスメソッドを定義してて、$("div").queue() みたいな感じで呼び出す。インスタンスメソッドの方はたいてい一つ目の方法で定義した関数を使って実装されてる。なので、jQuery.extend の方の実装を理解するのが大事。

こうやってメソッドを追加してるとこは今回に限らずいろんなとこで出てくるので、基本ルールとして押さえておく。

DOM に関係ない関数

361行目から719行目までで jQuery.extend({ ... }); 形式で jQuery のクラスメソッドを定義してる。気になったものは以下。

map

よくある map 関数なんだけど、最後がちょっとわかりにくい。

return ret.concat.apply([], ret);

これは何をしているかというと、jQuery の map は map 用の callback が配列を返したとき、その配列を展開した各要素が結果に追加される、という仕様になってる。で、 ret は map した結果がどんどん追加される配列なんだけど、最後の return するところで配列を一段階取り除いてる。

// concat で配列をなだらかにする例
[].concat(1, [2, 3], [4, 5]); //=> [1, 2, 3, 4, 5]

つまり Ruby でいうところの flatten。

proxy

そもそも $.proxy って何ですかってことで調べてみたら、わかりやすい解説があった。
jQuery1.4から追加されたjQuery.proxy()を試してみる | THE HAM MEDIA BLOG
$.proxy(fn, thisObject) みたいな感じで呼び出すと、「その中の this が thisObject になった新しい fn 関数」を返す。

proxy 関数の実装は、余計なところを取り除くとこうなる。

proxy: function(fn, thisObject) {
  return function() {
    return fn.apply(thisObject, arguments);
  };
}

つまり、proxy を使うと this をすり替えて実行してくれる新しい関数がもらえるので、bind する時にわざわざ function { thisObject.fn(); } とかしなくてもよくて便利だよ、ってことらしい。うん、まあ半年に一回くらいしか使わないだろう。

IE の DOMContentLoaded

759行目、IE で DOMContentLoaded イベントが発生しないので、その代わりのテクニックを使ってる。

function doScrollCheck() {
  try {
    // If IE is used, use the trick by Diego Perini
    // http://javascript.nwbox.com/IEContentLoaded/
    document.documentElement.doScroll("left");
  } catch( error ) {
    setTimeout( doScrollCheck, 1 );
    return;
  }

  // and execute any waiting functions
  jQuery.ready();
}

IE の「DOM ツリーができあがる前に doScroll を呼び出すと例外が発生する」という仕様なのかバグなのかよくわかんない動作を利用して、doScroll しまくって例外がでなくなったら DOM ツリー完成、という風にしてる。

このテクニックはその筋の人には有名みたいで、コメントにもある通り IEContentLoaded - An alternative for DOMContenloaded on Internet Explorer に詳細が書いてある。

この場合 doScroll でほんとにスクロールしてしまったら困るので "left" を指定して画面が移動しないようにしてる。だれかが「"up" でもいいんじゃないの?」って言ってたような気がするけど、"#main" みたいなページ内アンカーに飛んできた場合は最初の位置がページの一番上じゃないこともあるわけで、やっぱり "left" がいいと思う。

jQuery.support

みんな大好きバッドノウハウのコーナーということで、829行目からはブラウザが標準から外れた動作(というかバグ)をするかどうかをチェックしてる。

特に目を引いたのがこの1行。

div.innerHTML =
  "   <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";

この HTML 1行で10種類のバグがあるかどうかチェックしてる。

  • leadingWhitespace
  • tbody
  • htmlSerialize
  • style
  • hrefNormalized
  • opacity
  • cssFloat
  • checkOn
  • optSelected
  • parentNode

「こんなにがんばってチェックしてるんだし、IE もバージョンがあがればよくなるはずだし、if (jQuery.browser.msie) { /* fu*k */ } とか書くのやめようぜ」ってことらしい。もう職人芸だね。

jQuery.support.hrefNormalized

jQuery.support には hrefNormalized ってのがあって、こんな感じの実装になってる。

hrefNormalized: a.getAttribute("href") === "/a",

getAttribute("href") したら普通は href 属性に書いてある値がそのままとれるんだけど、IE の場合は 'http://' から始まる絶対 URL 形式の値が返ってくる。

で、この hrefNormalized って名前逆じゃね? って発言があった。僕もそう思う。

これに限らず、jQuery のコードにはよくない名前がけっこうある(なので安易にまねしない方がいい)。

jQuery.support.deleteExpando

続きを読んでると jQuery.support.deleteExpando ってのが出てきた。「というか expando って何?」って話になったけど、オブジェクトに動的に追加したプロパティのことをそう呼ぶらしい(知らなかった)。Wiktionary にも expando のページがある。

この deleteExpando チェックの内容なんだけど、まず JavaScript の仕様としてはオブジェクトの存在しないプロパティを delete しても特に問題なくて、例外が発生したりしない。

var a = document.createElement("a");
delete a.hoge;  // true が返る

でもこの delete で例外が発生するよろしくないブラウザ(つまり IE)があって、そのときには jQuery.support.deleteExpando が false になるようだ。

jQuery.data 関数

1001行目から jQuery.data の実装が始まる。普通の jQuery プログラミングではあんまり使用しない関数なんだけど、jQuery の内部では結構使われているので大事だ。データ構造を押さえておくことにする。

jQuery.data(element, key, value) の基本的な機能は、emelent(DOM 要素)ごとに名前と値の組としてデータを格納する、というもの。

データを格納する時は DOM 要素ごとに一意的な ID を割り振って、その ID をキーとして jQuery.cache というグローバルなオブジェクトに値を格納する。

jQuery.cache のイメージはこんな感じ。

jQuery.cache = {
  id1 : {
    // element1 用のデータ
    key1_1 : value1
    key1_2 : value2
  },
  id2 : {
    // element2 用のデータ
    key2_1 : value3
  }
}

対象となった DOM 要素側でも ID を覚えてるので、あとからそれをキーとしてデータを取ってこれる。

jQuery.data の実装としては中でいろいろやってるんだけど、基本的なとこだけを残すとこうなる。

// グローバルな変数の初期化
var uuid = 0;
JQuery.expando = "jQuery" + (new Date).getTime();
JQuery.cache = {};

// jQuery.data の実装
jQuery.data = function(element, key, value) {
  // 要素ごとに一意的な ID を割り振る
  var id = ++uuid;
  element[jQuery.expando] = id;

  if (!jQuery.cache[id]) {
    jQuery.cache[id] = {};
  }
  jQuery.cache[id][key] = value;

  return value;
}

DOM 要素側で ID を覚えておく際に、現在時刻を取ってプロパティ名としてかぶらない名前を使うようになってる。

jQuery.queue 関数

早速1117行目からの jQuery.queue 関数で jQuery.data を使ってる。

jQuery.queue(element, queueName, value) は、指定された DOM 要素に queueName という名前のキューを作ってそこに値を追加する関数。

こいつも結局 jQuery.data を使って、queueName という名前で配列データを作ってキューとして使用してる。

// jQuery.queue のイメージ
jQuery.queue = function(element, queueName, value) {
  var queueName = (queueName || "fx") + "queue";
  // 現在のキュー(配列)を取得する
  var q = jQuery.data(element, queueName);
  q.push(value);

  return q;
}

キューにためるデータとしては何でもいいんだけど、関数というかイベントハンドラを登録することを想定しているようだ。「上に移動した後、色を変える」とか順番に行いたい処理をキューにためるといいんだろう。

最後に

読む上での注意なんだけど、tkyk さんもブログで指摘してたように、ソースを読む前にリファレンスマニュアルを読むのが大事。処理の概要がわかるし、引数を省略した場合の扱いとかも書いてある。つまり、リファレンスマニュアルは API の利用者だけでなく、実装を読んだり改造したりする人にとっても重要ってことだね。

なんか今回でコツがつかめたような気がするので、次回も楽しみにしてます。