這部分說一下最近非常流行的事件代理。事件代理的實現簡單來說,是把事件綁定到目標元素的祖先元素上,然後通過冒泡或捕獲得到事件來源,然後再判定事件來源是否等於目標元素再執行回呼函數。由於對目標元素的判定有時非常模糊,因此通過判定即可調用回呼函數,這樣,我們就達到一個監聽器為許多事件來源服務的目的。對於效能一向非常不怎麼樣的IE6來說,實在幫了一個大忙。
假如,有一個無序列表,點擊彈出它的innerHTML,如果按事件綁定的做法,代碼大概是這樣:
var addEvent = function(el,type,callback,data){ if ( el.addEventListener ) {//如自訂對象就綁定回呼函數了 el.addEventListener( type, callback, !!data ); } else if ( el.attachEvent ) { el.attachEvent( "on" + type, function(){ return callback.call(el,window.event) } ); } } var els = document.getElementsByTagName("li") for(var i=0,n=els.length;i<n;i++){ addEvent(els[i],"click",function(){ alert(this.innerHTML) }) } }
<br /><!doctype html><br /><html lang="zh-ch" id="html"><br /> <head><br /> <meta charset="utf-8" /><br /> <meta content="IE=8" http-equiv="X-UA-Compatible"/><br /> <title>事件代理 by 司徒正美</title></p><p> <script type="text/javascript"></p><p> window.onload = function(){<br /> var addEvent = function(el,type,callback,data){<br /> if ( el.addEventListener ) {//如自訂對象就綁定回呼函數了<br /> el.addEventListener( type, callback, !!data );<br /> } else if ( el.attachEvent ) {<br /> el.attachEvent( "on" + type, function(){<br /> return callback.call(el,window.event)<br /> } );<br /> }<br /> }<br /> var els = document.getElementsByTagName("li")<br /> for(var i=0,n=els.length;i<n;i++){<br /> addEvent(els[i],"click",function(){<br /> alert(this.innerHTML)<br /> })<br /> }<br /> }</p><p> </script></p><p> </head><br /> <body id="body"><br /> <h1>事件代理</h1><br /> <ul><br /> <li>無序列表1</li><br /> <li>無序列表2</li><br /> <li>無序列表3</li><br /> <li>無序列表4</li><br /> </ul><br /> </body><br /></html><br />
運行代碼
如果用事件代理的做法大概是這樣:
var ul = document.getElementsByTagName("ul")[0] addEvent(ul,"click",function(e){ var el = e.srcElement || e.target; if(el.tagName === "LI") alert(el.innerHTML) })
<br /><!doctype html><br /><html lang="zh-ch" id="html"><br /> <head><br /> <meta charset="utf-8" /><br /> <meta content="IE=8" http-equiv="X-UA-Compatible"/><br /> <title>事件代理 by 司徒正美</title></p><p> <script type="text/javascript"></p><p> window.onload = function(){<br /> var addEvent = function(el,type,callback,data){<br /> if ( el.addEventListener ) {//如自訂對象就綁定回呼函數了<br /> el.addEventListener( type, callback, !!data );<br /> } else if ( el.attachEvent ) {<br /> el.attachEvent( "on" + type, function(){<br /> return callback.call(el,window.event)<br /> } );<br /> }<br /> }<br /> var ul = document.getElementsByTagName("ul")[0]<br /> addEvent(ul,"click",function(e){<br /> var el = e.srcElement || e.target;<br /> if(el.tagName === "LI")<br /> alert(el.innerHTML)<br /> })<br /> }</p><p> </script></p><p> </head><br /> <body id="body"><br /> <h1>事件代理</h1><br /> <ul><br /> <li>無序列表1</li><br /> <li>無序列表2</li><br /> <li>無序列表3</li><br /> <li>無序列表4</li><br /> </ul><br /> </body><br /></html><br />
運行代碼
但是這樣複用性不好,我們需要一個真正的事件代理函數。如果用YUI與jQuery的人應該知道,這其中必然涉及到選取器,利用選取器進行匹配,實現上面的el.tagName === "LI",只不過適用性更廣罷了。但現在我們不可能搞鼓個選取器出來,那可不是一下子就能實現的東西,況且它與我們的主題,為簡單起見,我們就使用原生的標籤選取器。換言之,新函數的參數之一,就是目標元素的tagName。其次,我們需要一個或一些代理元素,就像上面例子那個ul元素。我在《事件冒泡》一文中也給出各元素冒泡的情況了,若無視不能冒泡的事件,那麼元素大抵是分上浮到from元素與上浮到文檔這兩種情況,當然還有上浮到window的情況,但最高只能上浮document。現在我們無視這些情況,強製為document。
var delegate = function(a,type,callback){ var els = document.getElementsByTagName(a); addEvent(document,type,function(e){ var el = e.srcElement || e.target; for(var i=0,n=els.length;i<n;i++){ if(el === els[i]){ return callback.call(el,e) } } },true); } delegate("li","click",function(e){ alert(this.innerHTML+e.type) });
<br /><!doctype html><br /><html lang="zh-ch" id="html"><br /> <head><br /> <meta charset="utf-8" /><br /> <meta content="IE=8" http-equiv="X-UA-Compatible"/><br /> <title>事件代理 by 司徒正美</title></p><p> <script type="text/javascript"></p><p> window.onload = function(){<br /> var addEvent = function(el,type,callback,data){<br /> if ( el.addEventListener ) {//如自訂對象就綁定回呼函數了<br /> el.addEventListener( type, callback, !!data );<br /> } else if ( el.attachEvent ) {<br /> el.attachEvent( "on" + type, function(){<br /> return callback.call(el,window.event)<br /> } );<br /> }<br /> }<br /> var delegate = function(a,type,callback){<br /> var els = document.getElementsByTagName(a);<br /> addEvent(document,type,function(e){<br /> var el = e.srcElement || e.target;<br /> for(var i=0,n=els.length;i<n;i++){<br /> if(el === els[i]){<br /> return callback.call(el,e)<br /> }<br /> }<br /> },true);<br /> }<br /> delegate("li","click",function(e){<br /> alert(this.innerHTML+e.type)<br /> });</p><p> }<br /> var aaa = function(){<br /> var li = document.createElement("li");<br /> li.innerHTML = "動態添加"+(aaa.aa++)<br /> document.getElementsByTagName("ul")[0].appendChild(li)<br /> }<br /> aaa.aa = 0<br /> </script></p><p> </head><br /> <body id="body"><br /> <ul><br /> <li>無序列表1</li><br /> <li>無序列表2</li><br /> <li>無序列表3</li><br /> <li>無序列表4</li><br /> </ul><br /> <button type="button" onclick="aaa()">動態添加</button><br /> </body><br /></html><br />
運行代碼
這裡有個奇妙的現象,當我們執行delegate函數,LI元素的個數為4,每當我們動態添加一個時,它也會相應添加一個。輕鬆實現jQuery所謂的live函數。不過,當我們把els轉換為純數組時,它就失去這種動態性了。jQuery的選取器等價於getElementsByTagName嗎?不等於!因此,我們要重新來過了!
var delegate = function(a,type,callback){ var els = document.getElementsByTagName(a), tmp = []; for(var j=0,jn=els.length;j<jn;j++){ tmp.push(els[j]) } els = tmp;//強制轉換為純數組 addEvent(document,type,function(e){ var el = e.srcElement || e.target; for(var i=0,n=els.length;i<n;i++){ if(el === els[i]){ return callback.call(el,e) } } },true); }
<br /><!doctype html><br /><html lang="zh-ch" id="html"><br /> <head><br /> <meta charset="utf-8" /><br /> <meta content="IE=8" http-equiv="X-UA-Compatible"/><br /> <title>事件代理 by 司徒正美</title></p><p> <script type="text/javascript"></p><p> window.onload = function(){<br /> var addEvent = function(el,type,callback,data){<br /> if ( el.addEventListener ) {//如自訂對象就綁定回呼函數了<br /> el.addEventListener( type, callback, !!data );<br /> } else if ( el.attachEvent ) {<br /> el.attachEvent( "on" + type, function(){<br /> return callback.call(el,window.event)<br /> } );<br /> }<br /> }<br /> var delegate = function(a,type,callback){<br /> var els = document.getElementsByTagName(a),<br /> tmp = [];<br /> for(var j=0,jn=els.length;j<jn;j++){<br /> tmp.push(els[j])<br /> }<br /> els = tmp;//強制轉換為純數組<br /> addEvent(document,type,function(e){<br /> var el = e.srcElement || e.target;<br /> for(var i=0,n=els.length;i<n;i++){<br /> if(el === els[i]){<br /> return callback.call(el,e)<br /> }<br /> }<br /> },true);<br /> }<br /> delegate("li","click",function(e){<br /> alert(this.innerHTML+e.type)<br /> });</p><p> }<br /> var aaa = function(){<br /> var li = document.createElement("li");<br /> li.innerHTML = "動態添加"+(aaa.aa++)<br /> document.getElementsByTagName("ul")[0].appendChild(li)<br /> }<br /> aaa.aa = 0<br /> </script></p><p> </head><br /> <body id="body"><br /> <ul><br /> <li>無序列表1</li><br /> <li>無序列表2</li><br /> <li>無序列表3</li><br /> <li>無序列表4</li><br /> </ul><br /> <button type="button" onclick="aaa()">動態添加</button><br /> </body><br /></html><br />
運行代碼
為了盡量接近現實中的複雜需求,我最後還是給出一個小型選取器吧,注意只能運行於IE8(要求是標準模式)與較新的標準瀏覽器下。為了實現jQuery.live的效果,我們對代碼做了一些調整:
var $ = function(selector,context){ context = context || document try{ var els = context.querySelectorAll(selector), result = [],ri=0,i=0,n=els.length; for(;i<n;i++){ result[ri++] = els[i] } return result; }catch(e){ alert("你的瀏覽器不支援querySelectorAll") } } var delegate = function(a,type,callback){ addEvent(document,type,function(e){ var els = $(a); var node = e.srcElement || e.target; for(var i=0,el;el = els[i++];){ if(node === el){ return callback.call(node,e) } } },true); } delegate("#list li","click",function(e){ alert(this.innerHTML+e.type) });
<br /><!doctype html><br /><html lang="zh-ch" id="html"><br /> <head><br /> <meta charset="utf-8" /><br /> <meta content="IE=8" http-equiv="X-UA-Compatible"/><br /> <title>事件代理 by 司徒正美</title></p><p> <script type="text/javascript"><br /> window.onload = function(){<br /> var addEvent = function(el,type,callback,data){<br /> if ( el.addEventListener ) {//如自訂對象就綁定回呼函數了<br /> el.addEventListener( type, callback, !!data );<br /> } else if ( el.attachEvent ) {<br /> el.attachEvent( "on" + type, function(){<br /> return callback.call(el,window.event)<br /> } );<br /> }<br /> }</p><p> var delegate = function(a,type,callback){<br /> addEvent(document,type,function(e){<br /> var els = $(a);<br /> var node = e.srcElement || e.target;<br /> for(var i=0,el;el = els[i++];){<br /> if(node === el){<br /> return callback.call(node,e)<br /> }<br /> }<br /> },true);<br /> }<br /> delegate("#list li","click",function(e){<br /> alert(this.innerHTML+e.type)<br /> });<br /> }<br /> var $ = function(selector,context){<br /> context = context || document<br /> try{<br /> var els = context.querySelectorAll(selector),<br /> result = [],ri=0,i=0,n=els.length;<br /> for(;i<n;i++){<br /> result[ri++] = els[i]<br /> }<br /> return result;<br /> }catch(e){<br /> alert("你的瀏覽器不支援querySelectorAll")<br /> }<br /> }<br /> var aaa = function(){<br /> var li = document.createElement("li");<br /> li.innerHTML = "動態添加"+aaa.aa++<br /> $("#list")[0].appendChild(li)<br /> }<br /> aaa.aa = 0;<br /> </script><br /> </head><br /> <body id="body"><br /> <h1>司徒正美:事件代理</h1><br /> <ul id="list"><br /> <li>無序列表1</li><br /> <li>無序列表2</li><br /> <li>無序列表3</li><br /> <li>無序列表4</li><br /> </ul><br /> <p>下面的無序列表是沒有id的!</p><br /> <ul><br /> <li>無序列表1</li><br /> <li>無序列表2</li><br /> <li>無序列表3</li><br /> <li>無序列表4</li><br /> </ul><br /> <button type="button" onclick="aaa()">動態添加</button></p><p> </body><br /></html><br />
運行代碼
就這樣,一個粗糙的事件代理系統完成了。但有個問題,我們每次進入這個函數,就調用選取器,是不是很耗效能呢?因為動態添加的情況並不是每次都發生,我們只要比較原來的節點集合就行了,當發現原來的找不到,再調用選取器也不晚。第四部分我們將解決這問題,see you!