jQuery.live っぽい実装

submit, focus, blur, change をクロスブラウザにする方法を追記しました。
最新版のコードを追記しました。
デモを追加しました。

jQuery.live() は

  1. document.addEventListener(type, fn, capture) で、天辺までバブルアップしてくるイベントを拾う
  2. 拾ったイベントが CSSセレクタ式(expr) に一致するか比較
  3. 一致していれば登録されている fn をコールバック

という処理をやっているようです。
# 注意: jQuery のコードを見ずにしゃべってるよ

そんな感じのを実装してみた

var _livedb = {}; // { "expr\vnamespace.click": {...}, ... }

uu = uumix(_uujamfactory, {     // uu(expr, ctx) -> Instance(jam)
  // --- event.live ---
  live:   uumix(uulive, {       // uu.live("css > expr", "namespace.click", fn)
    has:        uulivehas,      // uu.live.has("css > expr", "namespace.click") -> Boolean
    unbind:     uuliveunbind    // uu.live.unbind("css > expr" = void 0, "namespace.click" = void 0)
  })
});

// uu.live
function uulive(expr,   // @param String: "css > expr"
                nstype, // @param String: "namespace.click"
                fn) {   // @param Function: callback fn(evt, node, src)
  function _uuevliveclosure(evt) {
    evt = evt || win.event;
    var src = evt.srcElement || evt.target;

    src = (_webkit && src.nodeType === 3) ? src.parentNode : src;
    if (uuquerymatch(expr, src)) { // document.matchesSelector
      evt.node = doc;
      evt.code = (_EVCODE[evt.type] || 0) & 255;
      evt.src = src;
      evt.px = _ie ? evt.clientX + uupub.iebody.scrollLeft : evt.pageX;
      evt.py = _ie ? evt.clientY + uupub.iebody.scrollTop  : evt.pageY;
      evt.ox = evt.offsetX || evt.layerX || 0; // [offsetX] IE, Opera, WebKit
      evt.oy = evt.offsetY || evt.layerY || 0; // [layerX]  Gecko, WebKit
      handler.call(fn, evt, doc, src);
    }
  }
  if (!uulivehas(expr, nstype)) {
    var ary = nstype.split("."), // "namespace.click" -> ["namespace", "click"]
        type = ary.pop(), ns = ary.pop() || "",
        handler = uuisfunc(fn) ? fn : fn.handleEvent,
        closure = fn.uuevliveclosure = _uuevliveclosure;

    _livedb[expr + "\v" + nstype] = {
        expr: expr, ns: ns, type: type, nstype: nstype, fn: closure };
    uuevattach(doc, _EVFIX[type] || type, closure); // document.addEvenetListener
  }
}

// uu.live.has
function uulivehas(expr,     // @param String: "css > expr"
                   nstype) { // @param String: "namespace.click"
  var db = _livedb[expr + "\v" + nstype];

  return db && expr === db.expr && nstype === db.nstype;
}

// uu.live.unbind
// [1][unbind all] uu.live.unbind()
// [2][unbind all] uu.live.unbind("expr")
// [3][unbind one] uu.live.unbind("expr", "click")
// [4][unbind namespace all] uu.live.unbind("expr", "namespace.*")
// [5][unbind namespace one] uu.live.unbind("expr", "namespace.click")
function uuliveunbind(expr,     // @param String(= void 0): "css > expr"
                      nstype) { // @param String(= void 0): "namespace.click"
  var ns, v, i, r,
      mode = !expr ? 1 :   // [1]
             !nstype ? 2 : // [2]
             nstype.indexOf("*") < 0 ? 3 :  // [3][5]
             (ns = nstype.slice(0, -2), 4); // [4] "namespace.*" -> "namespace"

  for (i in _livedb) { // i = "expr\vnamespace.click"
    v = _livedb[i];    // v = { expr, ns, type, nstype, closure }
    r = 1;
    switch (mode) {
    case 2: r = expr === v.expr; break; // [2]
    case 3: r = expr === v.expr && nstype === v.nstype; break; // [3][5]
    case 4: r = expr === v.expr && ns === v.ns; // [4]
    }
    if (r) {
      uuevdetach(doc, v.type, v.fn);
      delete _livedb[i];
    }
  }
}

どのイベントがバブルアップするのかを調査

DOM の仕様: http://www.w3.org/TR/DOM-Level-2-Events/events.html
IE のオレオレ仕様: http://msdn.microsoft.com/en-us/library/ms533051(VS.85).aspx

Bubbles
Event Type DOM(IE) uu.live() で利用可能なイベント
abort yes(no) ×
blur no(no) ×
DOMFocusOut(onfocusout) yes(yes) ○ uu.live(expr, "blur")
change yes(no) ○ uu.live(expr, "change")
click yes(yes)
error yes(no) ×
focus no(no) ×
DOMFocusIn(onfocusin) yes(yes) ○ uu.live(expr, "focus")
keydown yes(yes)
keypress yes(yes)
keyup yes(yes)
load no(no) ×
mousedown yes(yes)
mousemove yes(yes)
mouseout yes(yes)
mouseover yes(yes)
mouseup yes(yes)
mousewheel
gecko:DOMMouseScroll
yes(yes)
reset yes(no) ×
resize yes(no) ×
scroll yes(no) ×
select yes(no) ×
submit yes(no) ○ uu.live(expr, "submit")
unload no(no) ×

この表で yes(yes) となっているイベントがクロスブラウザであり、live() で利用できるイベントということになります。
# といっても実際にはそんなに甘くなかった

デモ

http://pigs.sourceforge.jp/blog/20091231/

  • uu.live.submit.htm は uu.live(expr, "submit"); のデモです。
  • uu.live.focus.htm は uu.live(expr, "focus"); と uu.live(expr, "blur") のデモです。
  • uu.live.change.htm は uu.live(expr, "change"); のデモです。
    • uu.ev.change.htm は通常のイベント uu.ev(expr, "change"); のデモです。uu.live 版との動作比較用です。

追記

submit を IE で拾えるようするには

IE の submit イベントはバブルアップしないので、
uu.live("form", "submit") を
内部的に、uu.live("form input[type=submit],form input[type=image]", "click") に変換して回避

    if (uu.ie) {
      // uu.live("form", "submit") ->
      //    uu.live("form input[type=submit],form input[type=image]", "click")
      if (/submit$/.test(type)) {
        _uulive(expr + " input[type=submit]," + expr + " input[type=image]",
                nstype.replace(/submit$/, "click"), fn, hash);
      }
    }
focus(DOMFocusIn), blur(DOMFocusOut) を gecko で拾えるようにするには

gecko は DOMFocusIn と DOMFocusOut を無視するので、

document.addEventListener("focus", fn, true); // capture
document.addEventListener("blur", fn, true); // capture

相当の処理を追加。要はキャプチャーフェーズで focus, blur イベントを拾って処理する

    if (uu.gecko) {
      // http://help.dottoro.com/ljuoivsj.php
      // http://twitter.com/uupaa/status/7221096300
      (type === "focus" || type === "blur") && (capt = 1);
    }

gecko 以外のブラウザは、イベントタイプを以下のように変換することで処理

var _LIVEFIX = uu.ie ? { focus: "focusin", blur: "focusout" }
             : uu.gecko ? {} : { focus: "DOMFocusIn", blur: "DOMFocusOut" };

uu.ev.attach(doc, _LIVEFIX[type] || type, closure, capt)
change イベントを IE で拾えるようにするには

IE の change イベントはバブルアップしません。
2時間悩んで…

  • onfocusin, onfocusout はバブルアップするので、onfocusin と onfocusout を設定して待機
  • onfocusin が発生したら event.srcElement に対し、change イベントを設定
  • onfocusout が発生したら event.srcElement の change イベントを解除

というトリックを考えました。

if (uu.ie) {
  if (/change$/.test(type)) {
    _uulive(expr, nstype.replace(/change$/, "focus"), function(evt) {
      uu.ev(evt.srcElement, "uulivehook.change", fn);
    }, hash);
    _uulive(expr, nstype.replace(/change$/, "blur"), function(evt) {
      uu.ev.unbind(evt.srcElement, "uulivehook.change");
    }, hash);
  }
}

submit, focus, blur, change 実装後のソースコード

_uulive() に GeckoIE 向けのコードが追加されています。

// === Live ===
// depend: uu.js
// http://d.hatena.ne.jp/uupaa/20091231
uu.waste || (function(win, doc, uu) {
var _livedb = {}, // { "expr\vnamespace.click": {...}, ... }
    _LIVEFIX = uu.ie ? { focus: "focusin", blur: "focusout" }
             : uu.gecko ? {} : { focus: "DOMFocusIn", blur: "DOMFocusOut" };

// uu.live
uu.live = uu.mix(uulive, {      // uu.live("css > expr", "namespace.click", fn)
  has:          uulivehas,      // uu.live.has("css > expr", "namespace.click") -> Boolean
  unbind:       uuliveunbind    // uu.live.unbind("css > expr" = void 0, "namespace.click" = void 0)
});

// uu.match
uu.match = uumatch;             // uu.match("p > a", NodeArray/Node, rtype = 0) -> Boolean/NodeArray

// uu.live
function uulive(expr,   // @param String: "css > expr"
                nstype, // @param String: "namespace.click"
                fn) {   // @param Function: callback fn(evt, node, src)
  _uulive(expr, nstype, fn);
}

// inner -
function _uulive(expr, nstype, fn, hash) {
  function _uuliveclosure(evt) {
    evt = evt || win.event;
    var src = evt.srcElement || evt.target;

    src = (uu.webkit && src.nodeType === 3) ? src.parentNode : src;
    if (uu.match(expr, src)) {
      evt.node = doc;
      evt.code = (uupub.EVCODE[evt.type] || 0) & 255;
      evt.src = src;
      evt.px = uu.ie ? evt.clientX + uupub.iebody.scrollLeft : evt.pageX;
      evt.py = uu.ie ? evt.clientY + uupub.iebody.scrollTop  : evt.pageY;
      evt.ox = evt.offsetX || evt.layerX || 0; // [offsetX] IE, Opera, WebKit
      evt.oy = evt.offsetY || evt.layerY || 0; // [layerX]  Gecko, WebKit
      handler.call(fn, evt, doc, src);
    }
  }
  if (!uulivehas(expr, nstype)) {
    var ary = nstype.split("."), // "namespace.click" -> ["namespace", "click"]
        type = ary.pop(), ns = ary.pop() || "", capt = 0,
        handler = uu.isfunc(fn) ? fn : fn.handleEvent,
        closure = fn.uuevliveclosure = _uuliveclosure;

    hash || (hash = _livedb[expr + "\v" + nstype] = {
                expr: expr, ns: ns, type: type, nstype: nstype, unbind: [] });

    if (uu.gecko) {
      (type === "focus" || type === "blur") && (capt = 1);
    }

    hash.unbind.push(function() {
      uu.ev.detach(doc, _LIVEFIX[type] || type, closure, capt);
    });
    uu.ev.attach(doc, _LIVEFIX[type] || type, closure, capt);

    if (uu.ie) {
      if (/submit$/.test(type)) {
        _uulive(expr + " input[type=submit]," + expr + " input[type=image]",
                nstype.replace(/submit$/, "click"), fn, hash);
      } else if (/change$/.test(type)) {
        _uulive(expr, nstype.replace(/change$/, "focus"), function(evt) {
          uu.ev(evt.srcElement, "uulivehook.change", fn);
        }, hash);
        _uulive(expr, nstype.replace(/change$/, "blur"), function(evt) {
          uu.ev.unbind(evt.srcElement, "uulivehook.change");
        }, hash);
      }
    }
  }
}

// uu.live.has
function uulivehas(expr,     // @param String: "css > expr"
                   nstype) { // @param String: "namespace.click"
  var db = _livedb[expr + "\v" + nstype];

  return db && expr === db.expr && nstype === db.nstype;
}

// uu.live.unbind
// [1][unbind all] uu.live.unbind()
// [2][unbind all] uu.live.unbind("expr")
// [3][unbind one] uu.live.unbind("expr", "click")
// [4][unbind namespace all] uu.live.unbind("expr", "namespace.*")
// [5][unbind namespace one] uu.live.unbind("expr", "namespace.click")
function uuliveunbind(expr,     // @param String(= void 0): "css > expr"
                      nstype) { // @param String(= void 0): "namespace.click"
  var ns, v, i, r,
      mode = !expr ? 1 :   // [1]
             !nstype ? 2 : // [2]
             nstype.indexOf("*") < 0 ? 3 :  // [3][5]
             (ns = nstype.slice(0, -2), 4); // [4] "namespace.*" -> "namespace"

  for (i in _livedb) { // i = "expr\vnamespace.click"
    v = _livedb[i];    // v = { expr, ns, type, nstype, closure }
    r = 1;
    switch (mode) {
    case 2: r = expr === v.expr; break; // [2]
    case 3: r = expr === v.expr && nstype === v.nstype; break; // [3][5]
    case 4: r = expr === v.expr && ns === v.ns; // [4]
    }
    if (r) {
      v.unbind.forEach(function(v) {
        v();
      });
      delete _livedb[i];
    }
  }
}

// uu.match - document.matchesSelector like function
function uumatch(expr,    // @param String: "css > expr"
                 ctx,     // @param NodeArray/Node: match context
                 rtype) { // @param Number(= 0): result type,
                          //             0 is Boolean result, matches all,
                          //             1 is Boolean result, matches any,
                          //             2 is NodeArray result, matches array
                          // @return Boolean/NodeArray:
  ctx = ctx.nodeType ? [ctx] : ctx;
  var rv = [], hash = {}, v, w, i = 0, j = 0, ary = uu.query(expr, doc);

  if (ctx.length === 1) {
    v = ctx[0];
    while ( (w = ary[i++]) ) {
      if (v === w) {
        rv.push(v);
        break;
      }
    }
  } else {
    while ( (v = ary[i++]) ) {
      hash[v.uuguid || uu.nodeid(v)] = 1;
    }
    while ( (v = ctx[j++]) ) {
      (v.uuguid || uu.nodeid(v)) in hash && rv.push(v);
    }
  }
  return !rtype ? rv.length === ctx.length : rtype < 2 ? !!rv.length : rv;
}

})(window, document, uu);

反省会

  • uu.live() は今朝の 04:00頃から実装を開始して、実装完了が 16:50
    • 100行実装するのに約12時間(+ 休憩)。
      • C や Java なら8時間で1000行ぐらい書ける事もあるけど、ブラウザが本来サポートしていない機能を攻略しつつだから、12時間で100行は悪くないと思うよ。

おまけ

Cancelable 一覧
Event Type DOM(IE)
abort no(yes)
blur no(no)
change no(no)
click yes(yes)
error no(yes)
focus no(no)
keydown 不明(yes)
keypress 不明(yes)
keyup 不明(no)
load no(no)
mousedown yes(yes)
mousemove no(no)
mouseout yes(no)
mouseover yes(yes)
mouseup yes(yes)
mousewheel
gecko:DOMMouseScroll
yes(yes)
reset no(yes)
resize no(no)
scroll no(no)
select no(yes)
submit yes(yes)
unload no(no)
DOMFocusIn(onfocusin) no(no)
DOMFocusOut(onfocusout) no(no)