javaScript 瀏覽器事件
1.事件基本概念
事件是指文檔或者瀏覽器中發(fā)生的一些特定交互瞬間,比如打開某一個(gè)網(wǎng)頁,瀏覽器加載完成后會(huì)觸發(fā)load事件,當(dāng)鼠標(biāo)浮于某一個(gè)元素上時(shí)會(huì)觸發(fā)hover事件,當(dāng)鼠標(biāo)點(diǎn)擊某一個(gè)元素時(shí)會(huì)觸發(fā)click事件等等。
事件處理就是當(dāng)事件被觸發(fā)吼,瀏覽器響應(yīng)這個(gè)事件的行為,而這個(gè)行為所對(duì)應(yīng)的代碼即為事件處理程序。
2.事件操作:監(jiān)聽與移除監(jiān)聽
2.1監(jiān)聽事件
瀏覽器會(huì)根據(jù)一些事件作出相對(duì)應(yīng)的事件處理,事件處理的前提是需要監(jiān)聽事件,監(jiān)聽事件的方法主要有以下三種:
2.1.1 HTML內(nèi)聯(lián)屬性
即在HTML元素里直接填寫與事件相關(guān)的屬性,屬性值為事件處理程序。示例如下:
<button onclick="console.log('You clicked me!');"></button>
onclick對(duì)應(yīng)著click事件,所以當(dāng)按鈕被點(diǎn)擊后,便會(huì)執(zhí)行事件處理程序,即控制臺(tái)輸出“You clicked me!”。
不過我們需要指出的是,這種方式將HTML代碼與JavaScript代碼耦合在一起,不利于代碼的維護(hù),所以應(yīng)該盡量避免使用這樣的方式。
2.1.2 DOM屬性綁定
通過直接設(shè)置某個(gè)DOM節(jié)點(diǎn)的屬性來指定事件和事件處理程序,上代碼
const btn = document.getElementById("btn");
btn.onclick = function(e) {
console.log("You clicked me!")
}
上面示例中,首先獲得btn這個(gè)對(duì)象,通過給這個(gè)對(duì)象添加onclick屬性的方式來監(jiān)聽click事件,這個(gè)屬性值對(duì)應(yīng)的就是事件處理程序。這段程序也被成為DOM 0級(jí)事件處理程序。
2.1.3 事件監(jiān)聽函數(shù)
標(biāo)準(zhǔn)的事件監(jiān)聽函數(shù)如下:
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
console.log("You click me!");
}, false);
上面的示例表示先獲得表示節(jié)點(diǎn)的btn對(duì)象,然后在這個(gè)對(duì)象上面添加了一個(gè)事件監(jiān)聽器,當(dāng)監(jiān)聽到click事件發(fā)生時(shí),則調(diào)用回調(diào)函數(shù),即在控制臺(tái)輸出“You clicked me!”。addEventListener函數(shù)包含了第三個(gè)參數(shù)false,第三個(gè)參數(shù)的含義在后面的事件觸發(fā)三個(gè)階段之后再講解。這段程序也被稱作DOM 2級(jí)事件處理程序。IE9+、FireFox、Safari、Chrome 和 Opera 都是支持 DOM 2 級(jí)事件處理程序的,對(duì)于 IE8 及以下版本,則用 attacEvent() 函數(shù)綁定事件。
所以我們得寫一段具有兼容性的代碼:
function addEventHandler(obj, eventName, handler) {
if (document.addEventListener) {
obj.addEventListener(eventName, handler, false);
} else if (document.attachEvent) {
obj.attachEvent("on" + eventName, handler);
} else {
obj["on" + eventName] = handler;
}
}
2.2移除事件監(jiān)聽
在為某個(gè)元素綁定了一個(gè)事件后,如果想解除綁定,則需要removeEventListener方法。
const handler = function() {
// hanlder logic
}
const btn = document.getElementById("btn");
btn.addEventListener("click", handler);
btn.removeEventListener("click", handler);
需要注意的是,綁定事件的回調(diào)函數(shù)不能是匿名函數(shù),必須是一個(gè)已經(jīng)被聲明的函數(shù),因?yàn)榻獬录壎〞r(shí)需要傳遞這個(gè)回調(diào)函數(shù)的引用。
同樣,IE8 及以下版本也不支持上面的方法,而是用 detachEvent 代替。
const handler = function() {
// hnalder logic
}
const btn = document.getElementById("btn");
btn.attachEvent("onclick", handler);
btn.detachEvent("onclick", handler);
同樣,可以寫一段具有兼容性的刪除事件函數(shù):
function removeEventHandler(obj, eventName, handler) {
if (document.removeEventListener) {
obj.removeEventListener(eventName, handler, false);
} else if (document.detachEvent) {
obj.detachEvent("on" + eventName, handler);
} else {
obj["on" + eventName] = null;
}
}
DOM事件級(jí)別
DOM級(jí)別一共可以分為四個(gè)級(jí)別:DOM0級(jí)、DOM1級(jí)、DOM2級(jí)、DOM3級(jí)。而DOM事件分為3個(gè)級(jí)別:DOM 0級(jí)事件處理,DOM2級(jí)事件處理和DOM3級(jí)事件處理。由于DOM1級(jí)中沒有事件的相關(guān)內(nèi)容,所以沒有DOM1級(jí)事件。
DOM0級(jí)事件
el.onclick=function(){}
var btn = document.getElementById("btn");
btn.onclick = function() {
alert(this.innerHTML);
}
當(dāng)希望為同一個(gè)元素、標(biāo)簽綁定多個(gè)同類型事件的時(shí)候(如給上面的這個(gè)btn元素綁定3個(gè)點(diǎn)擊事件),是不背允許的。DOM0事件綁定,給元素的事件行為綁定方法,這些方法都是在當(dāng)前元素事件行為的冒泡階段(或者目標(biāo)階段)執(zhí)行的。
DOM 2級(jí)事件
el.addEventListener(event-name,callback,useCapture)
- event-name: 事件名稱,可以是標(biāo)準(zhǔn)的DOM事件。
- callback:回調(diào)函數(shù),當(dāng)事件觸發(fā)時(shí),函數(shù)會(huì)被注入一個(gè)參數(shù)為當(dāng)前事件對(duì)象event。
- useCpature:默認(rèn)是false,代表事件句柄在冒泡執(zhí)行階段執(zhí)行(或者說注冊(cè)的是冒泡事件),true表示事件句柄在捕獲階段執(zhí)行(或者說注冊(cè)的是捕獲事件)
var btn = document.getElementById('btn');
btn.addEventListener("click", test, false);
function test(e) {
e = e || window.event;
alert((e.target || e.srcElement).innerHTML);
btn.removeEventListner("click", test);
}
IE9以下的IE瀏覽器不支持 addEventListener()和removeEventListener(),使用 attachEvent()與detachEvent() 代替,因?yàn)镮E9以下是不支持事件捕獲的,所以也沒有第三個(gè)參數(shù),第一個(gè)事件名稱前要加on。可以對(duì)此做個(gè)兼容性處理。
DOM 3級(jí)事件
在DOM 2級(jí)事件的基礎(chǔ)上添加了更多的事件類型。
- UI事件,當(dāng)用戶預(yù)頁面的元素交互時(shí)觸發(fā),如:load、scroll
- 焦點(diǎn)事件,當(dāng)元素獲得或失去焦點(diǎn)時(shí)觸發(fā),如:blur、focus
- 鼠標(biāo)事件,當(dāng)用戶通過誰啊哦在頁面執(zhí)行操作時(shí)觸發(fā)如:dblclick、mouseup
- 滾輪事件,當(dāng)使用鼠標(biāo)滾輪或類似設(shè)備時(shí)觸發(fā),如:mousewheel
- 文本事件,當(dāng)在文檔中輸入文本時(shí)觸發(fā),如:textinput
- 鍵盤事件,當(dāng)用戶通過鍵盤在頁面上執(zhí)行操作時(shí)觸發(fā),如:keydown、keypress
- 合成事件,當(dāng)為IME(輸入法編輯器)輸入字符時(shí)觸發(fā),如:compositionstart
- 變動(dòng)事件,當(dāng)?shù)讓覦OM結(jié)構(gòu)發(fā)生變化時(shí)觸發(fā),如:DOMsubtreeModified。
- 同時(shí)DOM 3級(jí)事件也允許使用者自定義一些事件。
總結(jié):
- DOM2級(jí)的好處是可以添加多個(gè)事件處理程序;DOM0級(jí)對(duì)每個(gè)事件支持持一個(gè)事件處理程序;
- 通過DOM2級(jí)添加的匿名函數(shù)無法移除,
addEventListener和removeEventListener的handler必須同名 - 作用域:DOM 0的
handler會(huì)在所屬元素的作用域內(nèi)運(yùn)行,IE的handler會(huì)在全局作用域運(yùn)行,this === window - 觸發(fā)順序:添加多個(gè)事件時(shí),DOM2會(huì)按照添加順序執(zhí)行,IE會(huì)以相反的順序執(zhí)行
3.事件觸發(fā)過程
事件流描述了頁面接收事件的順序?,F(xiàn)代瀏覽器事件流包含三個(gè)過程,分別是捕獲階段、目標(biāo)階段和冒泡階段。
下面詳細(xì)地講解這三個(gè)過程。
3.1捕獲階段
當(dāng)我們對(duì) DOM元素進(jìn)行操作時(shí),比如鼠標(biāo)點(diǎn)擊、懸浮等,就會(huì)有一個(gè)事件傳輸?shù)竭@個(gè)DOM元素,這個(gè)事件從Window開始,依次經(jīng)過document、html、body,再不斷經(jīng)過過子節(jié)點(diǎn)直到到達(dá)目標(biāo)元素,從 Window到達(dá)目標(biāo)元素父節(jié)點(diǎn)的過程稱為捕獲階段,注意此時(shí)還未到達(dá)目標(biāo)節(jié)點(diǎn)。
3.2目標(biāo)階段
捕獲階段結(jié)束時(shí),事件到達(dá)了目標(biāo)節(jié)點(diǎn)的父節(jié)點(diǎn),最終到達(dá)目標(biāo)節(jié)點(diǎn),并在目標(biāo)節(jié)點(diǎn)上觸發(fā)了這個(gè)時(shí)間,這就是目標(biāo)階段。
需要注意的是,事件觸發(fā)的目標(biāo)節(jié)點(diǎn)為最底層的節(jié)點(diǎn)。比如下面這個(gè)例子:
<div>
<p>
你猜,目標(biāo)在這里還是<span>哪里</span>
</p>
</div>
當(dāng)我們點(diǎn)擊“哪里“的時(shí)候,目標(biāo)節(jié)點(diǎn)是<span></span>,點(diǎn)擊這里的時(shí)候,目標(biāo)節(jié)點(diǎn)是<p></p>,而當(dāng)我們點(diǎn)擊<p></p>區(qū)域之外,<div></div>區(qū)域之內(nèi),目標(biāo)節(jié)點(diǎn)就是<div></div>。
3.3 冒泡階段
當(dāng)事件到達(dá)目標(biāo)節(jié)點(diǎn)之后,就會(huì)沿著原路返回,這個(gè)過程有點(diǎn)類似于水泡從水底浮出水面的過程,所以稱這個(gè)過程為冒泡階段。
現(xiàn)在再看addEventListener(eventName, handler, useCapture)函數(shù)。第三個(gè)參數(shù)是useCapture,代表是否在捕獲階段進(jìn)行事件處理,如果是false,則在冒泡階段進(jìn)行事件處理,如果是true則在捕獲階段進(jìn)行處理,默認(rèn)是false。
冒泡事件的流程剛好是事件捕獲的逆過程。我們來看個(gè)事件冒泡的例子:
<div id="outer">
<div id="inner">
</div>
</div>
<script>
window.onclick = function() {
console.log('window');
};
document.onclick = function() {
console.log('document');
};
document.documentElement.onclick = function() {
console.log('html');
};
document.body.onclick = function() {
console.log('body');
};
outer.onclick = function(ev) {
console.log('outer');
};
inner.onclick = function(ev) {
console.log('inner');
}
</script>
// 輸出為 inner outter body html dcoument window
4.事件委托
JavaScript中,事件的委托表示給元素的父級(jí)或者祖級(jí),甚至頁面,由他們來綁定事件,然后利用事件冒泡的基本原理,通過事件目標(biāo)對(duì)象進(jìn)行檢測(cè),然后執(zhí)行相關(guān)操作。
事件委托有兩個(gè)優(yōu)點(diǎn):
- 減少內(nèi)存消耗,提高性能
假設(shè)有一個(gè)列表,列表之中有大量的列表項(xiàng),我們需要在點(diǎn)擊每個(gè)列表項(xiàng)的時(shí)候響應(yīng)一個(gè)事件。
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
如果給每個(gè)列表項(xiàng)都一一綁定函數(shù),那么對(duì)內(nèi)存的消耗是非常大的,需要消耗更多性能。借助事件委托,我們只需要給父容器ul綁定方法即可,這樣不管點(diǎn)擊哪一個(gè)后代元素,都會(huì)根據(jù)冒泡傳播的傳遞機(jī)制,把容器的click行為處罰,然后把對(duì)應(yīng)的方法執(zhí)行,根據(jù)事件源,我們可以知道點(diǎn)擊的是誰,從而完成不同的事。
- 動(dòng)態(tài)綁定事件
在很多時(shí)候,我們需要通過用戶操作動(dòng)態(tài)的增刪列表項(xiàng)元素,如果一開始給每個(gè)子元素綁定事件,那么在列表發(fā)生變化時(shí),就需要從新給新增的元素綁定事件,給即將刪去的元素解綁事件,如果用事件委托就會(huì)省去很多這樣的麻煩。
接下來我們來實(shí)現(xiàn)上例中父元素#list下的li元素的事件委托到他的父層元素上:
// 給父層元素綁定事件
document.getElementById('list').addEventListener('click', function(e) {
// 兼容性處理
var event = e || window.event;
var target = event.target || event.srcElement;
// 判斷是否匹配目標(biāo)元素
if (target.nodeName.toLocaleLowerCase === 'li') {
console.log('the content is:', target.innerHTML);
}
});
5.事件對(duì)象
DOM0和DOM2的事件處理程序都會(huì)自動(dòng)傳入event對(duì)象,即觸發(fā)DOM上的某個(gè)事件時(shí),會(huì)產(chǎn)生一個(gè)事件對(duì)象,里面包含著所有和事件有關(guān)的信息。IE中的event對(duì)象取決于指定的事件處理程序的方法。
IE的
handler會(huì)在全局作用域運(yùn)行,this === window所以在IE中會(huì)有window.event、event兩種情況。
另外在IE中,事件對(duì)象的屬性也不一樣,對(duì)應(yīng)關(guān)系如下:
srcElement => target returnValue => preventDefault() cancelBubble => stopPropagation() IE不支持事件捕獲,因而只能取消事件冒泡,但stopPropagtion可以同時(shí)取消事件捕獲和冒泡。
只有在事件處理程序期間,
event對(duì)象才會(huì)存在,一旦事件處理程序執(zhí)行完成,event對(duì)象就會(huì)被銷毀。
1、event.preventDefault()
如果調(diào)用這個(gè)方法,默認(rèn)事件行為將不在觸發(fā)。什么是默認(rèn)事件呢?例如表單 - 點(diǎn)擊提交按鈕跳轉(zhuǎn)頁面、a標(biāo)簽?zāi)J(rèn)頁面跳轉(zhuǎn)或是錨點(diǎn)定位等。
很多時(shí)候我們使用a標(biāo)簽僅僅是想當(dāng)做一個(gè)普通的按鈕,點(diǎn)擊實(shí)現(xiàn)一個(gè)功能,不想頁面跳轉(zhuǎn),也不想錨點(diǎn)定位。
// 方法一
<a href="javascript:;">鏈接</a>
也可以通過JS方法來阻止,給其click事件綁定方法,當(dāng)我們點(diǎn)擊A標(biāo)簽的時(shí)候,先觸發(fā)click事件,其次才會(huì)執(zhí)行自己的默認(rèn)行為
// 方法二
<a id="test" >鏈接</a>
<script>
test.onclick = function(e) {
e = e || window.event;
return false;
}
</script>
// 方法三
<a id="test" >鏈接</a>
<script>
test.onclick = function(e) {
e = e || window.event;
e.preventDefalut();
}
</script>
接下來我們看個(gè)例子:輸入框最多只能輸入六個(gè)字符,如何實(shí)現(xiàn)?
// 例子5
<input type="text" id="tempInp">
<script>
tempInp.onkeydown = function(ev) {
ev = ev || window.event;
let val = this.value.trim();
let len = val.lenght;
if (len >= 6) {
this.value = val.substr(0, 6);
//阻止默認(rèn)行為去除特殊按鍵(DELETE\BACK-SPACE\方向鍵...)
let code = ev.which || ev.keyCode;
if (!/^(46|8|37|38|39|40)$/.test(code)) {
ev.preventDefault()
}
}
}
</script>
2.event.stopPropagation() & event.stopImmediatePropagation()
event.stopPropagation()方法阻止事件冒泡到父元素,阻止任何父事件處理程序被執(zhí)行(一般我們認(rèn)為stopPropagation是用來阻止事件冒泡的,其實(shí)該函數(shù)也可以阻止捕獲事件)。上面提到事件冒泡階段是指事件從目標(biāo)節(jié)點(diǎn)紫霞而上的向window對(duì)象傳播的階段。我們?cè)谏厦娴睦又械?code>inner元素click事件上,添加event.stopPropagation()這句話后,就阻止了父事件的執(zhí)行,最后只打印了'inner'。
inner.onclick = function(ev) {
console.log('inner');
ev.stopPropagation();
}
stopImmediatePropagation 既能阻止事件向父元素冒泡,也能阻止元素同事件類型的其它監(jiān)聽器被觸發(fā)。而 stopPropagation 只能實(shí)現(xiàn)前者的效果。我們來看個(gè)例子:
<body>
<button id="btn">click me to stop propagation</button>
</body>
<script>
const btn = document.querySelector('#btn');
btn.addEventListener('click', event => {
console.log('btn click 1');
event.stopImmediatePropagation();
});
btn.addEventListener('click', event => {
console.log('btn click 2');
});
document.body.addEventListener('click', () => {
console.log('body click');
});
// btn click 1
</script>
如上所示,使用stopImmediatePropagation后,點(diǎn)擊按鈕時(shí),不僅body綁定事件不會(huì)觸發(fā),與此同時(shí)按鈕的另一個(gè)點(diǎn)擊事件也不觸發(fā)。
event.target & event.currentTarget
event.target指向引起觸發(fā)事件的元素,而event.currentTarget則是事件綁定的元素,只有被點(diǎn)擊的那個(gè)目標(biāo)元素的event.target才會(huì)等于event.currentTarget。也就是說,event.currentTarget始終是監(jiān)聽事件這,而event.target是事件的真正發(fā)出者。
6.捕獲與冒泡的順序問題
當(dāng)有很多層交互嵌套時(shí),事件捕獲和時(shí)間冒泡的先后順序看起來是不好確定的。下面分5種情況討論給它們的順序,以及如何規(guī)避意外情況的發(fā)生。
1、在外層div注冊(cè)事件,點(diǎn)擊內(nèi)層div來觸發(fā)事件時(shí),捕獲事件總是要比冒泡事件先觸發(fā)(與代碼順序無關(guān))
假設(shè),有這樣的html結(jié)構(gòu):
<div id="test" class="test">
<div id="testInner" class="test-inner">
</div>
</div>
然后,我們?cè)谕鈱觗iv上注冊(cè)兩個(gè)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)
點(diǎn)擊內(nèi)層的div,先彈出capture is ok,后彈出bubble is ok。只有當(dāng)真正觸發(fā)事件的 DOM元素是內(nèi)層時(shí),外層DOM元素才有機(jī)會(huì)模擬捕獲事件和冒泡事件。
2、當(dāng)在觸發(fā)事件的DOM元素上注冊(cè)事件時(shí),那個(gè)先注冊(cè)就先執(zhí)行那個(gè)
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("caapture is ok");
}, true);
在本例中,冒泡事件先注冊(cè),所以先執(zhí)行。所以,點(diǎn)擊內(nèi)層div,先彈出bubble is ok,再彈出caputre is ok。
3、當(dāng)外層div和內(nèi)層div同時(shí)注冊(cè)了捕獲事件時(shí),點(diǎn)擊內(nèi)層的div時(shí),外層div的事件一定會(huì)先觸發(fā)
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 的事件注冊(cè)在后面,但會(huì)先觸發(fā)。所以,結(jié)果是先彈出 outer capture is ok,再彈出 inner capture is ok。
4、同理,當(dāng)外層div和內(nèi)層div都同時(shí)注冊(cè)了冒泡事件,點(diǎn)擊內(nèi)層div時(shí),一定是內(nèi)層div事件先觸發(fā)。
const btn = document.getElementById("test");
const btnInner = document.getElementById("testInner");
btn.addEventLisntener("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ā)
通常情況下,我們都希望點(diǎn)擊某個(gè)div時(shí),就觸發(fā)自己的事件回調(diào)。比如,明明點(diǎn)擊的是內(nèi)層div,但是外層div的事件也觸發(fā)了,這就不是我們想要的了。這時(shí),就需要阻止事件的派發(fā)。
事件觸發(fā)時(shí),會(huì)默認(rèn)傳入一個(gè)event對(duì)象,這個(gè)event對(duì)象上有一個(gè)方法:stopPropagation。MDN上的解釋是:阻止捕獲和冒泡階段中,當(dāng)前事件的進(jìn)一步傳播。所以通過此方法,讓外層div接收不到事件,自然也就不會(huì)觸發(fā)了。
btnInner.addEventListener("click", function(e) {
// 阻止冒泡
e.stopPropagation();
alert(”inner bubble is ok“);
}, false);