愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

iPhone/Android向けのselector-based libraryを実装しての色々(2) - 実装編

前回はほとんどライブラリの紹介しか書いてなかったので、今回の内容は得られた少々の知見とか実装中に考えてたこととかです。

環境を限って、ある程度の割り切りを行えばselector-basedなオレオレライブラリを書くのは大して難しくない、ということ

どこまで古いブラウザのサポートを行うかでも変ってくるのですが、ちゃんとしたクロスブラウザ対応でselector-basedなライブラリを書くのは落とし穴があって中々大変だと思います。あと、selector-basedなライブラリの要であるcssセレクタの実装も、真面目にクロスブラウザかつ速いものを書こうとするとけっこうめんどくさい(勿論それ故の面白さがあると思います)と思われます。

Riddle.jsもそうしているのですが、querySelectorAllが使える環境にターゲットを絞るならquerySelectorAllにとりあえずセレクタ部分を全部投げてしまうことによってcssセレクタの実装は省くことができます。

内部的なcssセレクタ関数としてquerySelectorAllを使うことの欠点は、大きいもので下の二点なのではないかと思います。

  1. querySelectorAllを使うと、適切に分岐してgetElementById, getElementsByClassNameなどの特化されたAPIを使う実装より遅くなってしまう
  2. 自前実装のように複雑/オリジナルなセレクタが使えない、というかどこまで複雑なセレクタが使えるかがブラウザの実装に依存してしまう


一つ目については、汎用的なセレクタ関数であるqueryなんとかよりgetElementByIdやgetElementsByClassNameで済む場合はそっちの方が速い、というのは直感的に分かると思います。TwitterがjQuery1.4.2から1.4.4(実際に影響のあった変更は1.4.3に含まれたもの)にアップデートした時にプギャーった話(http://ejohn.org/blog/learning-from-twitter/)にもそれ関係のことが書いてあります。どうやらメインの理由ではないようですが。

idとかclassで引くときは、querySelectorではなくもっと速いAPIを使いたい、という人もいるかと思われたのでRiddle.jsでは妥協案としてr.id()とかr.cls()とかを使えばgetElementById, getElementsByClassNameを内部的に使ってくれるようにしています。


二つ目については、「そもそもそこまで複雑怪奇なセレクタ使いたいか?使うか?」と常々思っているのでまーいいんじゃないかな、と。jQueryではオリジナルのセレクタが色々使えるのですが、Sizzleはクッソ遅いので、まともな人はjQuery使っててもそういう機能はあまり使わないです。なので、僕はオリジナルな便利セレクタなんてそこまで要らんと思うのです。


対応ブラウザの限定、cssセレクタの実装のパス、までやってしまえば後は提供する機能を決めて穴を埋めるだけなので、誰にでも書けます。(そんなに大したものではないとはいえ)僕にも書けたぐらいですから。

bind, unbindはelementにid振っちゃうのがラク

ほぼ見出しの通りです。このあたりから実装の話が入ってくるので、「その実装はいけてない!もっといい方法あるよ!」みたいな人は教えて頂けるとありがたいです。

addEventListener, removeEventListenerの簡易ラッパーとしてbind, unbindなどの関数を実装する場合、element自体へのcallback関数のbind, unbind操作を行うことに加えて、ユーザから渡されたコールバック関数をライブラリの中で別途管理する必要があります。

というのは、unbind的な関数でコールバック関数を除去する時に、内部的に呼ばれるremoveEventListenerの第二引数に結局元のコールバック関数を渡してあげないといけないからです。なので、どこかにbindで渡された関数の参照を持っておかないとダメ。

あとそれとは別に、ライブラリの内部で「このelementにはこのコールバックをaddして、このelementにはこれ」という判断をつけるためにelementを一意に判別する方法が必要です。見た感じ既存ライブラリでもelementにpropertyとしてセットしちゃっているようだったので、そこは真似しました。

bind, unbindはこんな感じです

  function getNodeId(elem) {
    return elem.nid || (elem.nid = nodeId++);
  }

  function findBoundsByEvent(bounds, event) {
    return bounds.filter(function(bound) { return bound.event === event });
  }

/**
 * bind callback function to elements
 * @name bind
 * @function
 * @memberOf r.fn
 * @param event {string}
 * @param callback {function(e: Object)}
 * @param useCapture {?boolean}
 * @example
 * r("#button").bind("click", function(e) {
 *   alert("button clicked");
 * });
 */
  function bind(event, callback, useCapture) {
    this.forEach(function(elem) {
      var id = getNodeId(elem),
          bounds = listeners[id] || (listeners[id] = []);
      bounds.push( { event: event, callback: callback, index: bounds.length } );
      elem.addEventListener(event, callback, useCapture || false);
    });
    return this;
  }

/**
 * unbind alreaedy-bound callback function from elements
 * @name unbind
 * @function
 * @memberOf r.fn
 * @param event {string}
 * @example
 * r("#button").unbind("click");
 */
  function unbind(event) {
    this.forEach(function(elem) {
      var id = getNodeId(elem),
          bounds = event ? findBoundsByEvent(listeners[id], event) : listeners[id];
      bounds && bounds.forEach(function(bound) {
        delete bounds[bound.index];
        elem.removeEventListener(bound.event, bound.callback, false);
      });
    });
    return this;
  }

elementにnidってプロパティつけちゃうのってどうなの、とか、数値だといつかオーバーフローしちゃうよね、とか、そういう細かい疑念はありますが、とりあえずはこれでOKかなあと。見てのとおり、今はeventの名前空間は実装してないです。

Property Descriptorsは(短くまとめたいコードでは)使いどころが難しいなあ、という話

せっかくiPhoneAndroidに環境を限定するんだから、ES5のProperty Descriptorsやらgetter/setterやら(これProperty hogehogeの一部だけど)を使ってカコイイライブラリにしたいなあ、と初めは思っていたんですが、結果としてそういう部分はなくなりました。

というのは、Property Descriptors周りの記述が加わるとどうしてもコードが長くなるからですね。一応コンセプトとして「できるだけファイルサイズ小さめに」というのを掲げている関係上、コードが長くなるコストに対して充分なユーザメリットがないとあまり使いたくないです。

getter/setterについても、例えば

// set element attribute with object.
// internally use setter
r("#foo").attr = {
  "href": "http://example.com",
  "target": "_blank"
};

みたいなインタフェースとかも考えたんだけど、結局jQueryみたいに引数の数や種類によって動作の変わる関数を提供した方がユーザにとって便利だよなあ、と思ってやめにしました。上のはあんまりいけてない例だし、今提供しているAPIについては適切な利用場面が見つからなかった、というだけの話なので、もっと違う機能があるライブラリとかではgetter/setterとかをオシャレに(かつユーザにとって利便性のある形で)提供できるのかもしれない、とは思います。


また長くなったので、さらに続きます。