前端互動體驗核心之事件(一)
- 如何繫結事件處理函式
- 事件處理程式的執行環境
- 解除事件處理程式
- 事件處理模型————冒泡、捕獲
- 預設事件
- 事件物件
- 事件委託
如何繫結、解除事件處理函式
一、繫結事件處理函式
1.ele.on xxx = function(event){}
- 相容性好,但是一個元素的同一個事件只能繫結一個處理函式
- 基本等同於寫在HTML行間
- 行間控制代碼繫結示例:<div style="width:100px;height:100px;background-color:red;" onclick="console.log('誰點我了?')"></div>
2.dom.addEventListener(‘事件型別‘,處理函式,false);
- IE9以下不相容,可以為一個事件繫結多個處理程式(W3C標準)
- 繫結同一個處理函式多次,不能執行多次
- 第三個引數指定事件是否在捕獲或冒泡階段執行
3.dom.attachEvent('on'+ 事件型別 , 處理函式);
- IE獨有,一個事件同樣可以繫結多個處理程式
- 繫結同一個處理函式多次,可以執行多次
- 示例:dom.attachEvent('onClick',function(){});
二、事件處理程式的執行環境
1.ele.onxxx = function(event){} ==> 程式this指向是DOM元素本身。
2.dom.addEventListener(type,fun,false); ==> 程式this指向是DOM元素本身。
3.dom.attachEvent('on' + Type ,fun); ==>程式this指向是window。
瞭解this指向規則可以訪問我的另一篇部落格:JavaScript中的this指向規則
封裝相容性的事件繫結函式addEvent(elem,type,handle)方法:(這個方法沒有考慮事件模型設定,還可以改進)
//實現全相容性的事件函式繫結方法 //addEventListener是w3c標準方法,IE9以下不相容 //attachEvent是ie的獨有方法,但是this指向window,通過call修正this指向dom本身 //on...方式不能繫結一個方法 function addEvent(elem,type,handle){ if(elem.addEventListener){ elem.addEventListener(type,handle,false); }else if(elem.attachEvent){ elem.attachEvent('on' + type, function(){ handle.call(elem); }); }else{ type = type.substring(0,1).toUpperCase()+type.substring(1); elem['on' + type] = handle; } }
三、解除事件處理程式
1.ele.on XXX = function(){}的事件解除和應用:
- 理解:控制代碼繫結的事件程式,是一個變數(屬性)賦值的方式。
- 解決方案:將值清空即等於解除繫結程式。
- 實現:ele.on XXX = false/""/null
應用:典型應用功能是網頁內的點選跳轉連結小廣告,點選一次後再點選就不會出現跳轉。(案例模擬邏輯,沒有具體實現)
//html <div style="width: 100px;height: 100px;background-color: red;"></div> //js var advDIV = document.getElementsByTaName('div')[0]; advDIV.onclick = function(){ console.log(a); advDIV.onclick = null; }
2.dom.addEventListener(‘事件型別‘,處理函式,false)的事件解除:
- 解除方法:dom.removeEventListener(‘事件型別‘,處理函式,false)
- 理解:與繫結的DOM物件要一致,事件、函式、要一一對應。
3.dom.attachEvent('on'+ 事件型別 , 處理函式)的事件解除:
- 解除方法:dom.detachEvent('on' + type, fn)
- 理解:與removeEventListener方法一致
解除事件處理程式需要注意的問題 (addEventListener、attachEvent):若繫結匿名函式,則無法解除。比如直接在繫結函式時直接在繫結方法上寫入函式表示式,這種繫結是無法解除的。
advDIV.addEventListener('click',function(){ console.log("a"); },false);
事件處理模型(冒泡、捕獲)
四、事件冒泡
定義:結構上(非視覺上)巢狀關係的元素,會存在事件冒泡功能,即同一事件,自子元素冒泡向父元素。(自底向上)
解析:事件冒泡就是巢狀的元素,同一事件在子元素上觸發後,從子元素到父元素及祖輩元素依照巢狀層級關係逐個觸發執行。
實現:在繫結事件程式時,第三個引數設定為false(預設)及設定dom的事件觸發模型為事件冒泡。(例如: addEventListener('click', function (){... }, false);)
點選測試
例項測試的事件採用控制代碼繫結的彈窗(控制代碼繫結可以預設觸發事件冒泡),瀏覽器如果有設定禁止彈窗無法測試,最後附上程式碼:
//html(為了讓程式碼更清晰,css程式碼大家自己填充) <div class="wrapper"> <div class="content"> <div class="box"></div> </div> </div> //js(重點在於理解事件冒泡,事件繫結沒有相容IE9以下的瀏覽器) var wrapper = document.getElementsByClassName("wrapper")[0]; var content = document.getElementsByClassName("content")[0]; var box = document.getElementsByClassName("box")[0]; wrapper.addEventListener('click',function(){ console.log("wrapper"); },false); content.addEventListener('click',function(){ console.log("console"); },false); box.addEventListener('click',function(){ console.log("box"); },false);
五、事件捕獲
定義:結構上(非視覺上)巢狀關係的元素,會存在事件捕獲功能,即同一事件,自父元素捕獲至子元素。(自上向下)
解析:事件捕獲就是巢狀的元素,同一事件在子元素上觸發時,從頂層祖輩元素到父元素再到自身逐個被觸發執行。
實現:在繫結事件程式時,第三個引數設定為true及設定dom的事件觸發模型為事件冒泡。(例如: addEventListener('click', function (){... }, true);)
由於控制代碼繫結事件程式不能設定事件模型,而控制代碼預設事件模型為事件冒泡,所以這裡不能提供例項演示,下面程式碼提供參考:
wrapper.addEventListener('click',function(){ console.log("wrapper"); },true); content.addEventListener('click',function(){ console.log("console"); },true); box.addEventListener('click',function(){ console.log("box"); },true);
- IE9以下的瀏覽器事件捕獲沒有事件捕獲模型addEventListener( type, fun, true)只有IE9及以上瀏覽器相容
- attachEvent('on' + type , fun)沒有第三個控制事件模型的引數,實質上與控制代碼類似,預設事件模型為冒泡
六、事件模型下事件程式的執行邏輯
- 觸發順序:先捕獲,後冒泡
- 捕獲:從外層向內層執行
- 冒泡:從內層向外層執行
後面兩個邏輯其實就是時間模型的本身特性,這裡的重點在第一個執行邏輯。其實前面有個設定事件模型的重要點沒有講,那就是同一個事件的同一個處理程式只能設定一種事件模型,但是同一個事件的不同處理程式可以設定不同的事件模型,所以就引出了先捕獲後執行的事件處理邏輯。
wrapper.addEventListener('click',function(){ console.log("wrapper"); },true); content.addEventListener('click',function(){ console.log("console"); },true); box.addEventListener('click',function(){ console.log("box"); },true); wrapper.addEventListener('click',function(){ console.log("wrapperBubble"); },false); content.addEventListener('click',function(){ console.log("consoleBubble"); },false); box.addEventListener('click',function(){ console.log("boxBubble"); },false); //執行結果 // wrapper // console // box // boxBubble // consoleBubble // wrapperBubble
這個先後更詳細的來說應該是:先將事件的捕獲模型的事件程式依次執行完再依次執行事件冒泡模型的事件程式。但是這裡有一個很容易被忽略的問題,因為我這個示例程式碼是將捕獲事件放到了前面,如果在後面呢?
wrapper.addEventListener('click',function(){ console.log("wrapperBubble"); },false); content.addEventListener('click',function(){ console.log("consoleBubble"); },false); box.addEventListener('click',function(){ console.log("boxBubble"); },false); wrapper.addEventListener('click',function(){ console.log("wrapper"); },true); content.addEventListener('click',function(){ console.log("console"); },true); box.addEventListener('click',function(){ console.log("box"); },true); //執行結果 // wrapper // console // boxBubble // box // consoleBubble // wrapperBubble
這次有點驚喜了,box的執行邏輯發生了變化,但是我們暫時不考慮這個問題,從總體上來看開始遵循了先執行捕獲模式的事件程式,然後執行冒泡模式的事件程式。那為什麼box的執行循序會發生變化呢?
這裡需要注意的是,被事件模型被動觸發的事件程式是按照先捕獲後冒泡的原則,但是被主動觸發事件的DOM執行的原則是事件執行(並非冒泡執行或捕獲執行),所以會遵循先繫結先執行的原則。
注:focus,blur,change,submit,select等事件不冒泡。
七、取消事件冒泡
在一些巢狀結構複雜的設計中,可能會出現不希望被子元素冒泡觸發事件程式執行的情況,這時需要在子元素的事件程式中執行取消冒泡行為的方法(event.stopPropagation()),這個方法會取消當前事件程式冒泡到父級元素 。例如前面的冒泡事件坐下面修改:
wrapper.addEventListener('click',function(){ console.log("wrapperBubble"); },false); content.addEventListener('click',function(e){ console.log("consoleBubble"); e.stopPropagation();//取消"content"冒泡到"wrapper" },false); box.addEventListener('click',function(e){ console.log("boxBubble"); },false);
以上示例中,當點選box會執行“boxBubble”,“contentBubble”。點選content時只觸發自身事件程式。
event.stopPropagation()是W3C的標準方法,但是任性IE(IE9以下)需要event.cancelBubble = true;屬性來取消事件冒泡。所以需要我們實現一個相容方法:
//封裝取消冒泡的函式相容方法stopBubble(event) function stopBubble(event){ if(event.stopPropagation){ event.stopPropagation(); }else{ event.cancelBubble = true; } }
預設事件與事件物件
八、預設事件及取消預設事件
- 常見的預設事件:表單事件,a標籤跳轉,右鍵選單等。
取消預設事件的方法
- return false;以物件屬性的方式註冊的事件才生效
- event.preventDefault();W3C標準方法,IE9以下不相容
- event.returnValue = false;相容IE(chrome也實現了)
例如:取消右鍵預設事件
document.oncontextmenu = function(event){ console.log('a'); //方法一: return false; //方法二: event.preventDefault(); (W3C標準) //方法三: event.returnValue = false;(相容IE) }
廢話不多說,相容方法走一波:
//封裝阻止預設事件的函式cancelHandler(event); function cancelHandler(event){ if(event.preventDefault){ event.preventDefault(); }else{ event.returnValue = false; } }
關於取消a標籤預設事件,引出的一系列思考?
在日常開發中,會經常將a標籤作為按鈕使用,我們知道a標籤在沒有設定連線的情況下,會預設有點選事件。當我們點選一個沒有設定連線的a標籤時會重新整理頁面,或者錨鏈接沒有設定具體位置(連線內容只寫了一個“#”)會跳轉到頁面最頂端。這時候我們就需要取消預設事件。
//沒有設定連線的a標籤,重新整理頁面 <a href="">dom<a> //沒有具體錨定位置的a標籤,回到頁面頂端 <a href="#">dom</a>
- 取消a標籤的預設事件
var aDom = document.getElementsByTagName('a')[0]; aDom.onclick = function(e){ console.log(this); cancelHandler(e);//使用了前面的相容方法 }
上面的示例演示了將a標籤的點選事件修改成了自己的事件程式,並且阻止了a標籤的預設事件觸發。但是這是採用onclick的物件屬性賦值的寫法,如果有多個事件程式呢?是否每一次繫結事件程式都需要操作阻止指定呢?請看以下程式碼:
//addEvent()是前面封裝的繫結事件的相容方法 addEvent(aDom,'click',aDomFn1); addEvent(aDom,'click',aDomFn); function aDomFn(e){ console.log(this); cancelHandler(e); } function aDomFn1(e){ console.log("我就不取消預設事件怎麼滴"); }
我特別將阻止預設事件的事件程式放到了後面繫結,但是當我點選a標籤時,第一個事件程式執行完以後接著執行的是第二個事件程式,所以暫時得到的答案是,只要我們在物件的事件中任意事件程式設定一個阻止預設事件觸發,就可以完全阻止這個物件的指定事件的預設事件的觸發。(對於這個問題的底層實現還沒做深入的研究,只能說是暫時的結論)
九、事件物件
- 獲取事件物件
- 獲取事件源物件
- 事件委託
9.1.當瀏覽器引擎執行事件程式時,瀏覽器引擎會自動的向這個程式傳入一個預設引數,這個引數就是事件物件,這個物件的屬性記錄了事件的各種屬性,其中重要的有滑鼠座標點,事件模型狀態、取消預設事件的屬性和方法等,最重要的是還有一個事件源物件。(但是在IE9之前的瀏覽器這個事件物件不是作為引數傳入的,而是被儲存在window.event上)。所以相容程式碼要來了:
function fnEvent(e){ var event = e || window.event; //這裡是事件程式的具體程式碼 }
9.2.在事件冒泡和事件捕獲中,我們知道了有些事件是被冒泡或者捕獲執行的,在很多情況下我們需要在父級或者祖輩上執行事件程式,但又需要獲取觸發事件的DOM物件相關的資料,所以前面說事件物件上的事件源物件很重要。
在事件物件上有target這個屬性儲存了觸發事件的源物件DOM,IE(IE9以下)上的事件源物件屬性是srcElement。在chrome上這兩個屬性都有。所以,封裝相容寫法又得來一波:
//封裝相容性方法獲取事件源物件 function getTarget(e){ var event = e || window.event; var target = event.target || event.srcElement; return target; }
9.3.初級前端必會的操作:事件委託
- 什麼是事件委託?
- 事件委託怎麼實現?
假設我們有列表包含10個“li”,我們需要在點選每個“li”的的時候列印其文字內容,以我們通常的想法就是迴圈每個“li”新增click事件,且不說效能問題。如果li是動態新增呢?所以事件冒泡+事件源物件就可以展現它們的強大了。先看程式碼,直接回答第二個問題,第一個問題你自己機會有答案了。
//html <ul> <li>1</li> <li>2</li> ...//這裡省略100個li </ul> //js var ul = document.getElementsByTagName('ul')[0]; ul.onclick = function (e) { var event = e || window.event;//獲取事件物件 var target = event.target || event.srcElement;//獲取事件源 console.log(target.innerText);//通過事件源獲取到點選的li的文字 }