對於事件的操作無非是addEvent,fireEvent,removeEvent這三個事 件方法。一般lib都會對浏覽器的提供的函數做一些擴展,解決兼容性內存洩漏等問題。第三個問題就是如何得到domReady的狀態。
6.1 event的包裹
浏覽器的事件兼容性是一個令人頭疼的問題。IE的event在是在全局的window下, 而mozilla的event是事件源參數傳入到回調函數中。還有很多的事件處理方式也一樣。
Jquery提供了一個 event的包裹,這個相對於其它的lib提供的有點簡單,但是足夠使用。
代碼如下:
//對事件進行包裹。
fix : function(event) {
if (event[expando] == true) return event;//表明事件已經包裹過
//保存原始event,同時clone一個。
var originalEvent = event; ①
event = { originalEvent : originalEvent};
for (var i = this.props.length, prop;i;) {
prop = this.props[--i];
event[prop] = originalEvent[prop];
}
event[expando] = true;
//加上preventDefault and stopPropagation,在clone不會運行
event.preventDefault = function() { ②
// 在原始事件上運行
if (originalEvent.preventDefault)
originalEvent.preventDefault();
originalEvent.returnValue = false;
};
event.stopPropagation = function() {
// 在原始事件上運行
if (originalEvent.stopPropagation)
originalEvent.stopPropagation();
originalEvent.cancelBubble = true;
};
// 修正 timeStamp
event.timeStamp = event.timeStamp || now();
// 修正target
if (!event.target) ③
event.target = event.srcElement || document;
if (event.target.nodeType == 3)//文本節點是父節點。
event.target = event.target.parentNode;
// relatedTarget
if (!event.relatedTarget && event.fromElement) ④
event.relatedTarget = event.fromElement == event.target
? event.toElement : event.fromElement;
// Calculate pageX/Y if missing and clientX/Y available
if (event.pageX == null && event.clientX != null) { ⑥
var doc = document.documentElement, body = document.body;
event.pageX = event.clientX
+ (doc && doc.scrollLeft || body && body.scrollLeft || 0)
- (doc.clientLeft || 0);
event.pageY = event.clientY
+ (doc && doc.scrollTop || body && body.scrollTop || 0)
- (doc.clientTop || 0);
}
// Add which for key events
if (!event.which && ((event.charCode || event.charCode === 0) ⑦
? event.charCode : event.keyCode))
event.which = event.charCode || event.keyCode;
// Add metaKey to non-Mac browsers
if (!event.metaKey && event.ctrlKey) ⑧
event.metaKey = event.ctrlKey;
// Add which for click: 1 == left; 2 == middle; 3 == right
// Note: button is not normalized, so don't use it
if (!event.which && event.button) ⑨
event.which = (event.button & 1 ? 1 : (event.button & 2
? 3 : (event.button & 4 ? 2 : 0)));
return event;
},
上面的代碼①處保留原始事件的引用,同時clone原始事件。在這個clone的事件上進行包裹。②處在原始事件上運行 preventDefault 和 stopPropagation兩個方法達到是否阻止默認的事件動作發生和是否停止冒泡事件事件向上傳遞。
③處是修正target個,IE中采用srcElement,同時對於文本節點事件,應該把target傳到其父節點。
④處 relatedTarget只是對於mouseout、mouseover有用。在IE中分成了to和from兩個Target變量,在mozilla中 沒有分開。為了保證兼容,采用relatedTarget統一起來。
⑥處是進行event的坐標位置。這個是相對於page。如果頁面 可以scroll,則要在其client上加上scroll。在IE中還應該減去默認的2px的body的邊框。
⑦處是把鍵盤事件的按 鍵統一到event.which的屬性上。Ext中的實現ev.charCode || ev.keyCode || 0; ⑨則是把鼠標事件的按鍵統一把event.which上。charCode、ev.keyCode一個是字符的按鍵,一個不是字符的按鍵。⑨處采 用&的方式來進行兼容性的處理。 Ext 通過下面三行解決兼容問題。
var btnMap = Ext.isIE ? {1:0,4:1,2:2} : (Ext.isSafari ? {1:0,2:1,3:2} : {0:0,1:1,2:2}); this.button = e.button ? btnMap[e.button] : (e.which ? e.which-1 : -1);
①②③④⑤⑥⑦⑧⑨⑩
6.2 事件的處理
Jquery提供了一些來進行regist,remove,fire事件 的方法。
6.2.1 Register
對於注冊事件,jquery提供了bind、one、toggle、 hover四種注冊事件的方法, bind是最基本的方法。One是注冊只運行一次的方法,toggle注冊交替運行的方法。Hover是注冊鼠標浮過的方法。
代碼如下:
bind : function(type, data, fn) {
return type == "unload" ? this.one(type, data, fn) : this
.each(function() {// fn || data, fn && data實現了data參數可有可無
jQuery.event.add(this, type, fn || data, fn && data);
}); },
Bind中對於unload的事件,只能運行一次,其它的就采用默認的注冊方式。
// 為每一個匹配元素的特定事件(像click)綁定一個一次性的事件處理函數。
// 在每個對象上,這個事件處理函數只會被執行一次。其他規則與bind()函數相同。
// 這個事件處理函數會接收到一個事件對象,可以通過它來阻止(浏覽器)默認的行為。
// 如果既想取消默認的行為,又想阻止事件起泡,這個事件處理函數必須返回false。
代碼如下:
one : function(type, data, fn) {
var one = jQuery.event.proxy(fn || data, function(event) {
jQuery(this).unbind(event, one);
return (fn || data).apply(this, arguments);/this->當前的元素
});
return this.each(function() {
jQuery.event.add(this, type, one, fn && data);
});
},
One與bind基 本上差不多,不同的在調用jQuery.event.add時,把注冊的事件處理的函數做了一個小小的調整。One調用了 jQuery.event.proxy進行了代理傳入的事件處理函數。在事件觸發調用這個代理的函數時,先把事件從cache中刪除,再執行注冊的事件函 數。這裡就是閉包的應用,通過閉包得到fn注冊的事件函數的引用。
//一個模仿懸停事件(鼠標移動到一個對象上面及移出這個對 象)的方法。
//這是一個自定義的方法,它為頻繁使用的任務提供了一種“保持在其中”的狀態。
//當鼠標移動到一個匹配的元素上面時,會 觸發指定的第一個函數。當鼠標移出這個元素時,
/會觸發指定的第二個函數。而且,會伴隨著對鼠標是否仍然處在特定元素中的檢測(例如,處在div 中的圖像),
//如果是,則會繼續保持“懸停”狀態,而不觸發移出事件(修正了使用mouseout事件的一個常見錯誤)。
hover : function(fnOver, fnOut) {
return this.bind('mouseenter', fnOver).bind('mouseleave', fnOut);
},
Hover則是建立在bind的基礎之上。
//每次點擊後依次調用函數。
toggle : function(fn) {
var args = arguments, i = 1;
while (i < args.length)//每個函數分配GUID
jQuery.event.proxy(fn, args[i++]);//修改後的還在args中
return this.click(jQuery.event.proxy(fn, function(event) {//分配GUID this.lastToggle = (this.lastToggle || 0) % i;//上一個函數 event.preventDefault();//阻止缺省動作
//執行參數中的第幾個函數,apply可以采用array-like的參數
return args[this.lastToggle++].apply(this,arguments)||false;
}));
},
Toggle中參數可以是多個fn。先把它們代碼生成UUID。之後調用click的方法來注冊再次進行代理的callback。這個函數在事件觸發時 運行,它先計算上一次是執行了參數中的那個函數。之後阻止缺省動作。之後找到下一個函數運行。
//為jquery對象增加常用 的事件方法
jQuery.each(
("blur,focus,load,resize,scroll,unload,click,dblclick,"
+ "mousedown,mouseup,mousemove,mouseover,mouseout,change,select,"
+ "submit,keydown,keypress,keyup,error").split(","),
function(i, name) {jQuery.fn[name] = function(fn) {
return fn ? this.bind(name, fn) : this.trigger(name);
};});
Jquery增加了一個常用的事件處理方法,包含上面調用的click。這裡可以看出這裡還是調用bind進行注冊。當然這裡還可以通過程序實現去觸發 事件。
上面的眾多方法都是注冊事件,其最終都落在jQuery.event.add();來完成注冊的功能。如果我們采用Dom0或DOM1 的事件方法,我們會采用elem.onclick=function(){}來為元素的某一種事件來注冊處理函數。這個最大的缺點就是每個一個事件只是一 個處理函數。在dom1的方式中有改進,我們可以采用elem.addEventListener(type, handle, false)為元素的事件注冊多個處理函數。
這樣的處理方式還不是很完美,如果我們只這個事件運行一次就有點麻煩了。我們要在事件的處 理函數中最後進行elem.removeEventListener來取消事件的監聽。這樣做可能會有事務上的問題。如果第一個事件處理函數在沒有取消事 件監聽之前,就再次觸發了怎麼辦?
還有采用浏覽器的方式,它不支持自定義事件的注冊和處理,還不能為多個事件注冊同一個處理函數。
代碼如下:
jQuery.event = {// add 事件到一個元素上。
add : function(elem, types, handler, data) {
if (elem.nodeType == 3 || elem.nodeType == 8) return;// 空白節點或注釋
// IE不能傳入window,先復制一下。
if (jQuery.browser.msie && elem.setInterval) elem = window;
// 為handler分配一個全局唯一的Id
if (!handler.guid) handler.guid = this.guid++;
// 把data附到handler.data中
if (data != undefined) { ①
var fn = handler;
handler =this.proxy(fn,function(){return fn.apply(this,arguments);});
handler.data = data;
}
// 初始化元素的events。如果沒有取到events中值,就初始化data: {} ②
var events =jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),
// 如果沒有取到handle中值,就初始化data: function() {....} ③
handle = jQuery.data(elem, "handle")|| jQuery.data(elem, "handle",
function() {//處理一個觸發器的第二個事件和當page已經unload之後調用一個事件。
if (typeof jQuery != "undefined"&& !jQuery.event.triggered)
return jQuery.event.handle.apply(//callee.elem=handle.elem
arguments.callee.elem, arguments);
});
// 增加elem做為handle屬性,防止IE由於沒有本地Event而內存洩露。
handle.elem = elem;
// 處理采用空格分隔多個事件名,如jQuery(...).bind("mouseover mouseout", fn);
jQuery.each(types.split(/s+/), function(index, type) { ④
// 命名空間的事件,一般不會用到。
var parts = type.split(".");type = parts[0];handler.type = parts[1];
// 捆綁到本元素type事件的所有處理函數
var handlers = events[type]; ⑤
if (!handlers) {// 沒有找到處理函數列表就初始化事件隊列
handlers = events[type] = {};
// 如果type不是ready,或ready的setup執行返回false ⑥
if (!jQuery.event.special[type]|| jQuery.event.special[type].setup
.call(elem, data) === false) {// 調用系統的事件函數來注冊事件
if(elem.addEventListener)elem.addEventListener(type,handle,false);
else if (elem.attachEvent)elem.attachEvent("on" + type, handle);
}
}
// 把處理器的id和handler形式屬性對的形式保存在handlers列表中,
// 也存在events[type][handler.guid]中。
handlers[handler.guid] = handler; ⑦
// 全局緩存這個事件的使用標識
jQuery.event.global[type] = true;
});
elem = null; // 防止IE內存洩露。
},
guid : 1,
global : {},
jQuery.event.add通過jQuery.data把事件相關的事件名和處理函數有機有序地組合起存放在 jQuery.cache中與該元素對應的空間裡。我們就一個例子分析一下add的過程中:假如我們招待下面 jQuery(e1).bind("mouseover mouseout", fn0);jQuery(e1).bind("mouseover mouseout", fn1)的語句。
在jQuery(e1).bind("mouseover mouseout", fn0);時,②③都不可能從cache取到數,先初始化。此時的cache:{e1_uuid:{events:{},handle:fn}}。接著在 ⑤會為mouseover mouseout名初始化。此時的cache: {e1_uuid:{events:{ mouseover:{}, mouseout:{}},handle:fn}}。在⑥處向浏覽器的事件中注冊處理函數。接著⑦會把處理函數到事件名中。此時的cache: {e1_uuid:{events:{mouseover:{fn0_uuid:fn0},mouseout:{ fn0_uuid:fn0}},handle:fn}}。這裡可以看出為采用proxy為函數生成uuid的作用了。
在jQuery(e1).bind("mouseover mouseout", fn1)時,②③都從cache取到數據{e1_uuid:{events:{mouseover:{fn0_uuid:fn0},mouseout:{ fn0_uuid:fn0}},接著在⑤取到mouseover:{fn0_uuid:fn0},mouseout:{ fn0_uuid:fn0}的引用。接著⑦會把處理函數注冊到事件名中。此時的cache: {e1_uuid:{events:{mouseover:{fn0_uuid:fn0, fn1_uuid:fn1,},mouseout:{ fn0_uuid:fn0, fn1_uuid:fn1}},handle:fn}}。
jQuery.event.add很重要的任務 就是把注冊的事件函數有序地存放起來。以便remove和fire事件的函數能找到。
//{elem_uuid_1:{events:{mouseover:{fn_uuid:fn1,fn_uuid1:fn2},
//mouseout:{fn_uuid:fn1,fn_uuid1:fn2}},handle:fn}}
6.2.2 trigger
注冊了事件,如onclick。那麼當用戶點擊這個元素時,就會自動觸發這個事件的已經注冊的事件處理函數。但是我們有的時候要采用程 序來模擬事件的觸發就得采用強迫觸發某個事件。在IE中我們可以采用.fireEvent()來實現。如:<form onsubmit="a()" >中,如果button的form.submit()的方式提交表單,是不會主動觸發onsumbit事件的,如果必須的話,就要在submit 前$(“:form”)[0].fireEvent("onsubmit”,),這樣就會觸發該事件。
在mozilla中有三個步驟: var evt = document.createEvent('HTMLEvents');
evt.initEvent('change',true,true); t.dispatchEvent( evt );
在 prototype是采用這樣的方式來實現的。那麼jquery中呢,它的實現方式有一點不一樣。
代碼如下:
trigger : function(type, data, fn) {
return this.each(function() {
jQuery.event.trigger(type, data, this, true, fn);
}); },
Trigger有三個參數,data參數是為了注冊的事件函數提供了實傳。如果data[0]中preventDefault存在,data[0]就可 以做為用戶自定義的包裹事件的空間。Fn是可以為事件提供一個即時即用的事件處理方法。也就是在沒有注冊事件的情況下也可以通過傳入處理函數來處理事件。 如果已經注冊了,那就是在原來的事件處理函數之後執行。
//這個方法將會觸發指定的事件類型上所有綁定的處理函數。但不會 執行浏覽器默認動作.
triggerHandler : function(type, data, fn) {
return this[0]&& jQuery.event.trigger(type,data,this[0],false,fn);
},
triggerHandle通過把jQuery.event.trigger的donative參數設為false,來阻止執行浏覽器 默處理方法。它與trigger不現的一點,還在於它只是處理jquery對象的第一個元素。
上面兩個方法都調用了 jQuery.event.trigger來完成任務:
代碼如下:
trigger : function(type, data, elem, donative, extra) {
data = jQuery.makeArray(data);//data可以為{xx:yy}
//支持getData!這樣的形式,exclusive = true表現會對add的注冊的
//事件的所有函數進行命名空間的分種類的來執行。
if (type.indexOf("!") >= 0) { ①
type = type.slice(0, -1);var exclusive = true;
}
if (!elem) {// 處理全局的fire事件 ②
if (this.global[type])
jQuery.each(jQuery.cache, function() {
// 從cache中找到所有注冊該事件的元素,觸發改事件的處理函數
if (this.events && this.events[type])
jQuery.event.trigger(type, data, this.handle.elem);
});
} else {// 處理單個元素事件的fire事件 ③
if (elem.nodeType == 3 || elem.nodeType == 8) return undefined;
var val, ret, fn = jQuery.isFunction(elem[type] || null),
// 如果data參數傳進入的不是浏覽器的event對象的話,event變量為true.
//如果data參數本身是婁組,那麼第一個元素不是 浏覽器的event對象時為true.
//對於event為true。即沒有event傳進入,先構建一個偽造的event對象存在 data[0]。
event = !data[0] || !data[0].preventDefault;
// 在沒有傳入event對象的情況下,構建偽造event對象。
if (event) {//存到數組中的第一個 ④
data.unshift( { type : type,target : elem,
preventDefault : function() {},stopPropagation :
function() {}, timeStamp : now() });
data[0][expando] = true; // 不需要修正偽造的event對象
}
data[0].type = type; //防止事件名出錯
//表現會進行事件注冊函數的分類(命名空間)執行。不是所有的。
if (exclusive) data[0].exclusive = true;
//與prototype等傳統的處理 方式不一樣,沒有采用fireEvent來
//來fire通過注冊到浏覽器事件中的事件處理方法。
//這裡分了三步,先fire 通過jQuery.event.add來注冊的事件,這個事件
//有可能是自定義的事件(沒有注冊到浏覽器事件中)。
//第二步 是fire通過elem.onclick方式注冊的事件的本地處理函數
//第三步是fire默認的事件處理方式(在本地的onclick的方式注冊
//不存在的情況下)。
// 這裡是觸發通過jQuery.event.add來注冊的事件,
var handle = jQuery.data(elem, "handle"); ⑤
if (handle)val = handle.apply(elem, data); //這裡data分成多個參數
//處理觸發通過elem.onfoo=function()這樣的注冊本地處理方法,
//但是是 對於links 's .click()不觸發,這個不會執行通過addEvent
//方式注冊的事件處理方式。
if ((!fn || (jQuery.nodeName(elem, 'a') && type == "click")) ⑥
&& elem["on"+ type]&& elem["on"+type].apply(elem,data) === false)
val = false;
//額外的函 數參數的開始幾個是通過data給定的。這裡會把偽造加上的event給去掉。
//它的最後一個參數是一系列的事件處理函數返回的結果,一般為 bool值
//這個函數可以根據這個結果來處理一個掃尾的工作。
if (event) data.shift();
// 處理觸發extra給定的函數處理。
if (extra && jQuery.isFunction(extra)) { ⑦
ret = extra.apply(elem, val == null ? data : data.concat(val));
//如果這個函數有返回值,那麼trigger的返回值就是它的返回值
//沒有的 話就是串連的事件處理函數的最後一個返回值。一般為bool
if (ret !== undefined) val = ret;
}
// 觸發默認本地事件方法,它是在沒有如.onclick注冊事件
//加上前面的執行事件處理函數返回值都不為 false的情況下,才會執行。
//它還可以通donative來控制是否執行。
//如form中可以采用 this.submit()來提交form.
if (fn && donative !== false && val !== false ⑧
&& !(jQuery.nodeName(elem, 'a') && type == "click")) {
this.triggered = true;
try {elem[type](); //對於一些hidden的元素,IE會報錯
} catch (e) {}
}
this.triggered = false;
}
return val;
},
Jquery的fire事件的方法與prototype中實現是完全不一樣的。Ext、YUI沒有提供強迫觸發事件的方法。對於一般的 思維,程序來觸發浏覽器的事件就應該采用fireEvent或dispatchEvent方法來運行。
但是jquery采用一種不同的 方法。對於通過jquery.event.add來注冊的事件(不管是自定義的還是注冊到浏覽器事件),它保存在一個與元素及事件名相對應的cache 中。在浏覽器的觸發中,這個是沒有什麼作用。但是它是為了通過等程序來強迫觸發時,從cache中取到對應的事件處理函數。這個時候就拋開了浏覽器的事 件。在這裡還可以執行一些自定義的事件函數。如⑤處。
對於通過html的標簽中如click或 elem.onclick=function(){}形式注冊的事件函數。在⑥處它采用執行元素的如onclick形式的回調函數就可以。通過這種 dom0的方式只能注冊一個函數。
有的時候,如果沒有onclick這樣的事件處理函數,浏覽器會執行默認的處理函數。如 form.submit()。⑧處可以看出對於這樣的默認的事件處理,還可以通過參數donative來控制。
程序手動強迫觸發事件, 有一點問題就是event是怎麼生成,就是沒有浏覽器生成event傳入到函數中。Prototype采用了是新生成的dataavailable的事 件。這樣的事件也沒有什麼作用。Jquery也采用fake的方式偽造一個一個事件,如④,它比prototype的事件好處在於它能通過trigger 的函數的參數來傳入需要的event。Prototype則不能。
通過上面的分析,隱隱可以看出Jquery是通過模擬浏覽器的觸發事 件的執行過程來構建這個trigger的函數的。先執行dom1方式(addEvent)注冊的事件,再執行dom0方式注冊的事件,最後看看要不要執行 默認的事件處理。
在⑦處,我們可以看出trigger還可能通過傳入回調函數和參數來完成對執行的事件處理函數的結果進行判斷處理,形 成新結果通過trigger的函數返回。這在有的時候是很有用的。
除了這些,它還能對於事件的處理函數進行分類(namespace),可以在合適的時候調用事件的不同分類的的處理函數(通過 jquery.event.add來注冊)。這個分類的處理在handle實現。
代碼如下:
handle : function(event) {
// 返回 undefined or false
var val, ret, namespace, all, handlers;
//修改了傳入的參數,這裡是引用。
event = arguments[0] = jQuery.event.fix(event || window.event);
// 命名空間處理
namespace = event.type.split(".");
event.type = namespace[0];
namespace = namespace[1];
// all = true 表明任何 handler,namespace不存在,同時
//event.exclusive不存在或為假時,all=true.
all = !namespace && !event.exclusive;
// 找到元素的events中緩存的事件名的處理函數列表
handlers = (jQuery.data(this, "events") || {})[event.type];
for (var j in handlers) {// 每個處理函數執行
var handler = handlers[j];
// Filter the functions by class
if (all || handler.type == namespace) {
// 傳入引用,為了之後刪除它們
event.handler = handler;
event.data = handler.data;//add的時候加上的
ret = handler.apply(this, arguments);// 執行事件處理函數
if (val !== false)
val = ret;// 只要有一個處理函數返回false,本函數就返回false.
if (ret === false) {// 不執行浏覽器默認的動作
event.preventDefault();
event.stopPropagation();
}
}
}
return val; }