# 其他常見事件
## 資源事件
### beforeunload 事件
`beforeunload`事件在窗口、文檔、各種資源將要卸載前觸發。它可以用來防止用戶不小心卸載資源。
如果該事件對象的`returnValue`屬性是一個非空字符串,那么瀏覽器就會彈出一個對話框,詢問用戶是否要卸載該資源。但是,用戶指定的字符串可能無法顯示,瀏覽器會展示預定義的字符串。如果用戶點擊“取消”按鈕,資源就不會卸載。
```javascript
window.addEventListener('beforeunload', function (event) {
event.returnValue = '你確定離開嗎?';
});
```
上面代碼中,用戶如果關閉窗口,瀏覽器會彈出一個窗口,要求用戶確認。
瀏覽器對這個事件的行為很不一致,有的瀏覽器調用`event.preventDefault()`,也會彈出對話框。IE 瀏覽器需要顯式返回一個非空的字符串,才會彈出對話框。而且,大多數瀏覽器在對話框中不顯示指定文本,只顯示默認文本。因此,可以采用下面的寫法,取得最大的兼容性。
```javascript
window.addEventListener('beforeunload', function (e) {
var confirmationMessage = '確認關閉窗口?';
e.returnValue = confirmationMessage;
return confirmationMessage;
});
```
注意,許多手機瀏覽器(比如 Safari)默認忽略這個事件,桌面瀏覽器也有辦法忽略這個事件。所以,它可能根本不會生效,不能依賴它來阻止用戶關閉瀏覽器窗口,最好不要使用這個事件。
另外,一旦使用了`beforeunload`事件,瀏覽器就不會緩存當前網頁,使用“回退”按鈕將重新向服務器請求網頁。這是因為監聽這個事件的目的,一般是為了網頁狀態,這時緩存頁面的初始狀態就沒意義了。
### unload 事件
`unload`事件在窗口關閉或者`document`對象將要卸載時觸發。它的觸發順序排在`beforeunload`、`pagehide`事件后面。
`unload`事件發生時,文檔處于一個特殊狀態。所有資源依然存在,但是對用戶來說都不可見,UI 互動全部無效。這個事件是無法取消的,即使在監聽函數里面拋出錯誤,也不能停止文檔的卸載。
```javascript
window.addEventListener('unload', function(event) {
console.log('文檔將要卸載');
});
```
手機上,瀏覽器或系統可能會直接丟棄網頁,這時該事件根本不會發生。而且跟`beforeunload`事件一樣,一旦使用了`unload`事件,瀏覽器就不會緩存當前網頁,理由同上。因此,任何情況下都不應該依賴這個事件,指定網頁卸載時要執行的代碼,可以考慮完全不使用這個事件。
該事件可以用`pagehide`代替。
### load 事件,error 事件
`load`事件在頁面或某個資源加載成功時觸發。注意,頁面或資源從瀏覽器緩存加載,并不會觸發`load`事件。
```javascript
window.addEventListener('load', function(event) {
console.log('所有資源都加載完成');
});
```
`error`事件是在頁面或資源加載失敗時觸發。`abort`事件在用戶取消加載時觸發。
這三個事件實際上屬于進度事件,不僅發生在`document`對象,還發生在各種外部資源上面。瀏覽網頁就是一個加載各種資源的過程,圖像(image)、樣式表(style sheet)、腳本(script)、視頻(video)、音頻(audio)、Ajax請求(XMLHttpRequest)等等。這些資源和`document`對象、`window`對象、XMLHttpRequestUpload 對象,都會觸發`load`事件和`error`事件。
最后,頁面的`load`事件也可以用`pageshow`事件代替。
## session 歷史事件
### pageshow 事件,pagehide 事件
默認情況下,瀏覽器會在當前會話(session)緩存頁面,當用戶點擊“前進/后退”按鈕時,瀏覽器就會從緩存中加載頁面。
`pageshow`事件在頁面加載時觸發,包括第一次加載和從緩存加載兩種情況。如果要指定頁面每次加載(不管是不是從瀏覽器緩存)時都運行的代碼,可以放在這個事件的監聽函數。
第一次加載時,它的觸發順序排在`load`事件后面。從緩存加載時,`load`事件不會觸發,因為網頁在緩存中的樣子通常是`load`事件的監聽函數運行后的樣子,所以不必重復執行。同理,如果是從緩存中加載頁面,網頁內初始化的 JavaScript 腳本(比如 DOMContentLoaded 事件的監聽函數)也不會執行。
```javascript
window.addEventListener('pageshow', function(event) {
console.log('pageshow: ', event);
});
```
`pageshow`事件有一個`persisted`屬性,返回一個布爾值。頁面第一次加載時,這個屬性是`false`;當頁面從緩存加載時,這個屬性是`true`。
```javascript
window.addEventListener('pageshow', function(event){
if (event.persisted) {
// ...
}
});
```
`pagehide`事件與`pageshow`事件類似,當用戶通過“前進/后退”按鈕,離開當前頁面時觸發。它與 unload 事件的區別在于,如果在 window 對象上定義`unload`事件的監聽函數之后,頁面不會保存在緩存中,而使用`pagehide`事件,頁面會保存在緩存中。
`pagehide`事件實例也有一個`persisted`屬性,將這個屬性設為`true`,就表示頁面要保存在緩存中;設為`false`,表示網頁不保存在緩存中,這時如果設置了unload 事件的監聽函數,該函數將在 pagehide 事件后立即運行。
如果頁面包含`<frame>`或`<iframe>`元素,則`<frame>`頁面的`pageshow`事件和`pagehide`事件,都會在主頁面之前觸發。
注意,這兩個事件只在瀏覽器的`history`對象發生變化時觸發,跟網頁是否可見沒有關系。
### popstate 事件
`popstate`事件在瀏覽器的`history`對象的當前記錄發生顯式切換時觸發。注意,調用`history.pushState()`或`history.replaceState()`,并不會觸發`popstate`事件。該事件只在用戶在`history`記錄之間顯式切換時觸發,比如鼠標點擊“后退/前進”按鈕,或者在腳本中調用`history.back()`、`history.forward()`、`history.go()`時觸發。
該事件對象有一個`state`屬性,保存`history.pushState`方法和`history.replaceState`方法為當前記錄添加的`state`對象。
```javascript
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`方法替換掉當前記錄。因此,連續兩次`back`方法,會讓當前條目退回到原始網址,它沒有附帶`state`對象,所以事件的`state`屬性為`null`,然后前進兩條記錄,又回到`replaceState`方法添加的記錄。
瀏覽器對于頁面首次加載,是否觸發`popstate`事件,處理不一樣,Firefox 不觸發該事件。
### hashchange 事件
`hashchange`事件在 URL 的 hash 部分(即`#`號后面的部分,包括`#`號)發生變化時觸發。該事件一般在`window`對象上監聽。
`hashchange`的事件實例具有兩個特有屬性:`oldURL`屬性和`newURL`屬性,分別表示變化前后的完整 URL。
```javascript
// 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
```
## 網頁狀態事件
### DOMContentLoaded 事件
網頁下載并解析完成以后,瀏覽器就會在`document`對象上觸發 DOMContentLoaded 事件。這時,僅僅完成了網頁的解析(整張頁面的 DOM 生成了),所有外部資源(樣式表、腳本、iframe 等等)可能還沒有下載結束。也就是說,這個事件比`load`事件,發生時間早得多。
```javascript
document.addEventListener('DOMContentLoaded', function (event) {
console.log('DOM生成');
});
```
注意,網頁的 JavaScript 腳本是同步執行的,腳本一旦發生堵塞,將推遲觸發`DOMContentLoaded`事件。
```javascript
document.addEventListener('DOMContentLoaded', function (event) {
console.log('DOM 生成');
});
// 這段代碼會推遲觸發 DOMContentLoaded 事件
for(var i = 0; i < 1000000000; i++) {
// ...
}
```
### readystatechange 事件
`readystatechange`事件當 Document 對象和 XMLHttpRequest 對象的`readyState`屬性發生變化時觸發。`document.readyState`有三個可能的值:`loading`(網頁正在加載)、`interactive`(網頁已經解析完成,但是外部資源仍然處在加載狀態)和`complete`(網頁和所有外部資源已經結束加載,`load`事件即將觸發)。
```javascript
document.onreadystatechange = function () {
if (document.readyState === 'interactive') {
// ...
}
}
```
這個事件可以看作`DOMContentLoaded`事件的另一種實現方法。
## 窗口事件
### scroll 事件
`scroll`事件在文檔或文檔元素滾動時觸發,主要出現在用戶拖動滾動條。
```javascript
window.addEventListener('scroll', callback);
```
該事件會連續地大量觸發,所以它的監聽函數之中不應該有非常耗費計算的操作。推薦的做法是使用`requestAnimationFrame`或`setTimeout`控制該事件的觸發頻率,然后可以結合`customEvent`拋出一個新事件。
```javascript
(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 事件轉為 optimizedScroll 事件
throttle('scroll', 'optimizedScroll');
})();
window.addEventListener('optimizedScroll', function() {
console.log('Resource conscious scroll callback!');
});
```
上面代碼中,`throttle()`函數用于控制事件觸發頻率,它有一個內部函數`func()`,每次`scroll`事件實際上觸發的是這個函數。`func()`函數內部使用`requestAnimationFrame()`方法,保證只有每次頁面重繪時(每秒60次),才可能會觸發`optimizedScroll`事件,從而實際上將`scroll`事件轉換為`optimizedScroll`事件,觸發頻率被控制在每秒最多60次。
改用`setTimeout()`方法,可以放置更大的時間間隔。
```javascript
(function() {
window.addEventListener('scroll', scrollThrottler, false);
var scrollTimeout;
function scrollThrottler() {
if (!scrollTimeout) {
scrollTimeout = setTimeout(function () {
scrollTimeout = null;
actualScrollHandler();
}, 66);
}
}
function actualScrollHandler() {
// ...
}
}());
```
上面代碼中,每次`scroll`事件都會執行`scrollThrottler`函數。該函數里面有一個定時器`setTimeout`,每66毫秒觸發一次(每秒15次)真正執行的任務`actualScrollHandler`。
下面是一個更一般的`throttle`函數的寫法。
```javascript
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`事件的觸發頻率,限制在一秒一次。
`lodash`函數庫提供了現成的`throttle`函數,可以直接使用。
```javascript
window.addEventListener('scroll', _.throttle(callback, 1000));
```
本書前面介紹過`debounce`的概念,`throttle`與它區別在于,`throttle`是“節流”,確保一段時間內只執行一次,而`debounce`是“防抖”,要連續操作結束后再執行。以網頁滾動為例,`debounce`要等到用戶停止滾動后才執行,`throttle`則是如果用戶一直在滾動網頁,那么在滾動過程中還是會執行。
### resize 事件
`resize`事件在改變瀏覽器窗口大小時觸發,主要發生在`window`對象上面。
```javascript
var resizeMethod = function () {
if (document.body.clientWidth < 768) {
console.log('移動設備的視口');
}
};
window.addEventListener('resize', resizeMethod, true);
```
該事件也會連續地大量觸發,所以最好像上面的`scroll`事件一樣,通過`throttle`函數控制事件觸發頻率。
### fullscreenchange 事件,fullscreenerror 事件
`fullscreenchange`事件在進入或退出全屏狀態時觸發,該事件發生在`document`對象上面。
```javascript
document.addEventListener('fullscreenchange', function (event) {
console.log(document.fullscreenElement);
});
```
`fullscreenerror`事件在瀏覽器無法切換到全屏狀態時觸發。
## 剪貼板事件
以下三個事件屬于剪貼板操作的相關事件。
- `cut`:將選中的內容從文檔中移除,加入剪貼板時觸發。
- `copy`:進行復制動作時觸發。
- `paste`:剪貼板內容粘貼到文檔后觸發。
舉例來說,如果希望禁止輸入框的粘貼事件,可以使用下面的代碼。
```javascript
inputElement.addEventListener('paste', e => e.preventDefault());
```
上面的代碼使得用戶無法在`<input>`輸入框里面粘貼內容。
`cut`、`copy`、`paste`這三個事件的事件對象都是`ClipboardEvent`接口的實例。`ClipboardEvent`有一個實例屬性`clipboardData`,是一個 DataTransfer 對象,存放剪貼的數據。具體的 API 接口和操作方法,請參見《拖拉事件》的 DataTransfer 對象部分。
```javascript
document.addEventListener('copy', function (e) {
e.clipboardData.setData('text/plain', 'Hello, world!');
e.clipboardData.setData('text/html', '<b>Hello, world!</b>');
e.preventDefault();
});
```
上面的代碼使得復制進入剪貼板的,都是開發者指定的數據,而不是用戶想要拷貝的數據。
## 焦點事件
焦點事件發生在元素節點和`document`對象上面,與獲得或失去焦點相關。它主要包括以下四個事件。
- `focus`:元素節點獲得焦點后觸發,該事件不會冒泡。
- `blur`:元素節點失去焦點后觸發,該事件不會冒泡。
- `focusin`:元素節點將要獲得焦點時觸發,發生在`focus`事件之前。該事件會冒泡。
- `focusout`:元素節點將要失去焦點時觸發,發生在`blur`事件之前。該事件會冒泡。
這四個事件的事件對象都繼承了`FocusEvent`接口。`FocusEvent`實例具有以下屬性。
- `FocusEvent.target`:事件的目標節點。
- `FocusEvent.relatedTarget`:對于`focusin`事件,返回失去焦點的節點;對于`focusout`事件,返回將要接受焦點的節點;對于`focus`和`blur`事件,返回`null`。
由于`focus`和`blur`事件不會冒泡,只能在捕獲階段觸發,所以`addEventListener`方法的第三個參數需要設為`true`。
```javascript
form.addEventListener('focus', function (event) {
event.target.style.background = 'pink';
}, true);
form.addEventListener('blur', function (event) {
event.target.style.background = '';
}, true);
```
上面代碼針對表單的文本輸入框,接受焦點時設置背景色,失去焦點時去除背景色。
## CustomEvent 接口
CustomEvent 接口用于生成自定義的事件實例。那些瀏覽器預定義的事件,雖然可以手動生成,但是往往不能在事件上綁定數據。如果需要在觸發事件的同時,傳入指定的數據,就可以使用 CustomEvent 接口生成的自定義事件對象。
瀏覽器原生提供`CustomEvent()`構造函數,用來生成 CustomEvent 事件實例。
```javascript
new CustomEvent(type, options)
```
`CustomEvent()`構造函數接受兩個參數。第一個參數是字符串,表示事件的名字,這是必須的。第二個參數是事件的配置對象,這個參數是可選的。`CustomEvent`的配置對象除了接受 Event 事件的配置屬性,只有一個自己的屬性。
- `detail`:表示事件的附帶數據,默認為`null`。
下面是一個例子。
```javascript
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`事件。該事件觸發后,會被監聽到,從而輸出該事件實例的`detail`屬性(即字符串`hello`)。
下面是另一個例子。
```javascript
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`屬性。
- 前言
- 入門篇
- 導論
- 歷史
- 基本語法
- 數據類型
- 概述
- null,undefined 和布爾值
- 數值
- 字符串
- 對象
- 函數
- 數組
- 運算符
- 算術運算符
- 比較運算符
- 布爾運算符
- 二進制位運算符
- 其他運算符,運算順序
- 語法專題
- 數據類型的轉換
- 錯誤處理機制
- 編程風格
- console 對象與控制臺
- 標準庫
- Object 對象
- 屬性描述對象
- Array 對象
- 包裝對象
- Boolean 對象
- Number 對象
- String 對象
- Math 對象
- Date 對象
- RegExp 對象
- JSON 對象
- 面向對象編程
- 實例對象與 new 命令
- this 關鍵字
- 對象的繼承
- Object 對象的相關方法
- 嚴格模式
- 異步操作
- 概述
- 定時器
- Promise 對象
- DOM
- 概述
- Node 接口
- NodeList 接口,HTMLCollection 接口
- ParentNode 接口,ChildNode 接口
- Document 節點
- Element 節點
- 屬性的操作
- Text 節點和 DocumentFragment 節點
- CSS 操作
- Mutation Observer API
- 事件
- EventTarget 接口
- 事件模型
- Event 對象
- 鼠標事件
- 鍵盤事件
- 進度事件
- 表單事件
- 觸摸事件
- 拖拉事件
- 其他常見事件
- GlobalEventHandlers 接口
- 瀏覽器模型
- 瀏覽器模型概述
- window 對象
- Navigator 對象,Screen 對象
- Cookie
- XMLHttpRequest 對象
- 同源限制
- CORS 通信
- Storage 接口
- History 對象
- Location 對象,URL 對象,URLSearchParams 對象
- ArrayBuffer 對象,Blob 對象
- File 對象,FileList 對象,FileReader 對象
- 表單,FormData 對象
- IndexedDB API
- Web Worker
- 附錄:網頁元素接口
- a
- img
- form
- input
- button
- option
- video,audio