資源事件
beforeunload 事件
beforeunload
事件在窗口、文檔、各種資源將要卸載前觸發(fā)。它可以用來防止用戶不小心卸載資源。
如果該事件對(duì)象的returnValue
屬性是一個(gè)非空字符串,那么瀏覽器就會(huì)彈出一個(gè)對(duì)話框,詢問用戶是否要卸載該資源。但是,用戶指定的字符串可能無法顯示,瀏覽器會(huì)展示預(yù)定義的字符串。如果用戶點(diǎn)擊“取消”按鈕,資源就不會(huì)卸載。
window.addEventListener('beforeunload', function (event) {
event.returnValue = '你確定離開嗎?';
});
上面代碼中,用戶如果關(guān)閉窗口,瀏覽器會(huì)彈出一個(gè)窗口,要求用戶確認(rèn)。
瀏覽器對(duì)這個(gè)事件的行為很不一致,有的瀏覽器調(diào)用event.preventDefault()
,也會(huì)彈出對(duì)話框。IE 瀏覽器需要顯式返回一個(gè)非空的字符串,才會(huì)彈出對(duì)話框。而且,大多數(shù)瀏覽器在對(duì)話框中不顯示指定文本,只顯示默認(rèn)文本。因此,可以采用下面的寫法,取得最大的兼容性。
window.addEventListener('beforeunload', function (e) {
var confirmationMessage = '確認(rèn)關(guān)閉窗口?';
e.returnValue = confirmationMessage;
return confirmationMessage;
});
注意,許多手機(jī)瀏覽器(比如 Safari)默認(rèn)忽略這個(gè)事件,桌面瀏覽器也有辦法忽略這個(gè)事件。所以,它可能根本不會(huì)生效,不能依賴它來阻止用戶關(guān)閉瀏覽器窗口,最好不要使用這個(gè)事件。
另外,一旦使用了beforeunload
事件,瀏覽器就不會(huì)緩存當(dāng)前網(wǎng)頁,使用“回退”按鈕將重新向服務(wù)器請(qǐng)求網(wǎng)頁。這是因?yàn)楸O(jiān)聽這個(gè)事件的目的,一般是為了網(wǎng)頁狀態(tài),這時(shí)緩存頁面的初始狀態(tài)就沒意義了。
unload 事件
unload
事件在窗口關(guān)閉或者document
對(duì)象將要卸載時(shí)觸發(fā)。它的觸發(fā)順序排在beforeunload
、pagehide
事件后面。
unload
事件發(fā)生時(shí),文檔處于一個(gè)特殊狀態(tài)。所有資源依然存在,但是對(duì)用戶來說都不可見,UI 互動(dòng)全部無效。這個(gè)事件是無法取消的,即使在監(jiān)聽函數(shù)里面拋出錯(cuò)誤,也不能停止文檔的卸載。
window.addEventListener('unload', function(event) {
console.log('文檔將要卸載');
});
手機(jī)上,瀏覽器或系統(tǒng)可能會(huì)直接丟棄網(wǎng)頁,這時(shí)該事件根本不會(huì)發(fā)生。而且跟beforeunload
事件一樣,一旦使用了unload
事件,瀏覽器就不會(huì)緩存當(dāng)前網(wǎng)頁,理由同上。因此,任何情況下都不應(yīng)該依賴這個(gè)事件,指定網(wǎng)頁卸載時(shí)要執(zhí)行的代碼,可以考慮完全不使用這個(gè)事件。
該事件可以用pagehide
代替。
load 事件,error 事件
load
事件在頁面或某個(gè)資源加載成功時(shí)觸發(fā)。注意,頁面或資源從瀏覽器緩存加載,并不會(huì)觸發(fā)load
事件。
window.addEventListener('load', function(event) {
console.log('所有資源都加載完成');
});
error
事件是在頁面或資源加載失敗時(shí)觸發(fā)。abort
事件在用戶取消加載時(shí)觸發(fā)。
這三個(gè)事件實(shí)際上屬于進(jìn)度事件,不僅發(fā)生在document
對(duì)象,還發(fā)生在各種外部資源上面。瀏覽網(wǎng)頁就是一個(gè)加載各種資源的過程,圖像(image)、樣式表(style sheet)、腳本(script)、視頻(video)、音頻(audio)、Ajax請(qǐng)求(XMLHttpRequest)等等。這些資源和document
對(duì)象、window
對(duì)象、XMLHttpRequestUpload 對(duì)象,都會(huì)觸發(fā)load
事件和error
事件。
最后,頁面的load
事件也可以用pageshow
事件代替。
session 歷史事件
pageshow 事件,pagehide 事件
默認(rèn)情況下,瀏覽器會(huì)在當(dāng)前會(huì)話(session)緩存頁面,當(dāng)用戶點(diǎn)擊“前進(jìn)/后退”按鈕時(shí),瀏覽器就會(huì)從緩存中加載頁面。
pageshow
事件在頁面加載時(shí)觸發(fā),包括第一次加載和從緩存加載兩種情況。如果要指定頁面每次加載(不管是不是從瀏覽器緩存)時(shí)都運(yùn)行的代碼,可以放在這個(gè)事件的監(jiān)聽函數(shù)。
第一次加載時(shí),它的觸發(fā)順序排在load
事件后面。從緩存加載時(shí),load
事件不會(huì)觸發(fā),因?yàn)榫W(wǎng)頁在緩存中的樣子通常是load
事件的監(jiān)聽函數(shù)運(yùn)行后的樣子,所以不必重復(fù)執(zhí)行。同理,如果是從緩存中加載頁面,網(wǎng)頁內(nèi)初始化的 JavaScript 腳本(比如 DOMContentLoaded 事件的監(jiān)聽函數(shù))也不會(huì)執(zhí)行。
window.addEventListener('pageshow', function(event) {
console.log('pageshow: ', event);
});
pageshow
事件有一個(gè)persisted
屬性,返回一個(gè)布爾值。頁面第一次加載時(shí),這個(gè)屬性是false
;當(dāng)頁面從緩存加載時(shí),這個(gè)屬性是true
。
window.addEventListener('pageshow', function(event){
if (event.persisted) {
// ...
}
});
pagehide
事件與pageshow
事件類似,當(dāng)用戶通過“前進(jìn)/后退”按鈕,離開當(dāng)前頁面時(shí)觸發(fā)。它與 unload 事件的區(qū)別在于,如果在 window 對(duì)象上定義unload
事件的監(jiān)聽函數(shù)之后,頁面不會(huì)保存在緩存中,而使用pagehide
事件,頁面會(huì)保存在緩存中。
pagehide
事件實(shí)例也有一個(gè)persisted
屬性,將這個(gè)屬性設(shè)為true
,就表示頁面要保存在緩存中;設(shè)為false
,表示網(wǎng)頁不保存在緩存中,這時(shí)如果設(shè)置了unload 事件的監(jiān)聽函數(shù),該函數(shù)將在 pagehide 事件后立即運(yùn)行。
如果頁面包含<frame>
或<iframe>
元素,則<frame>
頁面的pageshow
事件和pagehide
事件,都會(huì)在主頁面之前觸發(fā)。
注意,這兩個(gè)事件只在瀏覽器的history
對(duì)象發(fā)生變化時(shí)觸發(fā),跟網(wǎng)頁是否可見沒有關(guān)系。
popstate 事件
popstate
事件在瀏覽器的history
對(duì)象的當(dāng)前記錄發(fā)生顯式切換時(shí)觸發(fā)。注意,調(diào)用history.pushState()
或history.replaceState()
,并不會(huì)觸發(fā)popstate
事件。該事件只在用戶在history
記錄之間顯式切換時(shí)觸發(fā),比如鼠標(biāo)點(diǎn)擊“后退/前進(jìn)”按鈕,或者在腳本中調(diào)用history.back()
、history.forward()
、history.go()
時(shí)觸發(fā)。
該事件對(duì)象有一個(gè)state
屬性,保存history.pushState
方法和history.replaceState
方法為當(dāng)前記錄添加的state
對(duì)象。
window.onpopstate = function (event) {
console.log('state: ' + event.state);
};
history.pushState({page: 1}, 'title 1', '?page=1');
history.pushState({page: 2}, 'title 2', '?page=2');
history.replaceState({page: 3}, 'title 3', '?page=3');
history.back(); // state: {"page":1}
history.back(); // state: null
history.go(2); // state: {"page":3}
上面代碼中,pushState
方法向history
添加了兩條記錄,然后replaceState
方法替換掉當(dāng)前記錄。因此,連續(xù)兩次back
方法,會(huì)讓當(dāng)前條目退回到原始網(wǎng)址,它沒有附帶state
對(duì)象,所以事件的state
屬性為null
,然后前進(jìn)兩條記錄,又回到replaceState
方法添加的記錄。
瀏覽器對(duì)于頁面首次加載,是否觸發(fā)popstate
事件,處理不一樣,F(xiàn)irefox 不觸發(fā)該事件。
hashchange 事件
hashchange
事件在 URL 的 hash 部分(即#
號(hào)后面的部分,包括#
號(hào))發(fā)生變化時(shí)觸發(fā)。該事件一般在window
對(duì)象上監(jiān)聽。
hashchange
的事件實(shí)例具有兩個(gè)特有屬性:oldURL
屬性和newURL
屬性,分別表示變化前后的完整 URL。
// URL 是 http://www.example.com/
window.addEventListener('hashchange', myFunction);
function myFunction(e) {
console.log(e.oldURL);
console.log(e.newURL);
}
location.hash = 'part2';
// http://www.example.com/
// http://www.example.com/#part2
網(wǎng)頁狀態(tài)事件
DOMContentLoaded 事件
網(wǎng)頁下載并解析完成以后,瀏覽器就會(huì)在document
對(duì)象上觸發(fā) DOMContentLoaded 事件。這時(shí),僅僅完成了網(wǎng)頁的解析(整張頁面的 DOM 生成了),所有外部資源(樣式表、腳本、iframe 等等)可能還沒有下載結(jié)束。也就是說,這個(gè)事件比load
事件,發(fā)生時(shí)間早得多。
document.addEventListener('DOMContentLoaded', function (event) {
console.log('DOM生成');
});
注意,網(wǎng)頁的 JavaScript 腳本是同步執(zhí)行的,腳本一旦發(fā)生堵塞,將推遲觸發(fā)DOMContentLoaded
事件。
document.addEventListener('DOMContentLoaded', function (event) {
console.log('DOM 生成');
});
// 這段代碼會(huì)推遲觸發(fā) DOMContentLoaded 事件
for(var i = 0; i < 1000000000; i++) {
// ...
}
readystatechange 事件
readystatechange
事件當(dāng) Document 對(duì)象和 XMLHttpRequest 對(duì)象的readyState
屬性發(fā)生變化時(shí)觸發(fā)。document.readyState
有三個(gè)可能的值:loading
(網(wǎng)頁正在加載)、interactive
(網(wǎng)頁已經(jīng)解析完成,但是外部資源仍然處在加載狀態(tài))和complete
(網(wǎng)頁和所有外部資源已經(jīng)結(jié)束加載,load
事件即將觸發(fā))。
document.onreadystatechange = function () {
if (document.readyState === 'interactive') {
// ...
}
}
這個(gè)事件可以看作DOMContentLoaded
事件的另一種實(shí)現(xiàn)方法。
窗口事件
scroll 事件
scroll
事件在文檔或文檔元素滾動(dòng)時(shí)觸發(fā),主要出現(xiàn)在用戶拖動(dòng)滾動(dòng)條。
window.addEventListener('scroll', callback);
該事件會(huì)連續(xù)地大量觸發(fā),所以它的監(jiān)聽函數(shù)之中不應(yīng)該有非常耗費(fèi)計(jì)算的操作。推薦的做法是使用requestAnimationFrame
或setTimeout
控制該事件的觸發(fā)頻率,然后可以結(jié)合customEvent
拋出一個(gè)新事件。
(function () {
var throttle = function (type, name, obj) {
var obj = obj || window;
var running = false;
var func = function () {
if (running) { return; }
running = true;
requestAnimationFrame(function() {
obj.dispatchEvent(new CustomEvent(name));
running = false;
});
};
obj.addEventListener(type, func);
};
// 將 scroll 事件轉(zhuǎn)為 optimizedScroll 事件
throttle('scroll', 'optimizedScroll');
})();
window.addEventListener('optimizedScroll', function() {
console.log('Resource conscious scroll callback!');
});
上面代碼中,throttle()
函數(shù)用于控制事件觸發(fā)頻率,它有一個(gè)內(nèi)部函數(shù)func()
,每次scroll
事件實(shí)際上觸發(fā)的是這個(gè)函數(shù)。func()
函數(shù)內(nèi)部使用requestAnimationFrame()
方法,保證只有每次頁面重繪時(shí)(每秒60次),才可能會(huì)觸發(fā)optimizedScroll
事件,從而實(shí)際上將scroll
事件轉(zhuǎn)換為optimizedScroll
事件,觸發(fā)頻率被控制在每秒最多60次。
改用setTimeout()
方法,可以放置更大的時(shí)間間隔。
(function() {
window.addEventListener('scroll', scrollThrottler, false);
var scrollTimeout;
function scrollThrottler() {
if (!scrollTimeout) {
scrollTimeout = setTimeout(function () {
scrollTimeout = null;
actualScrollHandler();
}, 66);
}
}
function actualScrollHandler() {
// ...
}
}());
上面代碼中,每次scroll
事件都會(huì)執(zhí)行scrollThrottler
函數(shù)。該函數(shù)里面有一個(gè)定時(shí)器setTimeout
,每66毫秒觸發(fā)一次(每秒15次)真正執(zhí)行的任務(wù)actualScrollHandler
。
下面是一個(gè)更一般的throttle
函數(shù)的寫法。
function throttle(fn, wait) {
var time = Date.now();
return function() {
if ((time + wait - Date.now()) < 0) {
fn();
time = Date.now();
}
}
}
window.addEventListener('scroll', throttle(callback, 1000));
上面的代碼將scroll
事件的觸發(fā)頻率,限制在一秒一次。
lodash
函數(shù)庫提供了現(xiàn)成的throttle
函數(shù),可以直接使用。
window.addEventListener('scroll', _.throttle(callback, 1000));
本書前面介紹過debounce
的概念,throttle
與它區(qū)別在于,throttle
是“節(jié)流”,確保一段時(shí)間內(nèi)只執(zhí)行一次,而debounce
是“防抖”,要連續(xù)操作結(jié)束后再執(zhí)行。以網(wǎng)頁滾動(dòng)為例,debounce
要等到用戶停止?jié)L動(dòng)后才執(zhí)行,throttle
則是如果用戶一直在滾動(dòng)網(wǎng)頁,那么在滾動(dòng)過程中還是會(huì)執(zhí)行。
resize 事件
resize
事件在改變?yōu)g覽器窗口大小時(shí)觸發(fā),主要發(fā)生在window
對(duì)象上面。
var resizeMethod = function () {
if (document.body.clientWidth < 768) {
console.log('移動(dòng)設(shè)備的視口');
}
};
window.addEventListener('resize', resizeMethod, true);
該事件也會(huì)連續(xù)地大量觸發(fā),所以最好像上面的scroll
事件一樣,通過throttle
函數(shù)控制事件觸發(fā)頻率。
fullscreenchange 事件,fullscreenerror 事件
fullscreenchange
事件在進(jìn)入或退出全屏狀態(tài)時(shí)觸發(fā),該事件發(fā)生在document
對(duì)象上面。
document.addEventListener('fullscreenchange', function (event) {
console.log(document.fullscreenElement);
});
fullscreenerror
事件在瀏覽器無法切換到全屏狀態(tài)時(shí)觸發(fā)。
剪貼板事件
以下三個(gè)事件屬于剪貼板操作的相關(guān)事件。
cut
:將選中的內(nèi)容從文檔中移除,加入剪貼板時(shí)觸發(fā)。copy
:進(jìn)行復(fù)制動(dòng)作時(shí)觸發(fā)。paste
:剪貼板內(nèi)容粘貼到文檔后觸發(fā)。
舉例來說,如果希望禁止輸入框的粘貼事件,可以使用下面的代碼。
inputElement.addEventListener('paste', e => e.preventDefault());
上面的代碼使得用戶無法在<input>
輸入框里面粘貼內(nèi)容。
cut
、copy
、paste
這三個(gè)事件的事件對(duì)象都是ClipboardEvent
接口的實(shí)例。ClipboardEvent
有一個(gè)實(shí)例屬性clipboardData
,是一個(gè) DataTransfer 對(duì)象,存放剪貼的數(shù)據(jù)。具體的 API 接口和操作方法,請(qǐng)參見《拖拉事件》的 DataTransfer 對(duì)象部分。
document.addEventListener('copy', function (e) {
e.clipboardData.setData('text/plain', 'Hello, world!');
e.clipboardData.setData('text/html', '<b>Hello, world!</b>');
e.preventDefault();
});
上面的代碼使得復(fù)制進(jìn)入剪貼板的,都是開發(fā)者指定的數(shù)據(jù),而不是用戶想要拷貝的數(shù)據(jù)。
焦點(diǎn)事件
焦點(diǎn)事件發(fā)生在元素節(jié)點(diǎn)和document
對(duì)象上面,與獲得或失去焦點(diǎn)相關(guān)。它主要包括以下四個(gè)事件。
focus
:元素節(jié)點(diǎn)獲得焦點(diǎn)后觸發(fā),該事件不會(huì)冒泡。blur
:元素節(jié)點(diǎn)失去焦點(diǎn)后觸發(fā),該事件不會(huì)冒泡。focusin
:元素節(jié)點(diǎn)將要獲得焦點(diǎn)時(shí)觸發(fā),發(fā)生在focus
事件之前。該事件會(huì)冒泡。focusout
:元素節(jié)點(diǎn)將要失去焦點(diǎn)時(shí)觸發(fā),發(fā)生在blur
事件之前。該事件會(huì)冒泡。
這四個(gè)事件的事件對(duì)象都繼承了FocusEvent
接口。FocusEvent
實(shí)例具有以下屬性。
FocusEvent.target
:事件的目標(biāo)節(jié)點(diǎn)。FocusEvent.relatedTarget
:對(duì)于focusin
事件,返回失去焦點(diǎn)的節(jié)點(diǎn);對(duì)于focusout
事件,返回將要接受焦點(diǎn)的節(jié)點(diǎn);對(duì)于focus
和blur
事件,返回null
。
由于focus
和blur
事件不會(huì)冒泡,只能在捕獲階段觸發(fā),所以addEventListener
方法的第三個(gè)參數(shù)需要設(shè)為true
。
form.addEventListener('focus', function (event) {
event.target.style.background = 'pink';
}, true);
form.addEventListener('blur', function (event) {
event.target.style.background = '';
}, true);
上面代碼針對(duì)表單的文本輸入框,接受焦點(diǎn)時(shí)設(shè)置背景色,失去焦點(diǎn)時(shí)去除背景色。
CustomEvent 接口
CustomEvent 接口用于生成自定義的事件實(shí)例。那些瀏覽器預(yù)定義的事件,雖然可以手動(dòng)生成,但是往往不能在事件上綁定數(shù)據(jù)。如果需要在觸發(fā)事件的同時(shí),傳入指定的數(shù)據(jù),就可以使用 CustomEvent 接口生成的自定義事件對(duì)象。
瀏覽器原生提供CustomEvent()
構(gòu)造函數(shù),用來生成 CustomEvent 事件實(shí)例。
new CustomEvent(type, options)
CustomEvent()
構(gòu)造函數(shù)接受兩個(gè)參數(shù)。第一個(gè)參數(shù)是字符串,表示事件的名字,這是必須的。第二個(gè)參數(shù)是事件的配置對(duì)象,這個(gè)參數(shù)是可選的。CustomEvent
的配置對(duì)象除了接受 Event 事件的配置屬性,只有一個(gè)自己的屬性。
detail
:表示事件的附帶數(shù)據(jù),默認(rèn)為null
。
下面是一個(gè)例子。
var event = new CustomEvent('build', { 'detail': 'hello' });
function eventHandler(e) {
console.log(e.detail);
}
document.body.addEventListener('build', function (e) {
console.log(e.detail);
});
document.body.dispatchEvent(event);
上面代碼中,我們手動(dòng)定義了build
事件。該事件觸發(fā)后,會(huì)被監(jiān)聽到,從而輸出該事件實(shí)例的detail
屬性(即字符串hello
)。
下面是另一個(gè)例子。
var myEvent = new CustomEvent('myevent', {
detail: {
foo: 'bar'
},
bubbles: true,
cancelable: false
});
el.addEventListener('myevent', function (event) {
console.log('Hello ' + event.detail.foo);
});
el.dispatchEvent(myEvent);
上面代碼也說明,CustomEvent 的事件實(shí)例,除了具有 Event 接口的實(shí)例屬性,還具有detail
屬性。
更多建議: