這篇開始讀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完畢。