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