JS專題之事件模型

南波發表於2018-11-21

本文共 1960 字,讀完只需 8 分鐘

事件

使用者與網頁互動是通過事件實現的,事件剛開始是作為分擔伺服器負載的一個手段,起初沒有統一的規範,直到 DOM2 級,網景和 IE 才開始有各自的 API 規範。

對於事件的觸發機制,兩個公司都認為頁面的觸發機制並不只是點選了某個元素,就只觸發當前目標元素的事件。

比方說:頁面有多個同心圓,當點選最裡面的圓時,你其實也點選了包含這個圓外面的那些圓。 兩個公司對這點的認同是一致的,但是事件流的傳播順序上採用了不同的兩種方案來實現,即事件冒泡和事件捕獲。

一、事件冒泡

IE 瀏覽器從老版本開始就一直支援事件冒泡機制,所謂事件冒泡,指事件流開始是從較為具體的元素接收,一直傳播上不具體的元素上。

就是從目標元素傳播到父級元素。

<body>
    <div id="parent">
        <div id="child"></div>
    </div>
    <script>
    function childEventHandler(event) {
        console.log(this);
        console.log("child 被點選了");
    }
    function parentEventHandler(event) {
        console.log(this);
        console.log("parent 被點選了");
    }
    function bodyEventHandler(event) {
        console.log(this);
        console.log("body 被點選了");
    }
    function htmlEventHandler(event) {
        console.log(this);
        console.log("html 被點選了");
    }
    function documentEventHandler(event) {
        console.log(this);
        console.log("document 被點選了");
    }
    function windowEventHandler(event) {
        console.log(this);
        console.log("window 被點選了");
    }
    var bodyEl = document.getElementsByTagName("body")[0];
    var htmlEl = document.getElementsByTagName("html")[0];
    var win = window;
    var parentEl = document.getElementById("parent");
    var childEl = document.getElementById("child");
    childEl.onclick = childEventHandler;
    parentEl.onclick = parentEventHandler;
    bodyEl.onclick = bodyEventHandler;
    htmlEl.onclick = htmlEventHandler;
    document.onclick = documentEventHandler;
    win.onclick = windowEventHandler;
    </script>
</body>
複製程式碼

如下圖所示,如果點選了 id 為 child 的元素後,事件流會從 child 一直傳播到 window 物件。

JS專題之事件模型

JS專題之事件模型

所有現代瀏覽器都支援事件冒泡。

二、事件捕獲

由網景公司主導的事件捕獲則剛好和事件冒泡相反,事件流開始從不具體的元素觸發,然後傳播到具體的元素上。簡而言之就是從父級元素傳播到目標元素。

JS專題之事件模型

由於事件捕獲是從 IE 9開始支援,不相容老版本瀏覽器,所以使用的人比較少。

三、事件流

DOM 規定事件包括三個階段,事件捕獲,處於目標階段、事件冒泡。

從 IE 9 開始的瀏覽器規定,事件流的順序先是事件捕獲,會截獲到事件,然後是處於目標階段,實際的目標接收到事件,最後是事件冒泡,可以在這個階段對事件進行響應。

以之前的 child 元素為例,直到 child 元素接收到事件前(從 window 到 parent),都是事件捕獲階段。到了 child 元素,此時對事件進行處理,隨後冒泡到 window 物件上,冒泡階段也是可以對事件進行處理的。 基於事件冒泡能對事件進行處理的特點,隨後將講到與其有關的事件委託機制。

JS專題之事件模型

四、事件繫結

HTML 與 事件的繫結有三種形式:

1. 
<div id="child" onclick="console.log('hello');"></div>

2. 

var childEl = document.getElementById("child");
childEl.onclick = function() {
    console.log('hello');
}

3. 
var childEl = document.getElementById("child");
childEl.addEventListener('click', function() {
    console.log('hello');
}, false);
複製程式碼

JavaScript 是單執行緒的語言,在遇到元素有事件觸發時,會在事件佇列中尋找有沒有與這個事件繫結的函式,如果沒有則什麼都不做,如果有,則將該函式放到事件佇列的前面,等待主執行緒事件執行完畢後執行。

上述程式碼第一種繫結,將事件寫在 html 中,表現和行為沒有解耦,是不建議這樣寫程式碼的。

第二種繫結,將事件繫結在元素物件上,這種寫法主要是容易發生事件的覆蓋。

第三種繫結,首先,第三個引數為布林值,預設為 false, 表示在事件冒泡階段呼叫事件處理程式,如果為 true, 則表示在事件捕獲階段呼叫事件處理函式。

當我們想處理完一次事件後,將不想在處理該元素的事件繫結時,應該將元素的事件繫結置為空,如果容易發生記憶體洩漏。

第一種寫法:
childEl.onclick = null;


第三種寫法:
function eventHandler() {
    console.log('hello');
}

childEl.addEventListener('click', eventHandler, false);

childEl.removeEventListener('click', eventHandler, false);
複製程式碼

五、事件委託(事件代理)

事件委託是利用事件冒泡的性質,事件流開始是從較為具體的元素接收,一直傳播上不具體的元素上。

首先,假如有一個列表 ul,每個列表元素 li 點選會觸發事件處理程式,顯然,如果一個一個地給元素繫結事件, 效率肯定不好。

與此同時,當新增一個元素時,事件不見得會繫結成功。一起來看:

<ul id="menu">
	<li class="menu-item">menu-1</li>
	<li class="menu-item">menu-2</li>
	<li class="menu-item">menu-3</li>
	<li class="menu-item">menu-4</li>
</ul>
<input type="button" name="" id="addBtn" value="新增" />

<script>
window.onload = function() {
        var menu = document.getElementById("menu");
        var item = menu.getElementsByClassName('menu-item');
        for (var i = 0; i < item.length; i++) {
            item[i].onclick = (function(i) {
                return function() {
                    console.log(i);
                }
            }(i))
        }

        var addBtnEl = document.getElementById("addBtn");
        addBtnEl.onclick = function() {
            var newEl = document.createElement('li');
            newEl.innerHTML = "menu-new"
            menu.appendChild(newEl);
        }
    }
</script>
複製程式碼

新增加的 menu-new,點選發現沒有反應,說明事件沒有繫結進去,但是我們也並不想,每增加一個新元素,就為這個新元素繫結事件,重複低效率的工作應當避免去做。

我們通過事件委託的思路來想,事件流的傳播,目標元素本身依然會有事件,但同時,冒泡出去後,更高層次的 dom 也能處理事件程式。那麼,我們只需要給高層次節點繫結事件,通過判斷具體是觸發的哪個子節點,再做相應的事件處理。

<ul id="menu">
	<li class="menu-item">menu-1</li>
	<li class="menu-item">menu-2</li>
	<li class="menu-item">menu-3</li>
	<li class="menu-item">menu-4</li>
</ul>
<input type="button" name="" id="addBtn" value="新增" />

<script>
window.onload = function() {
        var menu = document.getElementById("menu");
        menu.onclick = function(event) {
            var e = event || window.event;
            var target = e.target || e.srcElement;
            console.log(e);
            switch (target.textContent) {
                case "menu-1":
                    console.log("menu-1 被點選了");
                    break;

                case "menu-2":
                    console.log("menu-2 被點選了");
                    break;

                case "menu-3":
                    console.log("menu-3 被點選了");
                    break;

                case "menu-4":
                    console.log("menu-4 被點選了");
                    break;

                case "menu-new":
                    console.log("menu-new 被點選了");
                    break;
            }
        }

        var addBtnEl = document.getElementById("addBtn");
        addBtnEl.onclick = function() {
            var newEl = document.createElement('li');
            newEl.innerHTML = "menu-new"
            menu.appendChild(newEl);
        }
    }
</script>
複製程式碼

menu 列表的每個子選單元素的事件都能正確響應,新增的 menu-new 同樣也能正確響應事件。

事件委託的好處在於,我們不用給每個元素都一一地手動新增繫結事件,避免重複低效的工作。

其次,事件委託更少得獲取 dom, 初始化元素物件和事件函式,能有效減少記憶體佔用。

每當將事件程式指定給元素時,html 程式碼和 js 程式碼之間就建立了一個連線,這種連線越多,網頁就執行起來越慢,所以事件委託能有效減少連線樹,提高網頁效能。

總結

使用者與網頁的互動是通過事件進行的,事件模型分為事件冒泡和事件捕獲,事件冒泡的相容性更好,應用更廣,同時通過事件冒泡,可以建立事件委託,提升網頁效能。

相關文章