[toc]
## 一些遺留問題
前面介紹 bind、delegate 和它們的 un 方法的時候,經提醒,忘記提到一些內容,卻是我們經常使用的。比如 `$('body').click`,`$('body').mouseleave`等,它們是直接定義在原型上的函數,不知道怎么,就把它們給忽略了。
```
jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
"change select submit keydown keypress keyup contextmenu" ).split( " " ),
function( i, name ) {
// Handle event binding
jQuery.fn[ name ] = function( data, fn ) {
return arguments.length > 0 ?
this.on( name, null, data, fn ) :
this.trigger( name );
};
} );
```
這個構造也是十分巧妙的,這些方法組成的字符串通過 split(" ") 變成數組,而后又通過 each 方法,在原型上對應每個名稱,定義函數,這里可以看到,依舊是 on,還有 targger:
```
jQuery.fn.extend( {
trigger: function(type, data){
return this.each(function (){
// // 依舊是 event 對象上的方法
jQuery.event.trigger(type, data, this);
})
}
} )
還缺少一個 one 方法,這個方法表示綁定的事件同類型只執行一次,.one():
jQuery.fn.extend( {
one: function( types, selector, data, fn ) {
// 全局 on 函數
return on( this, types, selector, data, fn, 1 );
},
} );
```
## DOM 事件知識點
發現隨著 event 源碼的不斷的深入,我自己出現越來越多的問題,比如沒有看到我所熟悉的 addEventListener,還有一些看得很迷糊的 events 事件,所以我決定還是先來看懂 JS 中的 DOM 事件吧。
### 早期 DOM 事件
在 HTML 的 DOM 對象中,有一些以 on 開頭的熟悉,比如 onclick、onmouseout 等,這些就是早期的 DOM 事件,它的最簡單的用法,就是支持直接在對象上以名稱來寫函數:
```
document.getElementsByTagName('body')[0].onclick = function(){
console.log('click!');
}
document.getElementsByTagName('body')[0].onmouseout = function(){
console.log('mouse out!');
}
```
onclick 函數會默認傳入一個 event 參數,表示觸發事件時的狀態,包括觸發對象,坐標等等。
這種方式有一個非常大的弊端,就是相同名稱的事件,會前后覆蓋,后一個 click 函數會把前一個 click 函數覆蓋掉:
```
var body = document.getElementsByTagName('body')[0];
body.onclick = function(){
console.log('click1');
}
body.onclick = function(){
console.log('click2');
}
// "click2"
body.onclick = null;
// 沒有效果
```
### DOM 2.0
隨著 DOM 的發展,已經來到 2.0 時代,也就是我所熟悉的 addEventListener 和 attachEvent(IE),JS 中的事件冒泡與捕獲。這個時候和之前相比,變化真的是太大了,MDN addEventListener()。
變化雖然是變化了,但是瀏覽器的兼容卻成了一個大問題,比如下面就可以實現不支持 addEventListener 瀏覽器:
```
(function() {
// 不支持 preventDefault
if (!Event.prototype.preventDefault) {
Event.prototype.preventDefault=function() {
this.returnValue=false;
};
}
// 不支持 stopPropagation
if (!Event.prototype.stopPropagation) {
Event.prototype.stopPropagation=function() {
this.cancelBubble=true;
};
}
// 不支持 addEventListener 時候
if (!Element.prototype.addEventListener) {
var eventListeners=[];
var addEventListener=function(type,listener /*, useCapture (will be ignored) */) {
var self=this;
var wrapper=function(e) {
e.target=e.srcElement;
e.currentTarget=self;
if (typeof listener.handleEvent != 'undefined') {
listener.handleEvent(e);
} else {
listener.call(self,e);
}
};
if (type=="DOMContentLoaded") {
var wrapper2=function(e) {
if (document.readyState=="complete") {
wrapper(e);
}
};
document.attachEvent("onreadystatechange",wrapper2);
eventListeners.push({object:this,type:type,listener:listener,wrapper:wrapper2});
if (document.readyState=="complete") {
var e=new Event();
e.srcElement=window;
wrapper2(e);
}
} else {
this.attachEvent("on"+type,wrapper);
eventListeners.push({object:this,type:type,listener:listener,wrapper:wrapper});
}
};
var removeEventListener=function(type,listener /*, useCapture (will be ignored) */) {
var counter=0;
while (counter<eventListeners.length) {
var eventListener=eventListeners[counter];
if (eventListener.object==this && eventListener.type==type && eventListener.listener==listener) {
if (type=="DOMContentLoaded") {
this.detachEvent("onreadystatechange",eventListener.wrapper);
} else {
this.detachEvent("on"+type,eventListener.wrapper);
}
eventListeners.splice(counter, 1);
break;
}
++counter;
}
};
Element.prototype.addEventListener=addEventListener;
Element.prototype.removeEventListener=removeEventListener;
if (HTMLDocument) {
HTMLDocument.prototype.addEventListener=addEventListener;
HTMLDocument.prototype.removeEventListener=removeEventListener;
}
if (Window) {
Window.prototype.addEventListener=addEventListener;
Window.prototype.removeEventListener=removeEventListener;
}
}
})();
```
雖然不支持 addEventListener 的瀏覽器可以實現這個功能,但本質上還是通過 attachEvent 函數來實現的,在理解 DOM 早期的事件如何來建立還是比較捉急的。
## addEvent 庫
addEvent庫的這篇博客發表于 2005 年 10 月,所以這篇博客所講述的 addEvent 方法算是經典型的,就連 jQuery 中的事件方法也是借鑒于此,故值得一提:
```
function addEvent(element, type, handler) {
// 給每一個要綁定的函數添加一個標識 guid
if (!handler.$$guid) handler.$$guid = addEvent.guid++;
// 在綁定的對象事件上創建一個事件對象
if (!element.events) element.events = {};
// 一個 type 對應一個 handlers 對象,比如 click 可同時處理多個函數
var handlers = element.events[type];
if (!handlers) {
handlers = element.events[type] = {};
// 如果 onclick 已經存在一個函數,拿過來
if (element["on" + type]) {
handlers[0] = element["on" + type];
}
}
// 防止重復綁定,每個對應一個 guid
handlers[handler.$$guid] = handler;
// 把 onclick 函數替換成 handleEvent
element["on" + type] = handleEvent;
};
// 初始 guid
addEvent.guid = 1;
function removeEvent(element, type, handler) {
// delete the event handler from the hash table
if (element.events && element.events[type]) {
delete element.events[type][handler.$$guid];
}
// 感覺后面是不是要加個判斷,當 element.events[type] 為空時,一起刪了
};
function handleEvent(event) {
// grab the event object (IE uses a global event object)
event = event || window.event;
// 這里的 this 指向 element
var handlers = this.events[event.type];
// execute each event handler
for (var i in handlers) {
// 這里有個小技巧,為什么不直接執行,而是先綁定到 this 后執行
// 是為了讓函數執行的時候,內部 this 指向 element
this.$$handleEvent = handlers[i];
this.$$handleEvent(event);
}
};
```
如果能將上面 addEvent 庫的這些代碼看懂,那么在看 jQuery 的 events 源碼就明朗多了。
還有一個問題,所謂事件監聽,是將事件綁定到父元素或 document 上,子元素來響應,如何實現?
要靠 event 傳入的參數 e:
```
var body = document.getElementsByTagName('body')[0];
body.onclick = function(e){
console.log(e.target.className);
}
```
這個 e.target 對象就是點擊的那個子元素了,無論是捕獲也好,冒泡也好,貌似都能夠模擬出來。接下來,可能要真的步入正題了。
## 總結
感覺事件委托的代碼還是相當復雜的,我自己也啃了好多天,有那么一點點頭緒,其中還有很多模模糊糊的知識點,只是覺得,存在就是牛逼的,我看不懂,但不代表它不牛逼。