1. 程式人生 > >關於面試中的原生js實現事件代理和事件模型和事件廣播的學習

關於面試中的原生js實現事件代理和事件模型和事件廣播的學習

這也是看了網上一篇面試題所以想整理下,順便對自己學習過的東西重新理解鞏固下

在看事件代理之前,我們先來重溫下事件,在與瀏覽器進行互動的時候瀏覽器會觸發各種事件,比如當我們開啟某一個網頁的時候,瀏覽器載入完成了這個網頁,就會觸發一個load事件,當我們點選頁面中的某一個地方,瀏覽器就會在那個地方觸發一個click事件,這樣我們可以編寫js,通過監聽某一事件,來實現某些功能擴充套件,例如監聽load事件,顯示歡迎資訊,那麼當瀏覽器載入完一個網頁之後,就會顯示歡迎資訊。

現在我們再來說下js事件的三階段,分別為捕獲,目標,冒泡,什麼意思呢?先舉個例子(雖然盜圖可恥,但是這張圖真的簡單明瞭)

比如說我現在點選一個button


問題來了,我明明只是點選了button,那其餘的這些是什麼?這就是我們前面提到的三個階段:

捕獲階段:從最上層元素,知道最下層,你點選的那個target元素,路過的所有節點都可以捕捉到這事件

         目標階段:如果給事件成功的到達了target元素,它會進行事件處理

        冒泡階段:事件從最下層向上傳遞,依次出發父元素的該事件處理函式

而這裡為什麼先是父節點捕獲瞭然後往下然後又回到父節點呢?

這就源於之前的瀏覽器大戰,我們都知道現在的瀏覽器有幾大巨頭,而IE在曾經是絕對的老大,於是IE覺得事件觸發就應該是從下層向上傳遞,就類似冒泡那種,

但是另一種瀏覽器不這麼認為,它覺得事件捕獲應該是父節點先觸發,最後才是目標節點觸發,當打的不可開交的時候,DOM2站出來了。為了世界的和平,

於是DOM2級事件規定事件流有三個階段:分別是捕獲,目標,冒泡(注:IE8以及更早版本不支援DOM事件流)。就是如上圖所示,走一個來回。然後根據我們需求,規定我們的事件監聽是在捕獲階段還是冒泡階段。

事件監聽

上面提到事件監聽,那我們再來說下事件監聽的幾種方法吧(會的童鞋可以自動略過這一節。。。)

1.在html中直接寫

<button onclick="alert('你點選了這個按鈕');">點選這個按鈕</button>

這種很明顯缺點是程式碼耦合,不便於維護和開發

2.DOM繫結

var element=document.getElementById('jianting');
element.onclick = function(event){
    alert('你點選了這個按鈕');
};
這種比較常見,比較簡單易懂,而且相容性較好,但是也有缺陷,只能實現一個繫結,也就是說我們再為element繫結第二個click事件時候,會覆蓋掉之前的click事件

3.使用事件監聽函式

標準的事件監聽函式如下:

element.addEventListener(<event-name>, <callback>, <use-capture>);
表示在element這個物件上面新增一個事件監聽器,當監聽到有<event-name>(如click)事件發生時候,呼叫callback這個回撥函式,至於<use-capture>這個引數, 表示事件監聽是在‘捕獲’階段監聽(設定為true)還是在冒泡階段中監聽(設定為false)。
但是。。。。這個有一種瀏覽器不支援這個寫法。。。。你應該猜到了吧,這就是某些IE,那我們應該怎麼寫呢
element.attachEvent("on" + type, handler)
這個你可能奇怪,怎麼只有兩個引數?對,因為IE只支援冒泡階段嘛,所以沒有第三個引數
所以針對不同的瀏覽器,為了讓我們的程式碼有相容性,所以我們可以這樣寫
              if (element.addEventListener) {
                      element.addEventListener(type, handler, false);
              } else if (element.attachEvent) {
                    element.attachEvent("on" + type, handler);
           } else {
                     element["on" + type] = handler;
              }
其實IE與其他瀏覽器對事件的解讀不止是新增監聽方法不同,還有其他的比如事件的event物件也不同
當一個事件被觸發時候,會建立一個事件物件(event object),這個物件裡面包含了一些屬性和方法,事件物件會作為第一個引數傳遞給我們的回撥函式,下面我們列印下這個屬性來看下,下面這段程式碼在chrome下做的測試
<<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Event Bubbling</title>
  <script>
window.onload=function(){

    var btn = document.getElementById('clickMe');
    btn.addEventListener('click', function(event) {
        console.log(event);
    }, false);

};
  </script>
</head>
<body>
  <button id="clickMe">Click Me</button>
</body>
</html>

將看到如下屬性:

介紹下常見屬性(從別處拷貝而來的)

type(string):事件名稱

target(node):事件要觸發的目標節點。

bubbles (boolean):表明該事件是否是在冒泡階段觸發的。

preventDefault (function):這個方法可以禁止一切預設的行為,例如點選 a 標籤時,會開啟一個新頁面,如果為 a 標籤監聽事件 click 同時呼叫該方法,則不會開啟新頁面。

stopPropagation (function):停止冒泡,上面有提到,不再贅述。

stopImmediatePropagation (function):與 stopPropagation 類似,就是阻止觸發其他監聽函式。但是與 stopPropagation 不同的是,它更加 “強力”,阻止除了目標之外的事件觸發,甚至阻止針對同一個目標節點的相同事件

cancelable (boolean):這個屬性表明該事件是否可以通過呼叫 event.preventDefault 方法來禁用預設行為。

eventPhase (number):這個屬性的數字表示當前事件觸發在什麼階段。none:0;捕獲:1;目標:2;冒泡:3。

pageX 和 pageY (number):這兩個屬性表示觸發事件時,滑鼠相對於頁面的座標。

isTrusted (boolean):表明該事件是瀏覽器觸發(使用者真實操作觸發),還是 JavaScript 程式碼觸發的。

別忘了IE的特立獨行啊,IE的某些版本對事件屬性的解讀的差異如下:只是列舉了下基本的

IE往回調函式中傳遞的事件物件和標準有差異,你需要使用window.event來獲取事件物件,所以為了程式碼的相容性你得這麼寫:

event=event||window.event

還有個幾個屬性
element=event.srcElement||event.target

事件的傳播的阻止

w3c中,使用stopPropagation()方法,IE設定為cancelBubble=true;

阻止事件的預設行為:

w3c使用preventDefault()方法

IE設定window.event.returnValue=false;

哎呀 。。。寫著寫著寫偏啦

迴歸正題

事件代理也叫做事件委託

現在有個需求,就是有個li的列表,現在我要對每個li新增以一個click監聽函式,彈出所點選的li的id,我們可能一下子就想到了在每個li上寫個addEventListener,如果你有100個呢?你就要遍歷100遍li,這個工作量其實有點大,有沒有別的辦法呢,前面已經說了target就是你點選的那個物件,但是事件是可以冒泡的啊,也就是說不管你點選哪個li,這個事件都會一層層的往上觸發,知道最上面的一層,但是有個target的屬性是不會變的,講到了這裡,你是不是有自己的小想法了呢

所謂的事件委託或者事件代理技術能讓你避免對特定的每個節點新增事件監聽器,相反事件監聽器是被新增到它們的元素上的,利用冒泡原理,把事件加到父級上,觸發執行效果

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Event Bubbling</title>
    <script>
window.onload=function(){

   function getEventTarget(e) {

  e = e || window.event;

  return e.target || e.srcElement;

}
var parentlist=document.getElementById("parent-list");
// 獲取父節點,併為它新增一個click事件
if(document.addEventListener){
parentlist.addEventListener("click",function(e) {
  // 檢查事件源e.targe是否為Li
    var target = getEventTarget(e);
  alert(target.id);

});
}else if(document.attachEvent){
parentlist.attachEvent("onclick",function(e) {
  // 檢查事件源e.targe是否為Li
    var target = getEventTarget(e);
  alert(target.id);

});
}else{
parentlist.onclick=function(){
  // 檢查事件源e.targe是否為Li
    var target = getEventTarget(e);
  alert(target.id);
}
}
};
  </script>
</head>
<body>
<ul id="parent-list">
<li id="post-1">Item 1</li>
<li id="post-2">Item 2</li>
<li	id="post-3">Item 3</li>
<li	id="post-4">Item 4</li>
<li	id="post-5">Item 5</li>
<li id="post-6">Item 6</li>
</ul>
</body>
</html>
普通寫法:
var tags=document.getElementsBytag('li');
for(var i=0;i<tags.length,i++){
var li=tags[i];


li.onclick=function(e){
	
e.stopPropagation();
alert(this.id);
 };
}
事件代理的優缺點:

通過上面的這個例子,我們可以總結出事件代理的優點:

1.可以節省大量記憶體佔用,減少事件註冊

2.可以方便的動態新增和修改元素,不需要因為元素改動而修改事件繫結

3.js和DOM節點之間的關聯少了,這樣也就減少了因迴圈引用而帶來的記憶體洩漏發生的概率

缺點:

很多事件是不能冒泡的,比如說focus、blur、load本身就沒有冒泡的特性,自然就不能用事件委託了。
事件模型:

事件模型有好幾種,什麼DOM事件模型,IE事件模型,簡單點說就是說當對應事件被觸發時候,監聽該事件的所有監聽函式都會被呼叫,如果面試官讓我們手寫一個事件模型的話其實就是想我們考慮到各種瀏覽器的相容性,

function Emitter() {
    this._listener = [];//_listener[自定義的事件名] = [所用執行的匿名函式1, 所用執行的匿名函式2]
}
 
//註冊事件
Emitter.prototype.bind = function(eventName, callback) {
    var listener = this._listener[eventName] || [];//this._listener[eventName]沒有值則將listener定義為[](陣列)。
    listener.push(callback);
    this._listener[eventName] = listener;
}
 
 //觸發事件
Emitter.prototype.trigger = function(eventName) {
    var args = Array.prototype.slice.apply(arguments).slice(1);//atgs為獲得除了eventName後面的引數(註冊事件的引數)
    var listener = this._listener[eventName];
 
    if(!Array.isArray(listener)) return;//自定義事件名不存在
    listener.forEach(function(callback) {
        try {
            callback.apply(this, args);
        }catch(e) {
            console.error(e);
        }
    })
}
//例項
var emitter = new Emitter();
    emitter.bind("myevent", function(arg1, arg2) {
        console.log(arg1, arg2);
    });
 
    emitter.bind("myevent", function(arg1, arg2) {
        console.log(arg2, arg1);
    });
 
    emitter.trigger('myevent', "a", "b");

事件派發/事件廣播(dispatchEvent)