最詳細的JavaScript和事件解讀

與瀏覽器進行交互的時候瀏覽器就會觸發各類事件。好比當咱們打開某一個網頁的時候,瀏覽器加載完成了這個網頁,就會觸發一個 load 事件;當咱們點擊頁面中的某一個「地方」,瀏覽器就會在那個「地方」觸發一個 click 事件。css

這樣,咱們就能夠編寫 JavaScript,經過監聽某一個事件,來實現某些功能擴展。例如監聽 load 事件,顯示歡迎信息,那麼當瀏覽器加載完一個網頁以後,就會顯示歡迎信息。html

下面就來介紹一下事件。node

基礎事件操做

監聽事件

瀏覽器會根據某些操做觸發對應事件,若是咱們須要針對某種事件進行處理,則須要監聽這個事件。監聽事件的方法主要有如下幾種:jquery

HTML 內聯屬性(避免使用)

HTML 元素裏面直接填寫事件有關屬性,屬性值爲 JavaScript 代碼,便可在觸發該事件的時候,執行屬性值的內容。git

例如:github

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

onclick 屬性表示觸發 click,屬性值的內容(JavaScript 代碼)會在單擊該 HTML 節點時執行。api

顯而易見,使用這種方法,JavaScript 代碼與 HTML 代碼耦合在了一塊兒,不便於維護和開發。因此除非在必須使用的狀況(例如統計連接點擊數據)下,儘可能避免使用這種方法。瀏覽器

DOM 屬性綁定

也能夠直接設置 DOM 屬性來指定某個事件對應的處理函數,這個方法比較簡單:app

element.onclick = function(event){
    alert('你點擊了這個按鈕');
};

上面代碼就是監聽 element 節點的 click 事件。它比較簡單易懂,並且有較好的兼容性。可是也有缺陷,由於直接賦值給對應屬性,若是你在後面代碼中再次爲 element 綁定一個回調函數,會覆蓋掉以前回調函數的內容。dom

雖然也能夠用一些方法實現多個綁定,但仍是推薦下面的標準事件監聽函數。

使用事件監聽函數

標準的事件監聽函數以下:

element.addEventListener(<event-name>, <callback>, <use-capture>);

表示在 element 這個對象上面添加一個事件監聽器,當監聽到有 <event-name> 事件發生的時候,調用 <callback> 這個回調函數。至於 <use-capture> 這個參數,表示該事件監聽是在「捕獲」階段中監聽(設置爲 true)仍是在「冒泡」階段中監聽(設置爲 false)。關於捕獲和冒泡,咱們會在下面講解。

用標準事件監聽函數改寫上面的例子:

var btn = document.getElementsByTagName('button');
btn[0].addEventListener('click', function() {
    alert('你點擊了這個按鈕');
}, false);

這裏最好是爲 HTML 結構定義個 id 或者 class 屬性,方便選擇,在這裏只做爲演示使用。

Demo:

移除事件監聽

當咱們爲某個元素綁定了一個事件,每次觸發這個事件的時候,都會執行事件綁定的回調函數。若是咱們想解除綁定,須要使用 removeEventListener 方法:

element.removeEventListener(<event-name>, <callback>, <use-capture>);

須要注意的是,綁定事件時的回調函數不能是匿名函數,必須是一個聲明的函數,由於解除事件綁定時須要傳遞這個回調函數的引用,才能夠斷開綁定。例如:

var fun = function() {
    // function logic
};

element.addEventListener('click', fun, false);
element.removeEventListener('click', fun, false);

Demo:

事件觸發過程

在上面大致瞭解了事件是什麼、如何監聽並執行某些操做,但咱們對事件觸發整個過程還不夠了解。

下圖就是事件的觸發過程,借用了 W3C 的圖片

捕獲階段(Capture Phase)

當咱們在 DOM 樹的某個節點發生了一些操做(例如單擊、鼠標移動上去),就會有一個事件發射過去。這個事件從 Window 發出,不斷通過下級節點直到目標節點。在到達目標節點以前的過程,就是捕獲階段(Capture Phase)。

全部通過的節點,都會觸發這個事件。捕獲階段的任務就是創建這個事件傳遞路線,以便後面冒泡階段順着這條路線返回 Window。

監聽某個在捕獲階段觸發的事件,須要在事件監聽函數傳遞第三個參數 true。

element.addEventListener(<event-name>, <callback>, true);

但通常使用時咱們每每傳遞 false,會在後面說明緣由。

目標階段(Target Phase)

當事件跑啊跑,跑到了事件觸發目標節點那裏,最終在目標節點上觸發這個事件,就是目標階段。

須要注意的時,事件觸發的目標老是最底層的節點。好比你點擊一段文字,你覺得你的事件目標節點在 div 上,但實際上觸發在 <p>、<span> 等子節點上。例如:

在 Demo 中,我監聽單擊事件,將目標節點的 tag name 彈出。當你點擊加粗字體時,事件的目標節點就爲最底層的 <strong> 節點。

冒泡階段(Bubbling Phase)

當事件達到目標節點以後,就會沿着原路返回,因爲這個過程相似水泡從底部浮到頂部,因此稱做冒泡階段。

在實際使用中,你並不須要把事件監聽函數準確綁定到最底層的節點也能夠正常工做。好比在上例,你想爲這個 <div> 綁定單擊時的回調函數,你無須爲這個 <div> 下面的全部子節點所有綁定單擊事件,只須要爲 <div> 這一個節點綁定便可。由於發生它子節點的單擊事件,都會冒泡上去,發生在 <div> 上面。

針對這三個階段,wilsonpage 作了一個很是棒的 Demo,能夠看下:

爲何不用第三個參數 true

介紹完上面三個事件觸發階段,咱們來看下這個問題。

全部介紹事件的文章都會說,在使用 addEventListener 函數來監聽事件時,第三個參數設置爲 false,這樣監聽事件時只會監聽冒泡階段發生的事件。

這是由於 IE 瀏覽器不支持在捕獲階段監聽事件,爲了統一而設置的,畢竟 IE 瀏覽器的份額是不可忽略的。

IE 瀏覽器在事件這方面與標準還有一些其餘的差別,咱們會在後面集中介紹。

使用事件代理(Event Delegate)提高性能

由於事件有冒泡機制,全部子節點的事件都會順着父級節點跑回去,因此咱們能夠經過監聽父級節點來實現監聽子節點的功能,這就是事件代理。

使用事件代理主要有兩個優點:

  1. 減小事件綁定,提高性能。以前你須要綁定一堆子節點,而如今你只須要綁定一個父節點便可。減小了綁定事件監聽函數的數量。
  2. 動態變化的 DOM 結構,仍然能夠監聽。當一個 DOM 動態建立以後,不會帶有任何事件監聽,除非你從新執行事件監聽函數,而使用事件監聽無須擔心這個問題。

看一個例子:

上面例子中,爲了簡便,我使用 jQuery 來實現普通事件綁定和事件代理。個人目標是監聽全部 a 連接的單擊事件,.ul1 是常規的事件綁定方法,jQuery 會循環每個 .ul > a 結構並綁定事件監聽函數。.ul2 則是事件監聽的方法,jQuery 只爲 .ul2 結構綁定事件監聽函數,由於 .ul2 下面可能會有不少無關節點也會觸發 click 事件,因此我在 on 函數裏傳遞了第二個參數,表示只監聽 a 子節點的事件。

它們均可以正常工做,可是當我動態建立新 DOM 結構的時候,第一個 ul 問題就出現了,新建立結構雖然仍是 .ul1 > a,可是沒有綁定事件,因此沒法執行回調函數。而第二個 ul 工做的很好,由於點擊新建立的 DOM ,它的事件會冒泡到父級節點進行處理。

若是使用原生的方式實現事件代理,須要注意過濾非目標節點,能夠經過 id、class 或者 tagname 等等,例如:

element.addEventListener('click', function(event) {
    // 判斷是不是 a 節點
    if ( event.target.tagName == 'A' ) {
        // a 的一些交互操做
    }
}, false);

中止事件冒泡(stopPropagation)

全部的事情都會有對立面,事件的冒泡階段雖然看起來很好,也會有不適合的場所。比較複雜的應用,因爲事件監聽比較複雜,可能會但願只監聽發生在具體節點的事件。這個時候就須要中止事件冒泡。

中止事件冒泡須要使用事件對象的 stopPropagation 方法,具體代碼以下:

element.addEventListener('click', function(event) {
    event.stopPropagation();
}, false);

在事件監聽的回調函數裏,會傳遞一個參數,這就是 Event 對象,在這個對象上調用 stopPropagation 方法便可中止事件冒泡。舉箇中止事件冒泡的應用實例:

JS Bin

在上面例子中,有一個彈出層,咱們能夠在彈出層上作任何操做,例如 click 等。當咱們想關掉這個彈出層,在彈出層外面的任意結構中點擊便可關掉。它首先對 document 節點進行 click 事件監聽,全部的 click 事件,都會讓彈出層隱藏掉。一樣的,咱們在彈出層上面的單擊操做也會致使彈出層隱藏。以後咱們對彈出層使用中止事件冒泡,掐斷了單擊事件返回 document 的冒泡路線,這樣在彈出層的操做就不會被 document 的事件處理函數監聽到。

更多關於 Event 對象的事情,咱們會在下面介紹。

事件的 Event 對象

當一個事件被觸發的時候,會建立一個事件對象(Event Object),這個對象裏面包含了一些有用的屬性或者方法。事件對象會做爲第一個參數,傳遞給咱們的毀掉函數。咱們可使用下面代碼,在瀏覽器中打印出這個事件對象:

<button>打印 Event Object</button>

<script>
    var btn = document.getElementsByTagName('button');
    btn[0].addEventListener('click', function(event) {
        console.log(event);
    }, false);
</script>

就能夠看到一堆屬性列表:

事件屬性列表

事件對象包括不少有用的信息,好比事件觸發時,鼠標在屏幕上的座標、被觸發的 DOM 詳細信息、以及上圖最下面繼承過來的中止冒泡方法(stopPropagation)。下面介紹一下比較經常使用的幾個屬性和方法:

type(string)

事件的名稱,好比 「click」。

target(node)

事件要觸發的目標節點。

bubbles (boolean)

代表該事件是不是在冒泡階段觸發的。

preventDefault (function)

這個方法能夠禁止一切默認的行爲,例如點擊 a 標籤時,會打開一個新頁面,若是爲 a 標籤監聽事件 click 同時調用該方法,則不會打開新頁面。

stopPropagation (function)

中止冒泡,上面有提到,再也不贅述。

stopImmediatePropagation (function)

與 stopPropagation 相似,就是阻止觸發其餘監聽函數。可是與 stopPropagation 不一樣的是,它更加 「強力」,阻止除了目標以外的事件觸發,甚至阻止針對同一個目標節點的相同事件,Demo:http://jsfiddle.net/yujiangshui/ju2ujmzp/2/。

cancelable (boolean)

這個屬性代表該事件是否能夠經過調用 event.preventDefault 方法來禁用默認行爲。

eventPhase (number)

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

pageX 和 pageY (number)

這兩個屬性表示觸發事件時,鼠標相對於頁面的座標。Demo:http://api.jquery.com/event.pagex/。

isTrusted (boolean)

代表該事件是瀏覽器觸發(用戶真實操做觸發),仍是 JavaScript 代碼觸發的。

jQuery 中的事件

若是你在寫文章或者 Demo,爲了簡單,你固然能夠用上面的事件監聽函數,以及那些事件對象提供的方法等。但在實際中,有一些方法和屬性是有兼容性問題的,因此咱們會使用 jQuery 來消除兼容性問題。

下面簡單的來講一下 jQuery 中事件的基礎操做。

綁定事件和事件代理

在 jQuery 中,提供了諸如 click() 這樣的語法糖來綁定對應事件,可是這裏推薦統一使用 on() 來綁定事件。語法:

.on( events [, selector ] [, data ], handler )

events 即爲事件的名稱,你能夠傳遞第二個參數來實現事件代理,具體文檔.on() 這裏再也不贅述。

處理過兼容性的事件對象(Event Object)

事件對象有些方法等也有兼容性差別,jQuery 將其封裝處理,並提供跟標準一直的命名。

若是你想在 jQuery 事件回調函數中訪問原來的事件對象,須要使用 event.originalEvent,它指向原生的事件對象。

觸發事件 trigger 方法

點擊某個綁定了 click 事件的節點,天然會觸發該節點的 click 事件,從而執行對應回調函數。

trigger 方法能夠模擬觸發事件,咱們單擊另外一個節點 elementB,可使用:

$(elementB).on('click', function(){
    $(elementA).trigger( "click" );
});

來觸發 elementA 節點的單擊監聽回調函數。詳情請看文檔 .trigger()

事件進階話題

IE 瀏覽器的差別和兼容性問題

IE 瀏覽器就是特立獨行,它對於事件的操做與標準有一些差別。不過 IE 瀏覽器如今也開始慢慢努力改造,讓瀏覽器變得更加標準。

IE 下綁定事件

在 IE 下面綁定一個事件監聽,在 IE9- 沒法使用標準的 addEventListener 函數,而是使用自家的 attachEvent,具體用法:

element.attachEvent(<event-name>, <callback>);

其中 <event-name> 參數須要注意,它須要爲事件名稱添加 on 前綴,好比有個事件叫 click,標準事件監聽函數監聽 click,IE 這裏須要監聽 onclick。

另外一個,它沒有第三個參數,也就是說它只支持監聽在冒泡階段觸發的事件,因此爲了統一,在使用標準事件監聽函數的時候,第三參數傳遞 false。

固然,這個方法在 IE9 已經被拋棄,在 IE11 已經被移除了,IE 也在慢慢變好。

IE 中 Event 對象須要注意的地方

IE 中往回調函數中傳遞的事件對象與標準也有一些差別,你須要使用 window.event 來獲取事件對象。因此你一般會寫出下面代碼來獲取事件對象:

event = event || window.event

此外還有一些事件屬性有差異,好比比較經常使用的 event.target 屬性,IE 中沒有,而是使用 event.srcElement 來代替。若是你的回調函數須要處理觸發事件的節點,那麼須要寫:

node = event.srcElement || event.target;

常見的就是這點,更細節的再也不多說。在概念學習中,咱們不必爲不標準的東西支付學習成本;在實際應用中,類庫已經幫咱們封裝好這些兼容性問題。可喜的是 IE 瀏覽器如今也開始不斷向標準進步。

事件回調函數的做用域問題

與事件綁定在一塊兒的回調函數做用域會有問題,咱們來看個例子:

Events in JavaScript: Removing event listeners

回調函數調用的 user.greeting 函數做用域應該是在 user 下的,本指望輸出 My name is Bob 結果卻輸出了 My name is undefined。這是由於事件綁定函數時,該函數會以當前元素爲做用域執行。爲了證實這一點,咱們能夠爲當前 element 添加屬性:

element.firstname = 'jiangshui';

再次點擊,能夠正確彈出 My name is jiangshui。那麼咱們來解決一下這個問題。

使用匿名函數

咱們爲回調函數包裹一層匿名函數。

Events in JavaScript: Removing event listeners

包裹以後,雖然匿名函數的做用域被指向事件觸發元素,但執行的內容就像直接調用同樣,不會影響其做用域。

使用 bind 方法

使用匿名函數是有缺陷的,每次調用都包裹進匿名函數裏面,增長了冗餘代碼等,此外若是想使用 removeEventListener 解除綁定,還須要再建立一個函數引用。Function 類型提供了 bind 方法,能夠爲函數綁定做用域,不管函數在哪裏調用,都不會改變它的做用域。經過以下語句綁定做用域:

user.greeting = user.greeting.bind(user);

這樣咱們就能夠直接使用:

element.addEventListener('click', user.greeting);

經常使用事件和技巧

用戶的操做有不少種,因此有不少事件。爲了開發方便,瀏覽器又提供了一些事件,因此有不少不少的事件。這裏只介紹幾種經常使用的事件和使用技巧。

load

load 事件在資源加載完成時觸發。這個資源能夠是圖片、CSS 文件、JS 文件、視頻、document 和 window 等等。

比較經常使用的就是監聽 window 的 load 事件,當頁面內全部資源所有加載完成以後就會觸發。好比用 JS 對圖片以及其餘資源處理,咱們在 load 事件中觸發,能夠保證 JS 不會在資源未加載完成就開始處理資源致使報錯。

一樣的,也能夠監聽圖片等其餘資源加載狀況。

beforeunload

當瀏覽者在頁面上的輸入框輸入一些內容時,未保存、誤操做關掉網頁可能會致使輸入信息丟失。

當瀏覽者輸入信息但未保存時關掉網頁,咱們就能夠開始監聽這個事件,例如:

window.addEventListener("beforeunload", function( event ) {
    event.returnValue = "放棄當前未保存內容而關閉頁面?";
});

這時候試圖關閉網頁的時候,會彈窗阻止操做,點擊確認以後纔會關閉。固然,若是沒有必要,就不要監聽,不要覺得使用它能夠爲你留住瀏覽者。

resize

當節點尺寸發生變化時,觸發這個事件。一般用在 window 上,這樣能夠監聽瀏覽器窗口的變化。一般用在複雜佈局和響應式上。

常見的視差滾動效果網站以及同類比較複雜的佈局網站,每每使用 JavaScript 來計算尺寸、位置。若是用戶調整瀏覽器大小,尺寸、位置不隨着改變則會出現錯位狀況。在 window 上監聽該事件,觸發時調用計算尺寸、位置的函數,能夠根據瀏覽器的大小來從新計算。

但須要注意一點,當瀏覽器發生任意變化都會觸發 resize 事件,哪怕是縮小 1px 的瀏覽器寬度,這樣調整瀏覽器時會觸發大量的 resize 事件,你的回調函數就會被大量的執行,致使變卡、崩潰等。

你可使用函數 throttle 或者 debounce 技巧來進行優化,throttle 方法大致思路就是在某一段時間內不管屢次調用,只執行一次函數,到達時間就執行;debounce 方法大致思路就是在某一段時間內等待是否還會重複調用,若是不會再調用,就執行函數,若是還有重複調用,則不執行繼續等待。關於它們更詳細的信息,我後面會介紹一下發表在個人博客上,這裏再也不贅述。

error

當咱們加載資源失敗或者加載成功可是隻加載一部分而沒法使用時,就會觸發 error 事件,咱們能夠經過監聽該事件來提示一個友好的報錯或者進行其餘處理。好比 JS 資源加載失敗,則提示嘗試刷新;圖片資源加載失敗,在圖片下面提示圖片加載失敗等。該事件不會冒泡。由於子節點加載失敗,並不意味着父節點加載失敗,因此你的處理函數必須精確綁定到目標節點。

須要注意的是,對於該事件,你可使用 addEventListener 等進行監聽,可是有時候會出現失效狀況(看這個例子),這是由於 error 事件都觸發過了,你的 JS 監聽處理代碼尚未加載進來執行。爲了不這種狀況,用內聯法更好一些:

<img src="not-found.jpg" onerror="doSomething" />

若是還有其餘經常使用事件,歡迎留言補充。

用 JavaScript 模擬觸發內置事件

內置的事件也能夠被 JavaScript 模擬觸發,好比下面函數模擬觸發單擊事件:

function simulateClick() {
  var event = new MouseEvent('click', {
    'view': window,
    'bubbles': true,
    'cancelable': true
  });
  var cb = document.getElementById('checkbox'); 
  var canceled = !cb.dispatchEvent(event);
  if (canceled) {
    // A handler called preventDefault.
    alert("canceled");
  } else {
    // None of the handlers called preventDefault.
    alert("not canceled");
  }
}

能夠看這個 Demo 來了解更多。

自定義事件

咱們能夠自定義事件來實現更靈活的開發,事件用好了能夠是一件很強大的工具,基於事件的開發有不少優點(後面介紹)。

與自定義事件的函數有 Event、CustomEvent 和 dispatchEvent。

直接自定義事件,使用 Event 構造函數:

var event = new Event('build');

// Listen for the event.
elem.addEventListener('build', function (e) { ... }, false);

// Dispatch the event.
elem.dispatchEvent(event);

CustomEvent 能夠建立一個更高度自定義事件,還能夠附帶一些數據,具體用法以下:

var myEvent = new CustomEvent(eventname, options);

其中 options 能夠是:

{
    detail: {
        ...
    },
    bubbles: true,
    cancelable: false
}

其中 detail 能夠存放一些初始化的信息,能夠在觸發的時候調用。其餘屬性就是定義該事件是否具備冒泡等等功能。

內置的事件會由瀏覽器根據某些操做進行觸發,自定義的事件就須要人工觸發。dispatchEvent 函數就是用來觸發某個事件:

element.dispatchEvent(customEvent);

上面代碼表示,在 element 上面觸發 customEvent 這個事件。結合起來用就是:

// add an appropriate event listener
obj.addEventListener("cat", function(e) { process(e.detail) });

// create and dispatch the event
var event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}});
obj.dispatchEvent(event);

使用自定義事件須要注意兼容性問題,而使用 jQuery 就簡單多了:

// 綁定自定義事件
$(element).on('myCustomEvent', function(){});

// 觸發事件
$(element).trigger('myCustomEvent');

此外,你還能夠在觸發自定義事件時傳遞更多參數信息:

$( "p" ).on( "myCustomEvent", function( event, myName ) {
  $( this ).text( myName + ", hi there!" );
});
$( "button" ).click(function () {
  $( "p" ).trigger( "myCustomEvent", [ "John" ] );
});

更詳細的用法請看 Introducing Custom Events,這裏再也不贅述。

在開發中應用事件

當咱們操做某一個 DOM,發出一個事件,咱們能夠在另外一個地方寫代碼捕獲這個事件執行處理邏輯。觸發操做和捕獲處理操做是分開的。咱們能夠根據這個特性來對程序解耦。

用事件解耦

咱們能夠將一個整個的功能,分割成獨立的小功能,每一個小功能綁定一個事件,由一個「控制器」負責根據條件觸發某個事件。這樣,在外面觸發這個事件,也能夠調用對應功能,使其更加靈活。

應用事件對程序解耦

在《基於 MVC 的 JavaScript Web 富應用開發》一書中,有更加具體的實例,有興趣的朋友能夠買本看看。

發佈(Publish)和訂閱(Subscribe)模式

針對上面這種用法,繼續抽象一下,就是發佈和訂閱開發模式。正如其名,這種模式有兩個角色:發佈者和訂閱者,此外有一條信道,發佈者被觸發往這個信道里面發信,訂閱者從這個信道里面收信,若是收到特定信件則執行某個對應的邏輯。這樣,發佈者和訂閱者之間是徹底解耦的,只有一條信道鏈接。這樣就很是容易擴展,也不會引入額外的依賴。

這樣若是須要添加新功能,只須要添加一個新的訂閱者(及其執行邏輯),監聽信道中某一類新的信件。再在應用中經過發佈者發送一類新的信件便可。

具體實現,這裏推薦 cowboy 開發的 Tiny Pub Sub,經過 jQuery 實現,很是簡潔直觀,jQuery 太贊。代碼就這幾行:

(function($) {

  var o = $({});

  $.subscribe = function() {
    o.on.apply(o, arguments);
  };

  $.unsubscribe = function() {
    o.off.apply(o, arguments);
  };

  $.publish = function() {
    o.trigger.apply(o, arguments);
  };

}(jQuery));

定義一個對象做爲信道,而後提供了三個方法,訂閱者、取消訂閱、發佈者。

總結和擴展閱讀

事件有關的基礎知識基本就這些,更多的還有待你繼續挖掘。本文資料參考和推薦擴展閱讀以下(感謝他們):

相關文章
相關標籤/搜索