摘要:要想注冊過的事件能夠被解除,必須將回調(diào)函數(shù)保存起來,否則無法解除。當(dāng)用阻止瀏覽器的默認(rèn)行為時,會做下面這件事停止回調(diào)函數(shù)執(zhí)行并立即返回。
前言
這是前端面試題系列的第 7 篇,你可能錯過了前面的篇章,可以在這里找到:
理解函數(shù)的柯里化
ES6 中箭頭函數(shù)的用法
this 的原理以及用法
偽類與偽元素的區(qū)別及實戰(zhàn)
如何實現(xiàn)一個圣杯布局?
今日頭條 面試題和思路解析
最近,小伙伴L 在溫習(xí) 《JavaScript高級程序設(shè)計》中的 事件 這一章節(jié)時,產(chǎn)生了困惑。
他問了我這樣幾個問題:
了解事件流的順序,對日常的工作有什么幫助么?
在 vue 的文檔中,有一個修飾符 native ,把它用 . 的形式 連結(jié)在事件之后,就可以監(jiān)聽原生事件了。它的背后有什么原理?
事件的 event 對象中,有好多的屬性和方法,該如何使用?
瀏覽器中的事件機制,也經(jīng)常在面試中被提及。所以這回,我們共同探討了這些問題,并最終整理成文,希望幫到有需要的同學(xué)。
事件流的概念先從概念說起,DOM 事件流分為三個階段:捕獲階段、目標(biāo)階段、冒泡階段。先調(diào)用捕獲階段的處理函數(shù),其次調(diào)用目標(biāo)階段的處理函數(shù),最后調(diào)用冒泡階段的處理函數(shù)。
網(wǎng)景公司提出了 事件捕獲 的事件流。這就好比采礦的小游戲,每次都會從地面開始一路往下,拋出抓斗,捕獲礦石。在上圖中就是,某個 div 元素觸發(fā)了某個事件,最先得到通知的是 window,然后是 document,依次往下,直到真正觸發(fā)事件的那個目標(biāo)元素 div 為止。
而 事件冒泡 則是由微軟提出的,與之順序相反。還是剛才的采礦小游戲,命中目標(biāo)后,抓斗再沿路收回,直到冒出地面。在上圖中就是,事件會從目標(biāo)元素 div 開始依次往上,直到 window 對象為止。
w3c 為了制定統(tǒng)一的標(biāo)準(zhǔn),采取了折中的方式:先捕獲在冒泡。同一個 DOM 元素可以注冊多個同類型的事件,通過 addEventListener 和 removeEventListener 進行管理。addEventListener 的第三個參數(shù),就是為了捕獲和冒泡準(zhǔn)備的。
注冊事件(addEventListener) 有三個參數(shù),分別為:"事件名稱", "事件回調(diào)", "捕獲/冒泡"(布爾型,true代表捕獲事件,false代表冒泡事件)。
target.addEventListener(type, listener[, useCapture]);
type 表示事件類型的字符串。
listener 是一個實現(xiàn)了 EventListener 接口的對象,或者是一個函數(shù)。當(dāng)所監(jiān)聽的事件類型觸發(fā)時,會接收到一個事件通知對象(實現(xiàn)了 Event 接口的對象)。
capture 表示 listener 會在該類型的事件捕獲階段,傳播到該 EventTarget 時觸發(fā),它是一個 Boolean 值。
解除事件(removeEventListener) 也有三個參數(shù),分別為:"事件名稱", "事件回調(diào)", "捕獲/冒泡"(Boolean 值,這個必須和注冊事件時的類型一致)。
target.removeEventListener(type, listener[, useCapture]);
要想注冊過的事件能夠被解除,必須將回調(diào)函數(shù)保存起來,否則無法解除。例如這樣:
const btn = document.getElementById("test"); //將回調(diào)存儲在變量中 const fn = function(e){ alert("ok"); }; //綁定 btn.addEventListener("click", fn, false); //解除 btn.removeEventListener("click", fn, false);事件捕獲和冒泡的5個注意點
當(dāng)有多層交互嵌套時,事件捕獲和冒泡的先后順序,似乎不是那么好理解。接下來,將分 5 種情況討論它們的順序,以及如何規(guī)避意外情況的發(fā)生。
1.在外層 div 注冊事件,點擊內(nèi)層 div 來觸發(fā)事件時,捕獲事件總是要比冒泡事件先觸發(fā)(與代碼順序無關(guān))假設(shè),有這樣的 html 結(jié)構(gòu):
然后,我們在外層 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);
點擊內(nèi)層的 div,先彈出 capture is ok,后彈出 bubble is ok。只有當(dāng)真正觸發(fā)事件的 DOM 元素是內(nèi)層的時候,外層 DOM 元素才有機會模擬捕獲事件和冒泡事件。
2.當(dāng)在觸發(fā)事件的 DOM 元素上注冊事件時,哪個先注冊,就先執(zhí)行哪個html 結(jié)構(gòu)同上,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);
本例中,冒泡事件先注冊,所以先執(zhí)行。所以,點擊內(nèi)層 div,先彈出 bubble is ok,再彈出 capture is ok。
3.當(dāng)外層 div 和內(nèi)層 div 同時注冊了捕獲事件時,點擊內(nèi)層 div 時,外層 div 的事件一定會先觸發(fā)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 的事件注冊在后面,但會先觸發(fā)。所以,結(jié)果是先彈出 outer capture is ok,再彈出 inner capture is ok。
4.同理,當(dāng)外層 div 和內(nèi)層 div 都同時注冊了冒泡事件,點擊內(nèi)層 div 時,一定是內(nèi)層 div 事件先觸發(fā)。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。
5.阻止事件的派發(fā)通常情況下,我們都希望點擊某個 div 時,就只觸發(fā)自己的事件回調(diào)。比如,明明點擊的是內(nèi)層 div,但是外層 div 的事件也觸發(fā)了,這是就不是我們想要的了。這時,就需要阻止事件的派發(fā)。
事件觸發(fā)時,會默認(rèn)傳入一個 event 對象,這個 event 對象上有一個方法:stopPropagation。MDN 上的解釋是:阻止 捕獲 和 冒泡 階段中,當(dāng)前事件的進一步傳播。所以,通過此方法,讓外層 div 接收不到事件,自然也就不會觸發(fā)了。
btnInner.addEventListener("click", function(e){ //阻止冒泡 e.stopPropagation(); alert("inner bubble is ok"); }, false);事件代理
我們經(jīng)常會遇到,要監(jiān)聽列表中多項 li 的情況,假設(shè)我們有一個列表如下:
如果我們要實現(xiàn)以下功能:當(dāng)鼠標(biāo)點擊某一 li 時,輸出該 li 的內(nèi)容,我們通常的寫法是這樣的:
window.onload=function(){ const ulNode = document.getElementById("list"); const liNodes = ulNode.children; for(var i=0; i在傳統(tǒng)的事件處理中,我們可能會按照需要,為每一個元素添加或者刪除事件處理器。然而,事件處理器將有可能導(dǎo)致內(nèi)存泄露,或者性能下降,用得越多這種風(fēng)險就越大。JavaScript 的事件代理,則是一種簡單的技巧。
用法及原理事件代理,用到了在 JavaSciprt 事件中的兩個特性:事件冒泡 和 目標(biāo)元素。使用事件代理,我們可以把事件處理器添加到一個元素上,等待一個事件從它的子級元素里冒泡上來,并且可以得知這個事件是從哪個元素開始的。
改進后的 js 代碼如下:
window.onload=function(){ const ulNode=document.getElementById("list"); ulNode.addEventListener("click", function(e) { /*判斷目標(biāo)事件是否為li*/ if(e.target && e.target.nodeName.toUpperCase()=="LI"){ console.log(e.target.innerHTML); } }, false); };一些常用技巧回到文章開頭的問題:了解事件流的順序,對日常的工作有什么幫助呢?我總結(jié)了以下幾個注意點。
1. 阻止默認(rèn)事件比如 href 的鏈接跳轉(zhuǎn),submit 的表單提交等。可以在方法的最后,加上一行 return false;。它會阻止通過 on 的方式綁定的事件的默認(rèn)事件。
ele.onclick = function() { …… // 通過返回 false 值,阻止默認(rèn)事件行為 return false; }另外,重寫 onclick 會覆蓋之前的屬性,所以解綁事件可以這么寫:
// 解綁事件,將 onlick 屬性設(shè)為 null 即可 ele.onclick = null;2. stopPropagation 和 stopImmediatePropagation前面說過 stopPropagation 的定義是:終止事件在傳播過程的捕獲、目標(biāo)處理或起泡階段進一步傳播。事件不再被分派到其他節(jié)點上。
// 事件捕獲到 ele 元素后,就不再向下傳播了 ele.addEventListener("click", function (event) { event.stopPropagation(); }, true); // 事件冒泡到 ele 元素后,就不再向上傳播了 ele.addEventListener("click", function (event) { event.stopPropagation(); }, false);但是,stopPropagation 只會阻止當(dāng)前元素 同類型的 事件冒泡或捕獲的傳播,并不會阻止該元素上 其他類型 事件的監(jiān)聽。以 click 事件為例:
ele.addEventListener("click", function (event) { event.stopPropagation(); console.log(1); }); ele.addEventListener("click", function(event) { // 仍然可以觸發(fā) console.log(2); });如果想禁用之后所有的 click 事件,就要用到 stopImmediatePropagation 了。但是,需要注意的是,stopImmediatePropagation 只會禁用之后注冊的同類型的監(jiān)聽事件。就比如阻止了之后的 click 事件監(jiān)聽函數(shù),但別的事件類型如 mousedown、dblclick 之類,還是可以監(jiān)聽到的。
ele.addEventListener("click", function (event) { event.stopImmediatePropagation(); console.log(1); }); ele.addEventListener("click", function(event) { // 不會觸發(fā) console.log(2); }); ele.addEventListener("mousedown", function(event) { // 會觸發(fā) console.log(3); });3. jquery 中的 return false;jquery 中的 on 是事件冒泡。當(dāng)用 return false; 阻止瀏覽器的默認(rèn)行為時,會做下面這 3 件事:
event.preventDefault();
event.stopPropagation();
停止回調(diào)函數(shù)執(zhí)行并立即返回。
這 3 件事中,只有 preventDefault 是用來阻止默認(rèn)行為的。除非你還想阻止事件冒泡,否則直接用 return false; 會埋下隱患。
4. angular 中的 $eventangular 是個包羅萬象的框架,似乎學(xué)完它的一整套之后,就能玩轉(zhuǎn)世界了。它加工封裝了許多原生的東西,其中就包括了 event,只是前面需要加一個 $,表示這是 angular 中的特有對象。
// template// js doSomething($event: Event) { $event.stopPropagation(); ... }$event 在這里作為一個變量,顯式地 傳入回調(diào)函數(shù),之后就可以將 $event 當(dāng)做原生的事件對象來用了。
5. vue 中的 native 修飾符在 vue 的自定義組件中綁定原生事件,需要用到修飾符 native。
那是因為,我們的自定義組件,最終會渲染成原生的 html 標(biāo)簽,而非類似于 這樣的自定義組件。如果想讓一個普通的 html 標(biāo)簽觸發(fā)事件,那就需要對它做事件監(jiān)聽(addEventListener)。修飾符 native 的作用就在這里,它可以在背后幫我們綁定了原生事件,進行監(jiān)聽。
一個常用的場景是,配合 element-ui 做登錄界面時,輸完賬號密碼,想按一下回車就能登錄。就可以像下面這樣用修飾符:
el-input 就是自定義組件,而 keyup 就是原生事件,需要用 native 修飾符進行綁定才能監(jiān)聽到。
6. react 中的合成事件想要在 react 的事件回調(diào)中使用 event 對象,會產(chǎn)生困擾,會發(fā)現(xiàn)不少原生的屬性都是 null。
那是因為在 react 中的事件,其實是合成事件(SyntheticEvent),并不是瀏覽器的原生事件,但它也符合 w3c 規(guī)范。
舉一個簡單的例子,我們要實現(xiàn)一個組件,它有一個按鈕,點擊按鈕后會顯示一張圖片,點擊這張圖片之外的任意區(qū)域,可以隱藏這張圖片,但是點擊該圖片本身時,不會隱藏。代碼如下:
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 (); } }按照之前說的原生事件機制,我們會錯誤地認(rèn)為通過:
handleClickImg (e) { e.stopPropagation(); }就可以阻止事件的派發(fā)了,但其實沒法這么做。想要解決這個問題,當(dāng)然也不復(fù)雜,就把 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(); }7. 事件對象 event當(dāng)對一個元素進行事件監(jiān)聽的時候,它的回調(diào)函數(shù)里就會默認(rèn)傳遞一個參數(shù) event,它是一個對象,包含了許多屬性。我列出了一些比較常用的屬性:
event.target:指的是觸發(fā)事件的那個節(jié)點,也就是事件最初發(fā)生的節(jié)點。
event.target.matches:可以對關(guān)鍵節(jié)點進行匹配,來執(zhí)行相應(yīng)操作。
event.currentTarget:指的是正在執(zhí)行的監(jiān)聽函數(shù)的那個節(jié)點。
event.isTrusted:表示事件是否是真實用戶觸發(fā)。
event.preventDefault():取消事件的默認(rèn)行為。
event.stopPropagation():阻止事件的派發(fā)(包括了捕獲和冒泡)。
event.stopImmediatePropagation():阻止同一個事件的其他監(jiān)聽函數(shù)被調(diào)用。
總結(jié)事件機制在瀏覽器中非常有用,所有用戶的交互型操作,都依賴于它。現(xiàn)代 JavaScript 框架應(yīng)用中,我們也都離不開與原生事件的交互。
所以,在理解了事件流的概念,清楚了事件捕獲與冒泡的順序,掌握了一些原生事件的技巧之后,相信下次再遇到坑的時候,可以少走一些彎路了。
PS:歡迎關(guān)注我的公眾號 “超哥前端小棧”,交流更多的想法與技術(shù)。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/102052.html
摘要:插件開發(fā)前端掘金作者原文地址譯者插件是為應(yīng)用添加全局功能的一種強大而且簡單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內(nèi)優(yōu)雅的實現(xiàn)文件分片斷點續(xù)傳。 Vue.js 插件開發(fā) - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應(yīng)用添加全局功能的一種強大而且簡單的方式。插....
摘要:系列種優(yōu)化頁面加載速度的方法隨筆分類中個最重要的技術(shù)點常用整理網(wǎng)頁性能管理詳解離線緩存簡介系列編寫高性能有趣的原生數(shù)組函數(shù)數(shù)據(jù)訪問性能優(yōu)化方案實現(xiàn)的大排序算法一怪對象常用方法函數(shù)收集數(shù)組的操作面向?qū)ο蠛驮屠^承中關(guān)鍵詞的優(yōu)雅解釋淺談系列 H5系列 10種優(yōu)化頁面加載速度的方法 隨筆分類 - HTML5 HTML5中40個最重要的技術(shù)點 常用meta整理 網(wǎng)頁性能管理詳解 HTML5 ...
摘要:系列種優(yōu)化頁面加載速度的方法隨筆分類中個最重要的技術(shù)點常用整理網(wǎng)頁性能管理詳解離線緩存簡介系列編寫高性能有趣的原生數(shù)組函數(shù)數(shù)據(jù)訪問性能優(yōu)化方案實現(xiàn)的大排序算法一怪對象常用方法函數(shù)收集數(shù)組的操作面向?qū)ο蠛驮屠^承中關(guān)鍵詞的優(yōu)雅解釋淺談系列 H5系列 10種優(yōu)化頁面加載速度的方法 隨筆分類 - HTML5 HTML5中40個最重要的技術(shù)點 常用meta整理 網(wǎng)頁性能管理詳解 HTML5 ...
摘要:函數(shù)式編程前端掘金引言面向?qū)ο缶幊桃恢币詠矶际侵械闹鲗?dǎo)范式。函數(shù)式編程是一種強調(diào)減少對程序外部狀態(tài)產(chǎn)生改變的方式。 JavaScript 函數(shù)式編程 - 前端 - 掘金引言 面向?qū)ο缶幊桃恢币詠矶际荍avaScript中的主導(dǎo)范式。JavaScript作為一門多范式編程語言,然而,近幾年,函數(shù)式編程越來越多得受到開發(fā)者的青睞。函數(shù)式編程是一種強調(diào)減少對程序外部狀態(tài)產(chǎn)生改變的方式。因此,...
閱讀 3056·2021-11-18 10:02
閱讀 3324·2021-11-02 14:48
閱讀 3387·2019-08-30 13:52
閱讀 547·2019-08-29 17:10
閱讀 2079·2019-08-29 12:53
閱讀 1400·2019-08-29 12:53
閱讀 1024·2019-08-29 12:25
閱讀 2162·2019-08-29 12:17