這是前端面試題系列的第 7 篇,你可能錯過了前面的篇章,能夠在這裏找到:html
最近,小夥伴L 在溫習 《JavaScript高級程序設計》中的 事件
這一章節時,產生了困惑。前端
他問了我這樣幾個問題:vue
.
的形式 連結在事件以後,就能夠監聽原生事件了。它的背後有什麼原理?瀏覽器中的事件機制,也常常在面試中被說起。因此這回,咱們共同探討了這些問題,並最終整理成文,但願幫到有須要的同窗。node
先從概念提及,DOM 事件流分爲三個階段:捕獲階段
、目標階段
、冒泡階段
。先調用捕獲階段的處理函數,其次調用目標階段的處理函數,最後調用冒泡階段的處理函數。react
網景公司提出了 事件捕獲
的事件流。這就比如採礦的小遊戲,每次都會從地面開始一路往下,拋出抓鬥,捕獲礦石。在上圖中就是,某個 div 元素觸發了某個事件,最早獲得通知的是 window,而後是 document,依次往下,直到真正觸發事件的那個目標元素 div 爲止。jquery
而 事件冒泡
則是由微軟提出的,與之順序相反。仍是剛纔的採礦小遊戲,命中目標後,抓鬥再沿路收回,直到冒出地面。在上圖中就是,事件會從目標元素 div 開始依次往上,直到 window 對象爲止。面試
w3c 爲了制定統一的標準,採起了折中的方式:先捕獲在冒泡
。同一個 DOM 元素能夠註冊多個同類型的事件,經過 addEventListener 和 removeEventListener 進行管理。addEventListener 的第三個參數,就是爲了捕獲和冒泡準備的。element-ui
註冊事件
(addEventListener) 有三個參數,分別爲:"事件名稱", "事件回調", "捕獲/冒泡"(布爾型,true表明捕獲事件,false表明冒泡事件)。segmentfault
target.addEventListener(type, listener[, useCapture]);
解除事件
(removeEventListener) 也有三個參數,分別爲:"事件名稱", "事件回調", "捕獲/冒泡"(Boolean 值,這個必須和註冊事件時的類型一致)。瀏覽器
target.removeEventListener(type, listener[, useCapture]);
要想註冊過的事件可以被解除,必須將回調函數保存起來,不然沒法解除。例如這樣:
const btn = document.getElementById("test"); //將回調存儲在變量中 const fn = function(e){ alert("ok"); }; //綁定 btn.addEventListener("click", fn, false); //解除 btn.removeEventListener("click", fn, false);
當有多層交互嵌套時,事件捕獲和冒泡的前後順序,彷佛不是那麼好理解。接下來,將分 5 種狀況討論它們的順序,以及如何規避意外狀況的發生。
假設,有這樣的 html 結構:
<div id="test" class="test"> <div id="testInner" class="test-inner"></div> </div>
而後,咱們在外層 div 上註冊兩個 click 事件,分別是捕獲事件和冒泡事件,代碼以下:
const btn = document.getElementById("test"); //捕獲事件 btn.addEventListener("click", function(e){ alert("capture is ok"); }, true); //冒泡事件 btn.addEventListener("click", function(e){ alert("bubble is ok"); }, false);
點擊內層的 div,先彈出 capture is ok,後彈出 bubble is ok。只有當真正觸發事件的 DOM 元素是內層的時候,外層 DOM 元素纔有機會模擬捕獲事件和冒泡事件。
html 結構同上,js 代碼以下:
const btnInner = document.getElementById("testInner"); //冒泡事件 btnInner.addEventListener("click", function(e){ alert("bubble is ok"); }, false); //捕獲事件 btnInner.addEventListener("click", function(e){ alert("capture is ok"); }, true);
本例中,冒泡事件先註冊,因此先執行。因此,點擊內層 div,先彈出 bubble is ok
,再彈出 capture is ok
。
js 代碼以下:
const btn = document.getElementById("test"); const btnInner = document.getElementById("testInner"); btnInner.addEventListener("click", function(e){ alert("inner capture is ok"); }, true); btn.addEventListener("click", function(e){ alert("outer capture is ok"); }, true);
雖然外層 div 的事件註冊在後面,但會先觸發。因此,結果是先彈出 outer capture is ok
,再彈出 inner capture is ok
。
const btn = document.getElementById("test"); const btnInner = document.getElementById("testInner"); btn.addEventListener("click", function(e){ alert("outer bubble is ok"); }, false); btnInner.addEventListener("click", function(e){ alert("inner bubble is ok"); }, false);
先彈出 inner bubble is ok
,再彈出 outer bubble is ok
。
一般狀況下,咱們都但願點擊某個 div 時,就只觸發本身的事件回調。好比,明明點擊的是內層 div,可是外層 div 的事件也觸發了,這是就不是咱們想要的了。這時,就須要阻止事件的派發。
事件觸發時,會默認傳入一個 event 對象,這個 event 對象上有一個方法:stopPropagation
。MDN 上的解釋是:阻止 捕獲 和 冒泡 階段中,當前事件的進一步傳播。因此,經過此方法,讓外層 div 接收不到事件,天然也就不會觸發了。
btnInner.addEventListener("click", function(e){ //阻止冒泡 e.stopPropagation(); alert("inner bubble is ok"); }, false);
咱們常常會遇到,要監聽列表中多項 li 的狀況,假設咱們有一個列表以下:
<ul id="list"> <li id="item1">item1</li> <li id="item2">item2</li> <li id="item3">item3</li> <li id="item4">item4</li> </ul>
若是咱們要實現如下功能:當鼠標點擊某一 li 時,輸出該 li 的內容,咱們一般的寫法是這樣的:
window.onload=function(){ const ulNode = document.getElementById("list"); const liNodes = ulNode.children; for(var i=0; i<liNodes.length; i++){ liNodes[i].addEventListener('click',function(e){ console.log(e.target.innerHTML); }, false); } }
在傳統的事件處理中,咱們可能會按照須要,爲每個元素添加或者刪除事件處理器。然而,事件處理器將有可能致使內存泄露,或者性能降低,用得越多這種風險就越大。JavaScript 的事件代理,則是一種簡單的技巧。
事件代理,用到了在 JavaSciprt 事件中的兩個特性:事件冒泡 和 目標元素。使用事件代理,咱們能夠把事件處理器添加到一個元素上,等待一個事件從它的子級元素裏冒泡上來,而且能夠得知這個事件是從哪一個元素開始的。
改進後的 js 代碼以下:
window.onload=function(){ const ulNode=document.getElementById("list"); ulNode.addEventListener('click', function(e) { /*判斷目標事件是否爲li*/ if(e.target && e.target.nodeName.toUpperCase()=="LI"){ console.log(e.target.innerHTML); } }, false); };
回到文章開頭的問題:瞭解事件流的順序,對平常的工做有什麼幫助呢?我總結了如下幾個注意點。
好比 href 的連接跳轉,submit 的表單提交等。能夠在方法的最後,加上一行 return false;
。它會阻止經過 on 的方式綁定的事件的默認事件。
ele.onclick = function() { …… // 經過返回 false 值,阻止默認事件行爲 return false; }
另外,重寫 onclick 會覆蓋以前的屬性,因此解綁事件能夠這麼寫:
// 解綁事件,將 onlick 屬性設爲 null 便可 ele.onclick = null;
前面說過 stopPropagation 的定義是:終止事件在傳播過程的捕獲、目標處理或起泡階段進一步傳播。事件再也不被分派到其餘節點上。
// 事件捕獲到 ele 元素後,就再也不向下傳播了 ele.addEventListener('click', function (event) { event.stopPropagation(); }, true); // 事件冒泡到 ele 元素後,就再也不向上傳播了 ele.addEventListener('click', function (event) { event.stopPropagation(); }, false);
可是,stopPropagation 只會阻止當前元素 同類型的
事件冒泡或捕獲的傳播,並不會阻止該元素上 其餘類型
事件的監聽。以 click 事件爲例:
ele.addEventListener('click', function (event) { event.stopPropagation(); console.log(1); }); ele.addEventListener('click', function(event) { // 仍然能夠觸發 console.log(2); });
若是想禁用以後全部的 click 事件,就要用到 stopImmediatePropagation 了。可是,須要注意的是,stopImmediatePropagation 只會禁用以後註冊的同類型的監聽事件。就好比阻止了以後的 click 事件監聽函數,但別的事件類型如 mousedown、dblclick 之類,仍是能夠監聽到的。
ele.addEventListener('click', function (event) { event.stopImmediatePropagation(); console.log(1); }); ele.addEventListener('click', function(event) { // 不會觸發 console.log(2); }); ele.addEventListener('mousedown', function(event) { // 會觸發 console.log(3); });
jquery 中的 on 是事件冒泡。當用 return false; 阻止瀏覽器的默認行爲時,會作下面這 3 件事:
這 3 件事中,只有 preventDefault 是用來阻止默認行爲的。除非你還想阻止事件冒泡,不然直接用 return false; 會埋下隱患。
angular 是個一應俱全的框架,彷佛學完它的一整套以後,就能玩轉世界了。它加工封裝了許多原生的東西,其中就包括了 event,只是前面須要加一個 $,表示這是 angular 中的特有對象。
// template <div> <button (click)="doSomething($event)">Click me</button> </div> // js doSomething($event: Event) { $event.stopPropagation(); ... }
$event 在這裏做爲一個變量,顯式地
傳入回調函數,以後就能夠將 $event 當作原生的事件對象來用了。
在 vue 的自定義組件中綁定原生事件,須要用到修飾符 native。
那是由於,咱們的自定義組件,最終會渲染成原生的 html 標籤,而非相似於 這樣的自定義組件。若是想讓一個普通的 html 標籤觸發事件,那就須要對它作事件監聽(addEventListener)。修飾符 native 的做用就在這裏,它能夠在背後幫咱們綁定了原生事件,進行監聽。
一個經常使用的場景是,配合 element-ui 作登陸界面時,輸完帳號密碼,想按一下回車就能登陸。就能夠像下面這樣用修飾符:
<el-input class="input" v-model="password" type="password" @keyup.enter.native="handleSubmit"> </el-input>
el-input 就是自定義組件,而 keyup 就是原生事件,須要用 native 修飾符進行綁定才能監聽到。
想要在 react 的事件回調中使用 event 對象,會產生困擾,會發現很多原生的屬性都是 null。
那是由於在 react 中的事件,實際上是合成事件(SyntheticEvent),並非瀏覽器的原生事件,但它也符合 w3c 規範。
舉一個簡單的例子,咱們要實現一個組件,它有一個按鈕,點擊按鈕後會顯示一張圖片,點擊這張圖片以外的任意區域,能夠隱藏這張圖片,可是點擊該圖片自己時,不會隱藏。代碼以下:
class ShowImg extends Component { constructor(props) { super(props); this.state = { active: false }; } componentDidMount() { document.addEventListener('click', this.hideImg.bind(this)); } componentWillUnmount() { document.removeEventListener('click', this.hideImg); } hideImg () { this.setState({ active: false }); } handleClickBtn() { this.setState({ active: !this.state.active }); } handleClickImg (e) { e.stopPropagation(); } render() { return ( <div className="img-wrapper"> <button className="showImgBtn" onClick={this.handleClickBtn.bind(this)}> 顯示圖片 </button> <div className="img" style={{ display: this.state.active ? 'block' : 'none' }} onClick={this.handleClickImg.bind(this)}> <img src="@/assets/avatar.jpg" > </div> </div> ); } }
按照以前說的原生事件機制,咱們會錯誤地認爲經過:
handleClickImg (e) { e.stopPropagation(); }
就能夠阻止事件的派發了,但其實無法這麼作。想要解決這個問題,固然也不復雜,就把 react 的事件和原生事件分開便可。
componentDidMount() { document.addEventListener('click', this.hideImg.bind(this)); document.addEventListener('click', this.imgStopPropagation.bind(this)); } componentWillUnmount() { document.removeEventListener('click', this.hideImg); document.removeEventListener('click', this.imgStopPropagation); } hideImg () { this.setState({ active: false }); } imgStopPropagation (e) { e.stopPropagation(); }
當對一個元素進行事件監聽的時候,它的回調函數裏就會默認傳遞一個參數 event,它是一個對象,包含了許多屬性。我列出了一些比較經常使用的屬性:
事件機制在瀏覽器中很是有用,全部用戶的交互型操做,都依賴於它。現代 JavaScript 框架應用中,咱們也都離不開與原生事件的交互。
因此,在理解了事件流的概念,清楚了事件捕獲與冒泡的順序,掌握了一些原生事件的技巧以後,相信下次再遇到坑的時候,能夠少走一些彎路了。
PS:歡迎關注個人公衆號 「超哥前端小棧」,交流更多的想法與技術。