JS事件那些事兒 一次整明白

DOM 事件流

事件流包括三個階段。簡而言之:事件一開始從文檔的根節點流向目標對象(捕獲階段),而後在目標對象上被觸發(目標階段),以後再回溯到文檔的根節點(冒泡階段)。javascript

DOM 事件流

事件捕獲階段(Capture Phase)

事件的第一個階段是捕獲階段。事件從文檔的根節點出發,隨着 DOM 樹的結構向事件的目標節點流去。途中通過各個層次的 DOM 節點,並在各節點上觸發捕獲事件,直到到達事件的目標節點。捕獲階段的主要任務是創建傳播路徑,在冒泡階段,事件會經過這個路徑回溯到文檔跟節點。css

目標階段(Target Phase)

當事件到達目標節點的,事件就進入了目標階段。事件在目標節點上被觸發,而後會逆向迴流,直到傳播至最外層的文檔節點。html

冒泡階段(Bubble Phase)

事件在目標元素上觸發後,並不在這個元素上終止。它會隨着 DOM 樹一層層向上冒泡,直到到達最外層的根節點。也就是說,同一個事件會依次在目標節點的父節點,父節點的父節點...直到最外層的節點上被觸發。java

冒泡過程很是有用。它將咱們從對特定元素的事件監聽中釋放出來,相反,咱們能夠監聽 DOM 樹上更上層的元素,等待事件冒泡的到達。若是沒有事件冒泡,在某些狀況下,咱們須要監聽不少不一樣的元素來確保捕獲到想要的事件。node

全部的事件都要通過捕捉階段和目標階段,可是有些事件會跳過冒泡階段。例如,讓元素得到輸入焦點的 focus 事件以及失去輸入焦點的 blur 事件就都不會冒泡。git

事件處理程序

HTML 事件處理程序

<!-- 輸出 click -->
<input type="button" value="Click Me" onclick="console.log(event.type)">

<!-- 輸出 Click Me this 值等於事件的目標元素 -->
<input type="button" value="Click Me" onclick="console.log(this.value)">
複製代碼

固然在 HTML 中定義的事件處理程序也能夠調用其它地方定義的腳本:web

<!-- Chrome 輸出 click -->
<script> function showMessage(event) { console.log(event.type); } </script>
<input type="button" value="Click Me" onclick="showMessage(event)">
複製代碼

經過 HTML 指定的事件處理程序都須要HTML的參與,即結構和行爲相耦合,不易維護。chrome

DOM0 級事件處理程序

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); btn.onclick=function(){ console.log(this.id); // 輸出 btn } </script>
複製代碼

這裏是將一個函數賦值給一個事件處理程序的屬性,以這種方式添加的事件處理程序會在事件流的冒泡階段被處理。要刪除事件將 btn.onclick 設置爲 null 便可。segmentfault

DOM2 級事件處理程序

DOM2 級事件定義了addEventListener()removeEventListener()兩個方法,用於處理和刪除事件處理程序的操做。瀏覽器

全部 DOM 節點都包含這兩個方法,它們接受3個參數:要處理的事件名做爲事件處理程序的函數一個布爾值。最後的布爾值參數是 true 表示在捕獲階段調用事件處理程序,若是是 false(默認) 表示在冒泡階段調用事件處理程序。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); btn.addEventListener("click",function(){ console.log(this.id); },false); btn.addEventListener("click",function(){ console.log('Hello word!'); },false); </script>
複製代碼

上面代碼兩個事件處理程序會按照它們的添加順序觸發,先輸出 btn 再輸出 Hello word!

經過 addEventListener()添加的事件處理程序只能使用 removeEventListener()來移除,移除時傳入的參數與添加時使用的參數相同,即匿名函數沒法被移除。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); var handler = function(){ console.log(this.id); } btn.addEventListener("click", handler, false); btn.removeEventListener("click",handler, false); </script>
複製代碼

IE 事件處理程序

IE一般都是特立獨行的,它添加和刪除事件處理程序的方法分別是:attachEvent()detachEvent()

一樣接受事件處理程序名稱與事件處理程序函數兩個參數,但跟addEventListener()的區別是:

  • 事件名稱須要加「on」,好比「onclick」;
  • 沒了第三個布爾值,IE8及更早版本只支持事件冒泡;
  • 仍可添加多個處理程序,但觸發順序是反着來的。

還有一點須要注意,DOM0 和 DOM2 級的方法,其做用域都是在其所依附的元素當中,attachEvent()則是全局,即若是像以前同樣使用this.id,訪問到的就不是 button 元素,而是 window,就得不到正確的結果。

跨瀏覽器事件處理程序

直接上碼:

var EventUtil={
    addHandler:function(element,type,handler){
        if(element.addEventListener){
            element.addEventListener(type,handler,false);
        } else if(element.attachEvent){
            element.attachEvent(「on」+ type,handler);
        } else {
            element[「on」 + type]=handler;
        }
    },
    removeHandler:function(element,type,handler){
        if(element.removeEventListener){
            element.removeEventListener(type,handler,false);
        } else if(element.detachEvent){
            element.detachEvent(「on」+ type,handler);
        } else {
            element[「on」 + type]=null;
        }
    }
}
複製代碼

事件對象

在觸發 DOM 上的某個事件時,會產生一個事件對象 event,這個對象中包含着全部與事件有關的信息。全部的瀏覽器都支持 event 對象,但支持方式不一樣。

DOM 中的事件對象

兼容 DOM 的瀏覽器會將一個 event 對象傳入到事件處理程序中。event 對象包含與建立它的特定事件有關的屬性和方法。觸發的事件類型不同,可用的屬性和方法也不同。

不過,全部事件都會有下面列出的成員。

  • bubbles (boolean) — 代表事件是否冒泡

  • cancelable (boolean) — 這個變量指明這個事件的默認行爲是否能夠經過調用 event.preventDefault 來阻止。也就是說,只有 cancelable 爲 true 的時候,調用 event.preventDefault 才能生效。

  • currentTarget(element) — 當事件遍歷DOM時,標識事件的當前目標。它老是引用事件處理程序附加到的元素,而不是event.target,event.target標識事件發生的元素。

  • defaultPrevented (boolean) — 這個狀態變量代表當前事件對象的 preventDefault 方法是否被調用過

  • eventPhase (number) — 這個數字變量表示當前這個事件所處的階段(phase):none(0),capture(1),target(2),bubbling(3)。

  • preventDefault(function) — 這個方法將阻止瀏覽器中用戶代理對當前事件的相關默認行爲被觸發。好比阻止<a>元素的 click 事件加載一個新的頁面

  • stopImmediatePropagation (function) — 這個方法將阻止當前事件鏈上全部的回調函數被觸發,也包括當前節點上針對此事件已綁定的其餘回調函數。

  • stopPropagation (function) — 阻止捕獲和冒泡階段中當前事件的進一步傳播。

  • target (element) — 事件起源的 DOM 節點(獲取標籤名:ev.target.nodeName)

  • type (String) — 事件的名稱

  • isTrusted (boolean) — 若是一個事件是由設備自己(如瀏覽器)觸發的,而不是經過 JavaScript 模擬合成的,那個這個事件被稱爲可信任的(trusted)

  • timestamp (number) — 事件發生的時間

在事件處理程序內部,對象 this 始終等於 currentTarget 的值,而 target 則只包含事件的實際目標。若是直接將事件處理程序指定給了目標元素, 則 this、currentTarget、target 包含相同的值。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); btn.onclick = function (event) { console.log(event.currentTarget === this); // true console.log(event.target === this); // true } </script>
複製代碼

若是事件處理程序存在於按鈕的父節點,那麼這些值是不一樣的。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); document.body.onclick = function (event) { console.log(event.currentTarget === document.body); // true console.log(this === document.body); // true console.log(event.target === btn); // true } </script>
複製代碼

在須要經過一個函數處理多個事件時,可使用 type 屬性。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); var handler = function(event) { switch (event.type) { case "click": console.log("clicked"); break; case "mouseover": event.target.style.backgroundColor = "red"; break; case "mouseout": event.target.style.backgroundColor = ""; break; } } btn.onclick = handler; btn.onmouseover = handler; btn.onmouseout = handler; </script>
複製代碼

跨瀏覽器的事件對象

萬惡的 IE!直接上碼:

EventUtil = {
    addHandler: function(element,type,handler){
        // 省略代碼
    },
    removeHandler: function(element,type,handler){
        // 省略代碼
    },
    getEvent: function(event){
        return event?event:window.event;
    },
    getTarget: function(event){
        return event.target || event.srcElement;
    },
    preventDefault: function(event){
        if(event.preventDefault){
            event.preventDefault();
        }else{
            event.returnValue = false;
        }
    },
    stopProgagation: function(event){
        if(event.stopProgagation){
            event.stopProgagation();
        }else{
            event.cancelBubble = true;
        }
    }
};
複製代碼

更多詳情參見:跨瀏覽器的事件對象

阻止事件冒泡/中止傳播(Stopping Propagation)

能夠經過調用事件對象的 stopPropagation 方法,在任何階段(捕獲階段或者冒泡階段)中斷事件的傳播。此後,事件不會在後面傳播過程當中的通過的節點上調用任何的監聽函數。

<input type="button" value="Click Me" id="btn">
<script> var btn=document.getElementById("btn"); btn.onclick = function (event) { console.log("Clicked"); // 觸發 event.stopPropagation(); } document.body.onclick = function (event) { console.log("Body clicked"); // 傳播阻斷 不觸發 } </script>
複製代碼

調用 event.stopPropagation()不會阻止當前節點上此事件其餘的監聽函數被調用。若是你但願阻止當前節點上的其餘回調函數被調用的話,你可使用更激進的 event.stopImmediatePropagation()方法。

阻止瀏覽器默認行爲

當特定事件發生的時候,瀏覽器會有一些默認的行爲做爲反應。最多見的事件不過於 link 被點擊。當一個 click 事件在一個<a>元素上被觸發時,它會向上冒泡直到 DOM 結構的最外層 document,瀏覽器會解釋 href 屬性,而且在窗口中加載新地址的內容。

在 web 應用中,開發人員常常但願可以自行管理導航(navigation)信息,而不是經過刷新頁面。爲了實現這個目的,咱們須要阻止瀏覽器針對點擊事件的默認行爲,而使用咱們本身的處理方式。這時,咱們就須要調用 event.preventDefault().

咱們能夠阻止瀏覽器的不少其餘默認行爲。好比,咱們能夠在 HTML5 遊戲中阻止敲擊空格時的頁面滾動行爲,或者阻止文本選擇框的點擊行爲。

調用 event.stopPropagation()只會阻止傳播鏈中後續的回調函數被觸發。它不會阻止瀏覽器的自身的行爲。

事件類型

事件的種類可謂至關繁多,不一樣的事件類型具備不一樣的信息,經常使用的大體可分爲以下幾類:

  • UI:load、 error(錯誤觸發)、select、resize、scroll等
  • 焦點:blur、focus、change(當用戶提交對元素值的更改時觸發)等
  • 鼠標:click、dblclick、mousedown/up、mouseenter/leave、mousemove、mouseover等

一些注意點:一、除 mouseenter/leave 全部鼠標事件都會冒泡; 二、只有在同一個元素上相繼觸發 mousedown/up 事件,纔會觸發 click 事件,只有觸發兩次 click 事件纔會觸發一次 dblclick 事件,4個事件的觸發順序爲 mousedown → mouseup → click → mousedown → mouseup → click → dblclick (ie8及以前雙擊事件會跳過第二個 mousedown 和 click 事件);

  • 鍵盤:keydown/up、keypress
  • 觸摸:touchstart(即便已經有一個手指放在屏幕上也會觸發)、touchend、touchmove等

在觸摸屏幕上的元素時事件的發生順序爲:touchstart → mouseover → mousemove(一次) → mousedown → mouseup → click → touchend;

  • 手勢:gesturestart、gestureend、gesturechange等

一、gesturestart:當一個手指已經按在屏幕上,而另外一個手指又觸摸在屏幕時觸發。 二、gesturechange:當觸摸屏幕的任何一個手指的位置發生變化時觸發。三、gestureend:當任何一個手指從屏幕上面移開時觸發。注意:只有兩個手指都觸摸到事件的接收容器時才觸發這些手勢事件。

  • 設備:orientationchange(檢測設備屏幕旋轉)、deviceorientation(檢測設備方向變化)等

上面這些,大都是人爲操做,還有些事件是網頁狀態帶來的,好比:網頁加載完成、提交表單、網頁出錯等。

除此以外,還有變更事件,複合事件,HTML5新加入的一些事件,再也不一一列出。完整列表可在這裏查看 Web Events

內存和性能

事件委託/代理事件監聽

事件委託利用了事件冒泡,只指定一個事件處理程序,就能夠管理某一類型的全部事件。例如,click 事件會一直冒泡到 document 層次,也就是說,咱們能夠爲整個頁面指定一個 onclick 事件處理程序,而沒必要爲每一個可點擊的元素分別添加事件處理程序。

<ul id="myLinks">
    <li id="goSomewhere">Go somewhere</li>
    <li id="doSomething">Do something</li>
    <li id="sayHi">Say hi</li>
</ul>
<script> var list = document.getElementById("myLinks"); EVentUtil.addHandler (list, "click", function (event) { event = EVentUtil.getEvent(event); var target = EVentUtil.gitTarget(event); switch(target.id) { case "doSomething": document.title = "I changed the document's title"; break; case "goSomewhere": location.href = "https://heycoder.cn/"; break; case "sayHi": console.log("hi"); break; } }) </script>
複製代碼

移除事件處理程序

大可能是 IE 的鍋!

內存中留有那些過期不用的「空事件處理程序」,也是形成 web 應用程序內存與性能問題的主要緣由。

在兩種狀況下,可能會形成上述問題:

第一種狀況就是從文檔中移除帶有事件處理程序的元素時。這多是經過純粹的DOM操做,例如使用removeChild()replaceChild()方法,但更多地是發生在使用 innerHTML 替換頁面中某一部分的時候。若是帶有事件處理程序的元素被 innerHTML 刪除了,那麼原來添加到元素中的事件處理程序極有可能被看成垃圾回收。來看下面的例子:

<div id="myDiv">
    <input type="button" value="ClickMe" id="myBtn">
</div>
<script> var btn = document.getElementById("myBtn"); btn.onclick=function(){ document.getElementById("myDiv").innerHTML="Processing…"; } </script>
複製代碼

這裏,有一個按鈕被包含在<div>元素中,爲避免雙擊,單擊這個按鈕時就將按鈕移除並替換成一條消息;這是網站設計中很是流行的一種作法。但問題在於,當按鈕被從頁面中移除時,它還帶着一個事件處理程序呢,在<div>元素中設置 innerHTML 能夠把按鈕移走,但事件處理各類仍然與按鈕保持着引用聯繫。有的瀏覽器(尤爲是IE)在這種狀況下不會做出恰當的處理,它們頗有可能會將對元素和事件處理程序的引用都保存在內存中。若是你想知道某個元即將被移除,那麼最好手工移除事件處理程序。以下面的例子所示:

<div id="myDiv">
    <input type="button" value="ClickMe" id="myBtn">
</div>
<script> var btn = document.getElementById("myBtn"); btn.onclick=function(){ btn.onclick=null; document.getElementById("myDiv").innerHTML="Processing…"; } </script>
複製代碼

在此,咱們設置<div>的innerHTML屬性以前,先移除了按鈕的事件處理程序。這樣就確保了內存能夠被再次利用,而從DOM中移除按鈕也作到了乾淨利索。

注意,在事件處理程序中刪除按鈕也能阻止事件冒泡。目標元素在文檔中是事件冒泡的前提。

致使「空事件處理程序」的另外一狀況,就是卸載頁面中的時候。絕不奇怪,IE在這種狀況下依然是問題最多的瀏覽器,儘管其餘瀏覽器或多或少也有相似的問題。若是在頁面被卸載以前沒有清理乾淨事件處理程序。那它們就會滯留在內存中。每次加載完頁面再卸載頁面時(多是在兩個頁面間來加切換,也能夠是單擊了「刷新」按鈕),內存中滯留的對象數目就會增長,由於事件處理程序佔用的內存並無被釋放。

通常來講,最好的作法是在頁面卸載以前 ,先經過 onunload 事件處理程序移除全部事件處理程序。在此,事件委託技術再次表現出它的優點——須要跟蹤的事件程序越少,移除它們就越容易,對這種相似的操做,咱們可把它想象成:只要是經過 onload 事件處理程序添加的東西,最後都要經過 onunload 事件處理程序將它們移除。

注:不要忘了,使用 onunload 事件處理程序意味着頁面不會被緩存在 bfcachek 中,若是你在乎這個問題,那麼就只能在 IE 中經過onunload 來移除事件處理程序了。

一些經常使用的操做

load 事件:

load 事件能夠在任何資源(包括被依賴的資源)被加載完成時被觸發,這些資源能夠是圖片,css,腳本,視頻,音頻等文件,也能夠是 document 或者 window。

Image 元素 load:

// window onload
EVentUtil.addHandler (window, "load", function () {
    var image = new Image();
    // 要在指定 src 屬性以前先指定事件
    EVentUtil.addHandler (image, "load", function () {
        console.log("Image loaded!");
    });
    image.src = "smile.gif";
})
複製代碼

注意:新圖像元素不必定要添加到文檔後纔開始下載,只要設置了 src 屬性就會開始下載。

script 元素 load:

// window onload
EVentUtil.addHandler (window, "load", function () {
    var script = document.createElement("script");
    EVentUtil.addHandler (script, "load", function (event) {
        console.log("loaded!");
    });
    script.src = "example.js";
    document.body.appendChild(script);
})
複製代碼

與圖像不一樣,只有設置了 script 元素的 src 屬性並將元素添加到文檔後,纔會開始下載 js 文件,對於 script 元素而言指定 src 屬性和指定事件處理程序的前後順序就不重要了。

onbeforeunload 事件(HTML5事件):

window.onbeforeunload 讓開發人員能夠在想用戶離開一個頁面的時候進行確認。這個在有些應用中很是有用,好比用戶不當心關閉瀏覽器的 tab,咱們能夠要求用戶保存他的修改和數據,不然將會丟失他此次的操做。

EVentUtil.addHandler (window, "onbeforeunload", function (event) {
    if (textarea.value != textarea.defaultValue) {
        return 'Do you want to leave the page and discard changes?';
    }
});
複製代碼

須要注意的是,對頁面添加 onbeforeunload 處理會致使瀏覽器不對頁面進行緩存,這樣會影響頁面的訪問響應時間。 同時,onbeforeunload 的處理函數必須是同步的(synchronous)。

resize 事件:

在一些複雜的響應式佈局中,對 window 對象監聽 resize 事件是很是經常使用的一個技巧。僅僅經過 css 來達到想要的佈局效果比較困難。不少時候,咱們須要使用 JavaScript 來計算並設置一個元素的大小。

EVentUtil.addHandler (window, "resize", function (event) {
    // update the layout
});
複製代碼

error 事件:

當咱們的應用在加載資源的時候發生了錯誤,咱們不少時候須要去作點什麼,尤爲當用戶處於一個不穩定的網絡狀況下。Financial Times 中,咱們使用 error 事件來監測文章中的某些圖片加載失敗,從而馬上隱藏它。因爲「DOM Leven 3 Event」規定從新定義了 error 事件再也不冒泡,咱們可使用以下的兩種方式來處理這個事件。

imageNode.addEventListener('error', function(event) {
    image.style.display = 'none';
});
複製代碼

不幸的是,addEventListener 並不能處理全部的狀況。而確保圖片加載錯誤回調函數被執行的惟一方式是使用讓人詬病內聯事件處理函數(inline event handlers)。

<img src="http://example.com/image.jpg" onerror="this.style.display='none';" />
複製代碼

緣由是你不能肯定綁定 error 事件處理函數的代碼會在 error 事件發生以前被執行。而使用內聯處理函數意味着在標籤被解析而且請求圖片的時候,error監聽器也將並綁定。

獲取鼠標在網頁中的座標:

From:js鼠標事件參數,獲取鼠標在網頁中的座標

直接上碼:

// 鼠標事件參數 兼容性封裝 Test Already.
var EventUtil = {
    getEvent : function(e){
        return e || window.event;
    },

    getTarget : function(e){
        return this.getEvent(e).target || this.getEvent(e).srcElement;
    },

    getClientX : function(e){
        return this.getEvent(e).clientX;
    },

    getClientY : function(e){
        return this.getEvent(e).clientY;
    },

    // 水平滾動條偏移
    getScrollLeft : function(){
        return  document.documentElement.scrollLeft ||    // 火狐 IE9及如下滾動條是HTML的
                window.pageXOffset ||                     // IE10及以上 window.pageXOffset
                document.body.scrollLeft;                 // chrome 滾動條是body的
    },

    // 垂直滾動條偏移
    getScrollTop : function(){
        return  document.documentElement.scrollTop ||    // 火狐 IE9 及如下滾動條是 HTML 的
                window.pageYOffset ||                    // IE10 及以上 window.pageXOffset
                document.body.scrollTop;                 // chrome 滾動條是body的,不過目前新版的滾動條也放到 document上去了
    },

    getPageX : function(e){
        return (this.getEvent(e).pageX)?( this.getEvent(e).pageX ):( this.getClientX(e)+this.getScrollLeft() );
    },

    getPageY : function(e){
        return (this.getEvent(e).pageY)?( this.getEvent(e).pageY ):( this.getClientY(e)+this.getScrollTop() );
    }
};
複製代碼

附加

關於Chrome瀏覽器 document.body.scrollTop 一直爲0的問題可參考這裏

寫在後面的話:對於 js 事件總會有一種朦朧美的感受,最近也算沒太忙,索性再翻看下 js 高程,本篇文章算是高程事件章節的讀書筆記,也會有所擴展。

相關文章
相關標籤/搜索