這篇開始讀ext-base-event.js。該檔案定義了Ext.lib.Event物件,Ext.lib這個名稱空間在Ext core的Ext.js中命名的。
Ext.ns("Ext.util", "Ext.lib", "Ext.data");
Ext.lib上的屬性如下:
Ext.lib.Ajax
Ext.lib.Anim
Ext.lib.AnimMgr
Ext.lib.Bezier
Ext.lib.Dom
Ext.lib.Easing
Ext.lib.Event
Ext.lib.AnimBase
Ext.lib.ColorAnim
Ext.lib.Motion
Ext.lib.Scroll
Ext.lib.Event 是Ext中事件處理的輕度封裝,概覽下
Ext.lib.Event = function() {
var loadComplete = false,
...
...
return pub;
}();
可以發現仍然是一個匿名函式執行,執行後返回物件pub,pub賦值給Ext.lib.Event。再看內部細節
var loadComplete = false,
unloadListeners = {},
retryCount = 0,
onAvailStack = [],
_interval,
locked = false,
win = window,
doc = document,
// constants
POLL_RETRYS = 200,
POLL_INTERVAL = 20,
EL = 0,
TYPE = 0,
FN = 1,
WFN = 2,
OBJ = 2,
ADJ_SCOPE = 3,
SCROLLLEFT = 'scrollLeft',
SCROLLTOP = 'scrollTop',
UNLOAD = 'unload',
MOUSEOVER = 'mouseover',
MOUSEOUT = 'mouseout',
以上定義了一堆變數。window,document物件分別賦值給了win,doc。這樣做的好處是減少了一層閉包。使用區域性變數win,doc比直接使用window,document要快。因為它們存在於執行函式的活動物件中,解析識別符號只需要查詢作用域鏈中的單個物件。
而讀取變數值的耗時是隨著查詢作用域鏈的逐層深入而不斷增加。這點可參考:《JS權威指南》第五版4.7節:深入理解變數作用域。
doc後是一堆常量定義,Ext的編碼習慣亦是常量全部使用大寫,有多個單詞時用下劃線連線。接下來是一堆私有方法/函式定義,即這些函式只能在上面提到的最外層的匿名函式內使用。
// private
doAdd = function() {
var ret;
if (win.addEventListener) {
ret = function(el, eventName, fn, capture) {
if (eventName == 'mouseenter') {
fn = fn.createInterceptor(checkRelatedTarget);
el.addEventListener(MOUSEOVER, fn, (capture));
} else if (eventName == 'mouseleave') {
fn = fn.createInterceptor(checkRelatedTarget);
el.addEventListener(MOUSEOUT, fn, (capture));
} else {
el.addEventListener(eventName, fn, (capture));
}
return fn;
};
} else if (win.attachEvent) {
ret = function(el, eventName, fn, capture) {
el.attachEvent("on" + eventName, fn);
return fn;
};
} else {
ret = function(){};
}
return ret;
}(),
doAdd,亦是一個匿名函式執行後返回新函式,用來給html元素新增事件及事件響應函式(handler)。這個函式和多數的事件新增函式差不多,用特性判斷 。標準瀏覽器使用addEventListener新增,IE系列使用attachEvent,都不支援則返回一個空函式。這裡有幾點,
1,有的程式碼中使用特性判斷時,先寫win.attachEvent,後是win.addEventListener。這是不對的,應該優先使用標準的addEventListener,而IE9同時支援這兩種方式。
2,這裡新增了mouseenter /mouseleave 事件,它們僅IE支援。mouseenter不同於mouseover,它是在第一次滑鼠進入節點區域時觸發,以後在節點區域內(子節點間)移動時不觸發。Goodbye mouseover, hello mouseenter 詳細講述了使用mouseenter的好處。此處有簡單的實現。
這裡為非IE瀏覽器間接實現了這兩個事件,需要另兩個函式的輔助
function checkRelatedTarget(e) {
return !elContains(e.currentTarget, pub.getRelatedTarget(e));
}
function elContains(parent, child) {
if(parent && parent.firstChild){
while(child) {
if(child === parent) {
return true;
}
child = child.parentNode;
if(child && (child.nodeType != 1)) {
child = null;
}
}
}
return false;
}
elContains 兩個引數parent,child判斷某個元素child是否是parent的子元素,是則返回true,否則false。
checkRelatedTarget 會作為一個攔截器,這裡e.currentTarget IE6/7/8不支援。pub.getRelatedTarget(e)是下面封裝好的方法,IE中使用fromElement,toElement。
fn = fn.createInterceptor(checkRelatedTarget);
實現的基本思路:使用mouseover事件,即當給某元素(parent)新增mouseenter事件時,滑鼠移至parent時觸發事件handler,但從其子元素上移動時並不觸發。
順便提下,Ext這裡的elContains方法的實現明顯欠妥,實際上IE中可以使用contains ,現代瀏覽器則可使用compareDocumentPosition ,謝謝天堂 提醒。John 寫了個
function contains(a, b){
return a.contains ?
a != b && a.contains(b) :
!!(a.compareDocumentPosition(b) & 16);
}
jQuery的選擇器Sizzle.contains也是這麼實現。
function getScroll() {
var dd = doc.documentElement,
db = doc.body;
if(dd && (dd[SCROLLTOP] || dd[SCROLLLEFT])){
return [dd[SCROLLLEFT], dd[SCROLLTOP]];
}else if(db){
return [db[SCROLLLEFT], db[SCROLLTOP]];
}else{
return [0, 0];
}
}
私有的getScroll方法返回文件的scrollTop和scrollLeft值,由於瀏覽器差異,該實現上先從document.documentElement取,為0後再從document.body上取。都沒有返回[0,0]。
function getPageCoord (ev, xy) {
ev = ev.browserEvent || ev;
var coord = ev['page' + xy];
if (!coord && coord !== 0) {
coord = ev['client' + xy] || 0;
if (Ext.isIE) {
coord += getScroll()[xy == "X" ? 0 : 1];
}
}
return coord;
}
私有的getPageCoord方法用來獲取滑鼠事件時相對於文件的座標(水平,垂直)。
Firefox引入了pageX / Y ,IE9/Safari/Chrome/Opera雖然支援但僅在文件(document)內而非頁面(page)。
Safari/Chrome/Opera可以使用標準的clientX/Y獲取,IE下可通過clientX/Y與scrollLeft/scrollTop計算得到。
IE9實際上也可通過clientX/Y獲取,這裡判斷瀏覽器Ext.isIE在IE9正式版即將釋出後明顯欠妥。
再往下就是一個物件pub,匿名函式執行後會返回該物件。猜測pub是public的簡寫,即匿名函式執行後對外公開的介面物件(pub)。pub有以下方法
addListener: function(el, eventName, fn) {
el = Ext.getDom(el);
if (el && fn) {
if (eventName == UNLOAD) {
if (unloadListeners[el.id] === undefined) {
unloadListeners[el.id] = [];
}
unloadListeners[el.id].push([eventName, fn]);
return fn;
}
return doAdd(el, eventName, fn, false);
}
return false;
},
為元素新增事件,el為新增事件的元素,eventName為事件名稱(如click),fn為響應函式(hanlder)。對“unload”事件做了單獨處理,內部呼叫私有的doAdd函式。
removeListener: function(el, eventName, fn) {
el = Ext.getDom(el);
var i, len, li, lis;
if (el && fn) {
if(eventName == UNLOAD){
if((lis = unloadListeners[el.id]) !== undefined){
for(i = 0, len = lis.length; i < len; i++){
if((li = lis[i]) && li[TYPE] == eventName && li[FN] == fn){
unloadListeners[id].splice(i, 1);
}
}
}
return;
}
doRemove(el, eventName, fn, false);
}
},
刪除元素已註冊的事件響應函式,引數同addListener。
這兩個函式都有個註釋:This function should ALWAYS be called from Ext.EventManager
可以發現,真正客戶端程式設計師在使用Ext庫時並不直接使用Ext.lib.Event.addListener / Ext.lib.Event.removeListener新增或刪除事件。
而是使用Ext.EventManager.addListener / Ext.EventManager.removeListener或者它們的縮寫Ext.EventManager.on / Ext.EventManager.un。
Ext.EventManager對事件管理提供了更高層次的封裝。後續會介紹。
getTarget : function(ev) {
ev = ev.browserEvent || ev;
return this.resolveTextNode(ev.target || ev.srcElement);
},
獲取事件源物件。W3C標準使用 target ,IE6/7/8使用了專有的 srcElement 。令人驚奇的是Safari/Chrome/Opera也支援IE6/7/8方式,即同時支援標準和IE專有方式。Firefox僅支援標準的target,IE9beta現已支援target。
getRelatedTarget : function(ev) {
ev = ev.browserEvent || ev;
return this.resolveTextNode(ev.relatedTarget ||
(ev.type == MOUSEOUT ? ev.toElement :
ev.type == MOUSEOVER ? ev.fromElement : null));
},
獲取事件相關的元素。W3C標準使用 relatedTarget ,IE6/7/8使用了專有的 fromElement / toElement 。同樣Safari/Chrome/Opera也支援IE6/7/8方式,即同時支援標準和IE專有方式。Firefox僅支援標準的relatedTarget,IE9也已支援relatedTarget。
getPageX : function(ev) {
return getPageCoord(ev, "X");
},
getPageY : function(ev) {
return getPageCoord(ev, "Y");
},
getXY : function(ev) {
return [this.getPageX(ev), this.getPageY(ev)];
},
getPageX,getPageY呼叫私有的getPageCoord,getPageCoord介紹如上。getXY呼叫getPageX,getPageY。
stopEvent : function(ev) {
this.stopPropagation(ev);
this.preventDefault(ev);
},
stopPropagation : function(ev) {
ev = ev.browserEvent || ev;
if (ev.stopPropagation) {
ev.stopPropagation();
} else {
ev.cancelBubble = true;
}
},
preventDefault : function(ev) {
ev = ev.browserEvent || ev;
if (ev.preventDefault) {
ev.preventDefault();
} else {
ev.returnValue = false;
}
},
這三個方法反過來說,即先說preventDefault,阻止元素的預設行為。如連結A點選,預設會跳轉;input[type=submit]點選,預設會提交表單。
W3C標準使用 preventDefault 方法,IE6/7/8則是設定 returnValue 為false。Safari/Chrome/Opera同時支援IE6/7/8方式。Firefox僅支援標準的preventDefault。IE9現已支援preventDefault。
stopPropagation 用來停止事件冒泡。W3C標準使用stopPropagation,IE6/7/8則是設定 cancelBubble 為true。
Safari/Chrome/Opera/Firefox也支援IE方式取消冒泡。目前為止這是Firefox唯一的一個支援IE方式的屬性。IE9beta現已支援stopPropagation。
stopEvent則同時阻止預設行為和事件冒泡。
getEvent : function(e) {
e = e || win.event;
if (!e) {
var c = this.getEvent.caller;
while (c) {
e = c.arguments[0];
if (e && Event == e.constructor) {
break;
}
c = c.caller;
}
}
return e;
},
getEvent顧名思義獲取事件物件。W3C標準使用響應函式的第一個引數獲取,IE6/7/8則使用window.event獲取。
Safari/Chrome/Opera也支援IE6/7/8方式獲取,IE9beta已支援W3C標準方式獲取。
關於各種情形下事件物件的獲取見:獲取事件物件的全家。
getCharCode : function(ev) {
ev = ev.browserEvent || ev;
return ev.charCode || ev.keyCode || 0;
},
獲取按鍵碼,注意在keypress 事件中使用。鍵盤事件DOM2中壓根沒有標準化,見:Key events
因此各瀏覽器自行實現,Firefox/Safari/Chrome/IE9beta支援charCode,IE6/7/8/Opera不支援但使用keyCode替代。
getListeners : function(el, eventName) {
Ext.EventManager.getListeners(el, eventName);
},
// deprecated, call from EventManager
purgeElement : function(el, recurse, eventName) {
Ext.EventManager.purgeElement(el, recurse, eventName);
},
這兩個方法在後續講述。
再下對load, unload做了單獨處理。
Ext.lib.Event完畢。