事件是前端之中,非常重要的一個部分。其作用在於對於使用者的各種行為進行相應。近日打算對於事件系統進行更為深入的學習,同時,對於這一部分學習的內容進行一個總結。因為瀏覽器發展至今,事件系統本身已經尤為的複雜了,所以事件這一部分內容可能會將分為很多章來進行總結。本章將對於事件系統,根據個人的經驗,以及其他地方學到的東西進行一個歸納,給出一些簡單地處理方案,而在後面幾章將會引入經典jquery的原始碼進行閱讀。
繫結方式整理
事件系統發展至今,我們常見的對於事件的繫結方式有三種。
- 直接將其寫在元素標籤之中。類似用如下的寫法
1<div onClick="function">
這種方法可以說是非常古老的寫法了,不過至今還是會有人在用。對已現在來說其實並不推薦使用這種方法來繫結事件,其不推薦使用的原因將在第二種繫結方法之中說明。 - 對於第一種onXXX的方法,也採用如下的方法進行繫結
1el.onclick = function
這種方法其實和第一種繫結方法本質上是一樣的。也就是我們常常所說的dom0事件。之前在一種中也說過,其實現在並不推薦使用這種方法,原因如下- 該方法繫結的事件所執行的回撥函式只允許一個,倘若繫結了兩個,那麼第二個將覆蓋掉對於第一個的繫結。
- 該方式只支援事件冒泡
- 在ie下的該方式的回撥函式,並不能像我們往常一樣,擁有事件物件引數。
- 該方式對於dom3的部分新增事件不支援。同時對於FF的部分私有實現也並不支援。
其實對於該方式繫結事件來說,前面三點就決定了,我們不能使用該方法進行繫結事件。而對於第四點,dom3的部分新增事件的不支援,其實我們在日常的使用之中,對於不支援的那些事件,我們的使用頻率也是很低的。因為許多新事件,可能還未曾進入我們的視野,就已經被廢棄了。
- 最後就是我們常說的dom2事件系統了不過dom2事件系統,現存的擁有兩套不同的API,因此,在下面將分為兩方。ie方面,對於事件的繫結,採用如下的方法
1el.attachEvent("on"+type,callback)
這是微軟對於ie5新增的API(除了事件繫結外,還有相應的解綁,建立,派發等)。他解決了之前採用onXXX方法會導致的只允許一個回撥的情況,支援了對於同種事件多個回撥的繫結。但是這套方案,其實並沒有給前端帶來什麼好處,當你對一個事件系統進行處理的適合,應該能很深的感覺到這種方式其帶來的無數問題,以及對於這些問題的解決,會花費很多很多的心思。大致帶來的問題如下
- 對於dom3事件的不支援
- this的指向不是被繫結元素,而是(個人感覺這也是this指向極為特殊的一個情況) – 對於多個回撥的繫結,其執行順序卻並不是按照理所當然的想的那樣按照繫結順序來執行,而是按照不規律的順序來執行的。
- 其event事件物件與w3c的event物件存在極大差異
- 同樣只支援事件冒泡
w3c方面,對於事件的繫結,採用如下方案
1el.addEventListener(type,callback,[phase])這個是我們現代瀏覽器上使用的方法,ie9開始也對於這套API進行了支援,這應該是我們目前最常為使用的方案,當然這套方法也擁有他的一些問題。
-
- 像之前所提到的,新事件本身就是不穩定的。可能還沒有進入人們的視野,就已經被廢棄掉了
- 許多瀏覽器並不遵循w3c的標準,對於一些事件並不予以支援
- 噁心的字首標識部分存在於事件名上
- 因為w3c的標準制定,晚於一些瀏覽器廠商,因此,對於早期一些版本的瀏覽器,事件的物件成員同樣存在和w3c標準差異的情況
事件系統的處理
事件系統是前端之中,極為核心的一個部分,因此,我們必須對其種種問題進行一個個的處理。先拋開強大的jquery的事件處理,倘若我們要寫出一個對於事件系統的處理,並將其投入使用,那麼我們至少應當解決如下的幾個問題。
- 不同瀏覽器對於事件系統的API支援的問題
- IE的this指向問題
- 事件物件的差異性問題
- IE執行回撥的順序問題
那麼,既然整理好了問題,我們現在就可以開始去解決那些問題。
對於不同瀏覽器API的支援問題:
我們採用條件判斷來進行簡單地實現就好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function addEvent(target,eventName,callback,useCapture){ if(target.addEventListener){ //w3c方法優先 target.addEventListener(eventName,callback,useCapture); }else if(target.attachEvent){ //然後採用ie下方法 target.attachEvent("on"+eventName,callback); }else{ //最後在考慮使用onXXX形式 target["on"+eventName] = callback; } //返回回撥函式,方便用於事件解綁 return callback; } function removeEvent(target,eventName,callback,useCapture) { if (target.removeEventListener) { target.removeEventListener(eventName, callback, useCapture); } else if (target.detachEvent) { target.detachEvent("on" + eventName, callback); } else { //onXXX的形式直接將其設定為null即可 target["on" + eventName] = null; } } |
這樣,對於問題1,可以說算是解決了,而這樣的一個事件註冊函式,對於對事件系統簡易需求的頁面,已經很是足夠了。不過既然提出了那些問題,那麼就一一來進行解決吧。
對於ie下this指向的問題:
說點題外話,關於this的指向,其實很簡單的來說,就是是誰呼叫的,this就指向誰。比較籠統的總結一下,日常this的指向就兩種情況,物件中的this,那麼就指向其對應的物件,普通函式中的this,就指向window。然而attachEvent的this指向,卻是指向window的,因此,我們不得不對其進行修改。實現方式很簡單,使用call或者apply,對於this指向進行修改就可以了。因此,上面的程式碼可以修改如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function addEvent(target,eventName,callback,useCapture){ if(target.addEventListener){ //w3c方法優先 target.addEventListener(eventName,handler,useCapture); }else if(target.attachEvent){ //然後採用ie下方法 target.attachEvent("on"+eventName,handler); }else{ //最後在考慮使用onXXX形式 target["on"+eventName] = handler; } //增加handler函式,在其中對於this指向進行改變,同時,採用handler處理函式來進行事件回撥 function handler(){ callback.call(target); } //返回回撥函式,方便用於事件解綁 return handler; } function removeEvent(target,eventName,callback,useCapture) { if (target.removeEventListener) { target.removeEventListener(eventName, callback, useCapture); } else if (target.detachEvent) { target.detachEvent("on" + eventName, callback); } else { //onXXX的形式直接將其設定為null即可 target["on" + eventName] = null; } } |
事件物件的差異性問題:
event物件以及對其的處理也是事件繫結之中,要進行的一個重點內容。而具體的處理,我們將在之前對於this指向處理之中的handler中來一一進行解決。
大致就是對於target,currentTarget,冒泡,取消預設事件這幾部分來進行簡單地處理。修改後的程式碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
function addEvent(target,eventName,callback,useCapture){ if(target.addEventListener){ //w3c方法優先 target.addEventListener(eventName,handler,useCapture); }else if(target.attachEvent){ //然後採用ie下方法 target.attachEvent("on"+eventName,handler); }else{ //最後在考慮使用onXXX形式 target["on"+eventName] = handler; } //處理傳入的引數ev function handler(event){ //ie下的事件名需要window.event var ev = event || window.event, stopPropagation = ev.stopPropagation, preventDefault = ev.preventDefault; //獲取觸發事件的物件 ie下的ev.srcElement相當於其他瀏覽器下ev.target ev.target = ev.target || ev.srcElement; //獲取當前事件活動的物件(捕獲或者冒泡階段) ev.currentTarget = ev.currentTarget || target; //取消冒泡的處理 ev.stopPropagation = function(){ if(stopPropagation){ stopPropagation.call(event); }else{ ev.cancelBubble = true; } }; //取消預設事件的處理 ev.preventDefault = function(){ if(preventDefault){ preventDefault.call(event); }else{ ev.returnValue = false; } }; //執行callback函式,並且this指向,同時用flag接收其返回值 var flag = callback.call(target,ev); //處理flag接收到的返回著為false的情況 if(flag === false){ ev.stopPropagation(); ev.preventDefault(); } } //返回回撥函式,方便用於事件解綁 return handler; } |
補充對於匿名函式取綁的問題:
之前採用了return回撥函式的方法,同時,在繫結函式時,用一個變數來儲存回撥函式,在解綁時再將變數傳入,以達到解綁的目的。
我們來看一段如下的程式碼
1 2 |
var a = addEvent(el,"click", function(){}); removeEvent(el,"click",a); |
這種方法,其實對於解綁來說,程式碼量是很少的。同時,也不需要在解綁的時候,再對程式碼進行修改,將匿名函式變成非匿名,然後在進行操作。當然,這種方法也有些缺陷,匿名函式並不會佔用命名,而這種方案,始終是需要對變數名進行佔用。因此,如果執意於要對於匿名函式進行解綁的話,可以考慮對匿名函式變為非匿名的轉換。參考程式碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
//事件註冊 function addEvent(target,eventName,callback,useCapture){ //壓縮函式的空格 var fnStr = callback.toString().replace(/\s+/g,''); if(!target[eventName+"event"]){ target[eventName+"event"] = {}; } //儲存事件的函式到target[eventName+'event'][fnStr]中 target[eventName+"event"][fnStr] = handler; useCapture = useCapture || false; //高設上的事件註冊簡單相容 if(target.addEventListener){ target.addEventListener(eventName,handler,useCapture); }else if(target.attachEvent){ target.attachEvent("on"+eventName,handler); }else{ target["on"+eventName] = handler; } //處理傳入的引數ev function handler(event){ //ie下的事件名需要window.event var ev = event || window.event, stopPropagation = ev.stopPropagation, preventDefault = ev.preventDefault; //獲取觸發事件的物件 ie下的ev.srcElement相當於其他瀏覽器下ev.target ev.target = ev.target || ev.srcElement; //獲取當前事件活動的物件(捕獲或者冒泡階段) ev.currentTarget = ev.currentTarget || target; //取消冒泡的處理 ev.stopPropagation = function(){ if(stopPropagation){ stopPropagation.call(event); }else{ ev.cancelBubble = true; } }; //取消預設事件的處理 ev.preventDefault = function(){ if(preventDefault){ preventDefault.call(event); }else{ ev.returnValue = false; } }; //執行callback函式,並且this指向,同時用flag接收其返回值 var flag = callback.call(target,ev); //處理flag接收到的返回著為false的情況 if(flag === false){ ev.stopPropagation(); ev.preventDefault(); } } } //事件取綁(匿名函式) function removeEvent(target,eventName,callback,useCapture){ //壓縮空格 var fnStr = callback.toString().replace(/\s+/g,''), handler; if(!target[eventName+"event"]){ return; } //獲取到儲存的函式 handler = target[eventName+"event"][fnStr]; useCapture = useCapture || false; if(target.removeEventListener){ target.removeEventListener(eventName,handler,useCapture); }else if(target.detachEvent){ target.detachEvent("on"+eventName,handler); }else{ target["on"+eventName] = null; } } |
對於ie下執行順序的問題:
很多情況下,我們對於事件繫結的順序肯定是有要求的,因此不按照順序的執行很多時候是我們所不想看到的,因此,我們需要對於回撥的執行順序進行一個處理。處理的思路也不算複雜。倘若在ie下,我們對於同一個事件做了多個回撥,那麼我們將對其進行判斷,並將其合併到一個回撥之中。簡單來說,就是如下的這種思路
1 2 3 4 5 6 7 8 9 |
//對於el繫結了a和b兩個回撥 el.attachEvent("onclick",a) el.attachEvent("onclick",b) //對兩個回撥進行整合處理,然後讓其按順序執行 el.attachEvent("onclick",function(){ a(); b(); }) |
上面是對於思路的一種抽象,不過當真正開始具體執行的時候,其實是很複雜的。
簡單來說,這種執行方案,就是將多個函式進行打包,然後丟到一個事件繫結中去執行,而如果採用addEventListener或者attachEvent直接進行繫結的話,無論如何處理,都很難達到只對事件繫結一次的目的。(起碼用這兩個函式,採取直接對元素進行繫結,我沒想到什麼很好的解決方案。)當然,這也並不是不能解決的,很早之前Dean Edwards大神的addEvent庫就巧妙的解決了這個問題,他並沒有採用流行的addeventListener/attachEvent方法,而是直接採用dom0事件系統對其進行了處理,巧妙的利用了dom0事件系統只能繫結一個事件的特性。現在很流行的jquery事件系統,很多思想也是參考的這個庫中的思想。
因此對於執行順序的處理,在上面的事件註冊之中,並沒有提及到。但是在之後的章節會進行提及。同時,這樣的事件註冊,對於需求不算複雜的頁面,也算是足夠了。
第一章也就到這裡了……