Tuesday, June 9, 2009

Google Maps API v3, event system

此處程式是以 http://maps.gstatic.com/mapfiles/api-3/5/main.js 為對象。

Google maps api v3 的 event system 有兩種 event,一種是 Normal event,一種是 DOM event。Normal event 可以定義任何名稱的 event,而 DOM 的 event 就是 DOM 已定義的 event,而兩種 event 可以並用。範例如下:
HTML:
<span id="ts">test</span>

JavaScript:
var ts = document.getElementById("ts");
// register DOM event listener will also register normal event listener 
google.maps.event.addDomListener(ts, "click", function() {
  console.log("ts.click dom");
});
// Normal event listener will not be triggered by DOM event.
google.maps.event.addListener(ts, "click", function() {
  console.log("ts.click");
});
// can register multiple handler on same event
google.maps.event.addListener(ts, "click", function() {
  console.log("ts.click2");
});

// will trigger Normal event listener and DOM event Listener
google.maps.event.trigger(ts, "click");

執行結果為:
ts.click dom
ts.click
ts.click2
(而 mouse click 只觸發 ts.click dom 訊息)

可同時定義任意個數的 Normal event 和 DOM event。而 trigger 會同時觸發 Normal event 和 Dom event。

而 event 的 source code 有幾個相關的部分,第一是 event 本身:
var event = {};
 // Adds the given listener function to the the given event name for the given object instance.
 // Returns an identifier for this listener that can be used with eventRemoveListener().
 // 
 // instance:Object, eventName:string, handler:Function, owner?:Object
 // return EventListener
 //
 event.addListener = function(instance, eventName, handler, owner) {
  // already add listener to instance's event listener chain
  var e = new EventListener(instance, eventName, handler, 0, owner);
  getInstance(EventListenerManager).addListener(e);
  return e
 };

其中 EventListener 和 EventListenerManager 的程式如下:
// instance, eventName, handler, 0, owner
 function EventListener(instance, eventName, handler, domEventMethod, owner) {
  nc(instance);
  nc(typeof handler == "function");
  this.u = instance;
  this.Ga = eventName;
  this.mb = handler;
  this.ic = null;
  this.Hi = domEventMethod;
  this.ni = owner || null;
  this._idx = -1;
  getEventListenerChain(instance, eventName, true)["push"](this)
 }
 EventListener["prototype"].createOldStyleHandler = function() {
  var a = this;
  return this.ic = function(b) {
   if (!b)
    b = window.event;
   if (b && !b.target)
    try {
     b.target = b.srcElement
    } catch (c) {
    }
   var d = a.triggerHandler([b]);
   if (b && "click" == b["type"]) {
    var e = b.srcElement;
    if (e && "A" == e["tagName"] && "javascript:void(0)" == e.href)
     return false
   }
   return d
  }
 };
 EventListener["prototype"].remove = function() {
  if (this.u) {
   switch (this.Hi) {
    case 1 :
     this.u.removeEventListener(this.Ga, this.mb, false);
     break;
    case 4 :
     this.u.removeEventListener(this.Ga, this.mb, true);
     break;
    case 2 :
     this.u.detachEvent("on" + this.Ga, this.ic);
     break;
    case 3 :
     this.u["on" + this.Ga] = null;
     break
   }
   for (var a = getEventListenerChain(this.u, this.Ga), b = 0, c = 0; c < getLength(a); ++c)
    if (a[c] === this) {
     a.splice(c--, 1);
     b++
    }
   this.ic = this.mb = this.u = null
  }
 };
 EventListener["prototype"].isOwner = function(a) {
  return this.ni === a
 };
 EventListener["prototype"].triggerHandler = function(a) {
  if (this.u)
   return this.mb["apply"](this.u, a)
 };

 function EventListenerManager() {
  this.I = []
 }
 EventListenerManager["prototype"].removeListener = function(listener) {
  var b = listener._idx;
  if (!(b < 0)) {
   var c = this.I.pop();
   if (b < this.I["length"]) {
    this.I[b] = c;
    c._idx = b
   }
   listener._idx = -1
  }
 };
 EventListenerManager["prototype"].addListener = function(listener) {
  this.I["push"](listener);
  listener._idx = this.I["length"] - 1
 };
 EventListenerManager["prototype"].clear = function() {
  array_foreach(this.I, function(a) {
     a._idx = -1
    });
  setLength(this.I, 0)
 };
可以看得出來,Normal Event 就是在 instance 下找個地方把 handler 收集起來,以便之後 trigger 時呼叫。其中 remove 是要配合之後 DOM event 的 remove 做法。

remove 也很容易理解:
// Removes the given listener, which should have been returned by eventAddListener above.
 // listener:EventListener
 event.removeListener = function(listener) {
  listener["remove"]();
  getInstance(EventListenerManager)["removeListener"](listener)
 };

 // Removes all listeners for the given event for the given instance.
 // instance:Object, eventName:string, owner?:Object
 event.clearListeners = function(instance, eventName, owner) {
  array_foreach(cloneEventListenerChain(instance, eventName), function(d) {
     if (!owner || d.isOwner(owner)) {
      d["remove"]();
      getInstance(EventListenerManager)["removeListener"](d)
     }
    })
 };
 // Removes all listeners for all events for the given instance.
 // instance:Object, owner?:Object
 event.clearInstanceListeners = function(instance, owner) {
  array_foreach(cloneEventListenerChain(instance), function(c) {
     if (!owner || c.isOwner(owner)) {
      c["remove"]();
      getInstance(EventListenerManager)["removeListener"](c)
     }
    })
 };
 event.clearAllEventListener = function() {
  for (var a = [], b = getInstance(EventListenerManager).I, c = 0, d = getLength(b); c < d; ++c) {
   var e = b[c], g = e.u;
   if (!g.__tag__) {
    g.__tag__ = true;
    a["push"](g)
   }
   e["remove"]()
  }
  for (c = 0; c < getLength(a); ++c) {
   g = a[c];
   if (g.__tag__)
    try {
     delete g.__tag__;
     delete g.__e_
    } catch (h) {
     g.__tag__ = false;
     g.__e_ = null
    }
  }
  getInstance(EventListenerManager).clear()
 };

值得看到的是 getInstance 是用簡單的手法做 Singleton pattern。
function getInstance(a) {
  if (!a.u)
   a.u = new a;
  return a.u
 }

幾個 tool function:
// instance, eventName
 function cloneEventListenerChain(instance, eventName) {
  var c = [], d = instance.__e_;
  if (d)
   if (eventName)
    d[eventName] && pushAll(c, d[eventName]);
   else
    foreach(d, function(e, g) {
       pushAll(c, g)
      });
  return c
 }
 // instance, eventName, true
 function getEventListenerChain(instance, eventName, create) {
  var d = null, e = instance.__e_;
  if (e) {
   d = e[eventName];
   if (!d) {
    d = [];
    if (create)
     e[eventName] = d
   }
  } else {
   d = [];
   if (create) {
    instance.__e_ = {};
    instance.__e_[eventName] = d
   }
  }
  return d
 }

trigger 也很容易理解,就是找出註冊的 handler 然後一一呼叫就行了。
// Triggers the given event. All arguments after eventName are passed as arguments to the listeners.
 // instance:Object, eventName:string, var_args:*
 event.trigger = function(instance, eventName) {
  var c = dc(arguments, 2);
  array_foreach(cloneEventListenerChain(instance, eventName), function(d) {
     d.triggerHandler(c)
    })
 };

DOM event 因為要支搜多種不同 browser 的 event 系統,所以需要做一些判斷。
// Cross browser event handler registration.
 // This listener is removed by calling eventRemoveListener(handle) for the handle that is returned by this function.
 // instance:Object, eventName:string, handler:Function, owner?:Object
 // return EventListener
 event.addDomListener = function(instance, eventName, handler, owner) {
  var e;
  if (instance.addEventListener) {
   var g = false;
   if (eventName == "focusin") {
    eventName = "focus";
    g = true
   } else if (eventName == "focusout") {
    eventName = "blur";
    g = true
   }
   var h = g ? 4 : 1;
   instance.addEventListener(eventName, handler, g);
   e = new EventListener(instance, eventName, handler, h, owner)
  } else if (instance.attachEvent) {
   e = new EventListener(instance, eventName, handler, 2, owner);
   instance.attachEvent("on" + eventName, e.createOldStyleHandler())
  } else {
   instance["on" + eventName] = handler;
   e = new EventListener(instance, eventName, handler, 3, owner)
  }
  if (instance != window || eventName != "unload")
   getInstance(EventListenerManager).addListener(e);
  return e
 };

也提供一些 forward 的手法方便使用。
event.forwardDomListener = function(instance, eventName, target, handler, owner) {
  var g = xc(target, handler);
  return event["addDomListener"](instance, eventName, g, owner)
 };
 function xc(a, b) {
  nc(b);
  return function(c) {
   return b["call"](a, c, this)
  }
 }
 event.forwardListener = function(instance, eventName, target, handler, owner) {
  nc(handler);
  return event["addListener"](instance, eventName, createCaller(target, handler), owner)
 };
 event.addOnetimeListener = function(instance, eventName, handler) {
  var d = event["addListener"](instance, eventName, function() {
     handler["apply"](instance, arguments);
     event["removeListener"](d)
    });
  return d
 };
 event.forward = function(instance, eventName, target) {
  return event["addListener"](instance, eventName, yc(eventName, target))
 };
 event.forwardDom = function(instance, eventName, target, owner) {
  return event["addDomListener"](instance, eventName, yc(eventName, target, true), owner)
 };
 function yc(eventName, instance, isDom) {
  return function(d) {
   var e = [instance, eventName];
   pushAll(e, arguments);
   event["trigger"]["apply"](this, e);
   if (isDom) {
    var g = d || window.event;
    g.cancelBubble = true;
    g["stopPropagation"] && g["stopPropagation"]()
   }
  }
 }


event 系統並沒有太複雜的做法,但卻是整個系統都會使用到的工具。

1 comments:

Anonymous said...

Thanks so much for this write-up. This is the only working example I can find of the addDomListener handler on the entire god damn Internet.

Post a Comment