一個完整的事件系統,通常存在以下三個角色:
- 事件對象,用於儲存事件的狀態。
- 事件來源對象,當前事件在操作的對象,如元素節點,文檔對象,window對象,XMLHttpRequest對象等。
- 事件監聽器,當一個事件來源產生一個事件對象時,它會調用相應的回呼函數進行操作。在IE中,事件對象恒為全域屬性window.event的分身。
在w3c沒有把其DOM 模型引入網頁時,netscape與微軟已經逼不及待到快他們熟悉的語言中把相關的DOM模型搞進來了。這其實也怪javascript之父忙於把抄襲其他語言,忽略了自身事件系統的建設。從此世界被劃分為兩大陣營了。
DOM0時代,這裡的DOM指w3c的DOM。雙方都設計兩種綁定事件的方法,無侵入式與侵入式。你可以說內聯式與非內聯式的區別。
侵入式,雙方都一樣。沒有辦法,那是很早就實現的。那時IE只有抄襲的份,還不敢胡來。
<input name="ruby" onclick="alert(this.nam)" />
然後是無侵入式,這估計是它們都完成了各自的DOM模型,實現對元素節點的索引機制之後的事了。比如有以下網頁片斷:
我們必須自上而下,一步步找到此元素節點才能操作它。注意,那時沒有所謂的document.getElementById。網景的做法,把相關綁定的代碼放進一個script標籤中:
<form name="form1"> <input type="button" name="button1" value="aaaa"/> </form>
如果你不想把代碼用window.onload = function(){}這代碼塊括起來,那麼你得把這script標籤放於表單元素之後。
微軟也有一套索引機制,基本與網景的一樣,但IE4還引入了document.all與document.all.tags。不過IE還有另一套方式:
<script for="button1" event="onclick" language="JavaScript"> alert("this.aaa") </script>
不過,它用不了this(或者能,我不會),另要求一個script標籤對應body中的一個標籤,實在很浪費,最終被淘汰出局了。
這就是DOM0的綁定機制,另以內聯方式寫在標籤中的代碼,其實相當於以下方式:
<p id="aa" onclick="alert('aaaa')">相當於↓</p> <script type="text/javascript"> var p = document.getElementById("aa") p.onclick = new Function("alert('aaaa')")//相當於↓ p.onclick = function(){alert('aaaa')} </script>
至此,事件系統三個角色都出場了。通過索引機製得到的對象(元素節點什麼的),作為事件來源,onclick,onmousemove之類的事件屬性,它們充當監聽器,onclick後面的函數就是回呼函數,這是非同步執行的。
隨著無侵入的興起,放到web標準中,應該叫做表現行為結構相分離。在標籤內寫onclick什麼的應該唾棄。無侵入式編程有一種讓人越寫越多代碼的慾望。以前總是縮在一個標籤內,隨時注意雙引號與單引號的套嵌,寫多了就煩了,不想寫了,現在沒有這限制,就像脫韁的馬,把更多注意力用於相容更多瀏覽器與創造新的點子上。好了,寫著寫著,人們就開始想能不能在同一個元素上綁定兩個onclick事件呢?!
<script type="text/javascript"> var p = document.getElementById("aa") p.onclick = function(){alert('第一次')} p.onclick = function(){alert('第二次')} </script>
當然,只能alert第二個,我們當然也可以用一些技巧達到這目的:
<p id="aa" onclick="alert('第一次')">能綁定多個同類型函數</p> <script type="text/javascript"> var p = document.getElementById("aa") var addEvent = function(el,type,fn) { var type = "on"+type var old = el[type]; if (typeof el[type] != 'function') { el[type] = fn }else { el[type] = function() { old(); fn(); } } } addEvent(p,"click",function(){alert('第二次')}); addEvent(p,"click",function(){alert('第三次')}); </script>
<br /><!doctype html><br /><html lang="zh-ch"><br /> <head><br /> <meta charset="utf-8" /><br /> <meta content="IE=8" http-equiv="X-UA-Compatible"/><br /> <title>綁定多個事件對象 by 司徒正美</title><br /> <script type="text/javascript"><br /> window.onload = function(){<br /> var p = document.getElementById("aa")<br /> var addEvent = function(el,type,fn) {<br /> var type = "on"+type<br /> var old = el[type];<br /> if (typeof el[type] != 'function') {<br /> el[type] = fn<br /> }else {<br /> el[type] = function() {<br /> old();<br /> fn();<br /> }<br /> }<br /> }<br /> addEvent(p,"click",function(){alert('第二次')});<br /> addEvent(p,"click",function(){alert('第三次')});<br /> }<br /> </script></p><p> </head><br /> <body></p><p> <p id="aa" onclick="alert('第一次')">能綁定多個同類型函數(請點我)</p><br /> </body><br /></html><br />
運行代碼
但當要使用者搞這東西是不行,因此瀏覽器商把它們做成內建的。順帶還搞了個事件流,也就是允許事件對象在控制項間(標籤)中傳遞。IE的一套API是createEventObject, attachEvent, dettachEvent, fireEvent,事件流是自下向上。網景那套就不清楚了,但聽說w3c也是從它那一套發展而來,API比較複雜,createEvent, initEvent,addEventListener, removeEventListener dispatchEvent,那個initEvent還有許多版本呢,如initMouseEvent, initKeyEvent,參數非常多,用於更精確的配置。addEventListener擁有三個參數,但第三個參數通常只在事件代理中有用,通常為false,與IE保持一致,自下而上的冒泡。由於w3c的劣性根,總想與IE劃分界線,它最高能冒泡到window(IE為document):
event = dom.Event(type); args = [event].concat(args); var parent = caller; while(!event.isPropagationStopped() && parent){//isPropagationStopped為w3c dom的一個方法, dom.events.handle.apply( parent,args); //判定是否已禁止冒泡 parent = parent.parentNode || (parent != window) && window; }
很奇怪的是HTML的parentNode竟然是文檔對象。如果是捕獲就麻煩多了,這裡不談它。現在看一下多事件綁定時的相容問題吧。比如上面那個addEvent其實夠用了,DE大神的addEvent也是根據DOM0事件搞出來的。但有一些事件是DOM0絕對類比不了,如FF的DOMMouseScroll事件,因為沒有onDOMMouseScroll這個屬性,它必須要用addEventListener,但IE,opera,chrome等支援的mousewheel。因此我們還是離不開這些進階的API。一個通用addEvent函數:
var addEvent = (function () { if (document.addEventListener) { return function (el, type, fn) { el.addEventListener(type, fn, false); }; } else { return function (el, type, fn) { el.attachEvent('on' + type, function () { return fn.call(el, window.event); }); } }})();
不過還是有問題,IE下綁定回呼函數不是先進先出,詳見《IE與非IE瀏覽器在事件綁定的執行順序問題》。嗯,這些我將留在下一部分講。
PS,這個系列與《javascript 跨瀏覽器的事件系統》系列是不一樣,這裡著重講述設計一個事件系統遇到的各種各樣的問題。而後者則給出具體的解決方案。