JavaScript與HTML的交互是通過事件實現的,事件代表文檔或瀏覽器窗口中某個有意義的時刻。
可以使用僅在事件發生時執行的監聽器(即處理程序)訂閲事件。=>觀察者模式 => 頁面行為(JavaScript中定義)與頁面展示(HTML和CSS定義)的分離。
最早的事件是為了把某些表單處理工作從服務器轉移到瀏覽器上來。DOM2開始嘗試以符合邏輯的方式來標準化DOM事件。IE8是最後一個使用專有事件系統的主流瀏覽器。
事件流
頁面哪個部分擁有特定的事件呢?當你點擊一個按鈕時,實際上不光點擊了這個按鈕,還點擊了它的容器以及整個頁面。
事件流描述了頁面接收事件的順序。
IE支持事件冒泡流,Netscape Communicator支持事件捕獲流
事件冒泡
從最具體的元素開始觸發,然後向上傳播至沒有那麼具體的元素(document)。
點擊事件:被點擊的元素最先觸發click事件,然後click事件沿DOM樹一路向上,在經過的每個節點上依次觸發,直至到達document對象。
現代瀏覽器中的事件會一直冒泡到window對象。
事件捕獲
最不具體的節點(document)最先收到事件,而最具體的節點最後收到事件。=>為了在事件到達最終目標前攔截事件。
點擊事件:最先由document元素捕獲,然後沿DOM樹依次向下傳播,直至到達實際的目標元素。
現代瀏覽器都是從window對象開始捕獲事件。DOM2 Events規範規定的是從document開始。
由於舊版本瀏覽器不支持,通常建議使用事件冒泡,特殊情況下可以使用事件捕獲。
DOM事件流
DOM2 Events規範規定事件流分為3個階段:事件捕獲、到達目標和事件冒泡。
- 事件捕獲:最先發生,為提前攔截事件提供了可能;
- 實際的目標元素接收到事件;
- 冒泡:最遲要在這個階段響應事件。
在DOM事件流中,實際的目標在捕獲階段不會接收到事件。下一階段會在實際目標元素上觸發事件的“到達目標”階段,通常在事件處理時被認為是冒泡階段的一部分;然後冒泡階段開始,事件反向傳播至文檔。
雖然DOM2 Events規範明確捕獲階段不命中事件目標,但現代瀏覽器都會在捕獲階段在事件目標上觸發事件。=> 在事件目標上有兩個機會來處理事件。
所有現代瀏覽器都支持DOM事件流,只有IE8及更早版本不支持。
capture phase | | / \ bubbling up
-----------------| |--| |-----------------
| element1 | | | | |
| -------------| |--| |----------- |
| |element2 \ / | | | |
| -------------------------------- |
| W3C event model |
------------------------------------------
事件處理程序
事件是用户或瀏覽器執行的某種動作。如click、load等。
為響應事件而調用的函數被稱為事件處理程序(或事件監聽器)。事件處理程序的名字以“on”開頭。
有多種方式可以指定事件處理程序。
HTML事件處理程序
特定元素支持的每個事件都可以使用事件處理程序的名字(onxxx)以HTML屬性的形式來指定。此時屬性的值必須是能夠執行的JavaScript代碼。
因為屬性的值是JavaScript代碼,所以不能在未經轉義的情況下使用HTML語法字符,如&、"、<和>。為避免使用HTML實體,可以使用單引號代替雙引號,或者使用\"。
在HTML中定義的事件處理程序可以包含精確的動作指令,也可以調用在頁面其他地方定義的腳本。作為事件處理程序執行的代碼可以訪問全局作用域中的一切。
以這種方式指定的事件處理程序有一些特殊的地方:
- 會創建一個函數來封裝屬性的值。這個函數有一個特殊的局部變量event,即event對象;
- 在這個函數中,this值相當於事件的目標元素;
-
這個動態創建的包裝函數,其作用域鏈被擴展了。=> document和元素自身的成員都可以被當成局部變量來訪問。這是通過使用with實現的。
// 實際上的包裝函數是onclick屬性的值 function () { with(document) { with(this) { // ... HTML事件處理程序屬性值 } } }
=> 事件處理程序可以更方便地訪問自己的屬性(不用帶this.)
如果元素是一個表單輸入框,則作用域鏈中還會包含表單元素 => 事件處理程序的代碼可以不必引用表單元素,而直接訪問同一表單中的其他成員了(通過name屬性)。
在HTML中指定事件處理程序存在的問題:
-
時機問題。有可能HTML元素已經顯示在頁面上,但事件處理程序的代碼還無法執行。=> 大多數HTML事件處理程序會封裝在try/catch塊中,以便在這種情況下靜默失敗。
<input type="button" value="Click Me" onclick="try{doSomething();}catch(ex){}"> - 對事件處理程序作用域鏈的擴展在不同瀏覽器中可能導致不同的結果。不同JavaScript引擎中標識符解析的規則存在差異 => 訪問無限定的對象成員可能導致錯誤。
- HTML與JavaScript的強耦合。(如果要修改,必須在HTML和JavaScript中都修改代碼)
DOM0事件處理程序
把一個函數賦值給(DOM元素的)一個事件處理程序屬性。=> 簡單
要使用JavaScript指定事件處理程序,必須先取得要操作對象的引用。
每個元素(包括window和document)都有通常小寫的事件處理程序屬性。
賦值代碼運行之後才會給事件處理程序賦值。
所賦函數被視為元素的方法。=> 事件處理程序會在元素的作用域中運行,即this等於元素。
let btn = document.querySelector('#myBtn');
btn.onclick = function() {
console.log(this.id); // "myBtn"
}
以這種方式添加事件處理程序是註冊在事件流的冒泡階段的。
通過將事件處理程序屬性的值設置為null,可以移除通過DOM0方式添加的事件處理程序。(在HTML中指定的事件處理程序,也可以通過JavaScript將相應屬性設置為null來移除)
btn.onclick = null;
DOM2事件處理程序
DOM2 Events為事件處理程序的賦值和移除定義了兩個方法:addEventListener()和removeEventListener()。暴露在所有DOM節點上。
接收3個參數:事件名、事件處理函數和一個表示是否在捕獲階段調用處理函數的布爾值(默認值為false,在冒泡階段調用)。
這個事件處理程序同樣在被附加到的元素的作用域中運行:this等於元素。
主要優勢:可以為同一個事件添加多個事件處理程序。多個事件處理程序以添加順序來觸發。
通過addEventListener()添加的事件處理程序只能使用removeEventListener()並傳入與添加時同樣的參數來移除。(事件處理函數必須是同一個)
大多數情況下,事件處理程序會被添加到事件流的冒泡階段,主要原因是跨瀏覽器兼容性好。(除非需要在事件到達其指定目標之前攔截事件)
IE事件處理程序
IE實現了與DOM類似的方法,attachEvent()和detachEvent()。
接收2個參數:事件處理程序的名字和事件處理函數。因為IE8及更早版本只支持事件冒泡,所以使用attachEvent()添加的事件處理程序會添加到冒泡階段。
在IE中使用attachEvent()與使用DOM0方式的主要區別在於事件處理程序的作用域。使用attachEvent()時,事件處理程序是在全局作用域中運行的,因此this等於window。
attachEvent()方法也可以給一個元素添加多個事件處理程序。以添加它們的順序反向觸發。
使用attachEvent()添加的事件處理程序將使用detachEvent()來移除,只要提供相同的參數(處理函數是相同的函數引用)。
let btn = document.querySelector('#myBtn');
var handler = function() {
console.log("Clicked");
};
btn.attachEvent("onclick", handler);
btn.detachEvent("onclick", handler);
跨瀏覽器事件處理程序
以跨瀏覽器兼容的方式處理事件。
自己編寫跨瀏覽器事件處理代碼主要依賴能力檢測。要確保最大兼容性,只要讓代碼在冒泡階段運行即可。
var EventUtil = {
addHandler: function(element, type, handler) {
if(element.addEventListener) {
element.addEventListener(type, handler, false);
} else if(element.attachEvent) {
element.attachEvent("on"+type, handler);
} else { // 默認DOM0方式
element["on"+type] = handler;
}
},
removeHandler: function(element, type, handler) {
if(element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if(element.detachEvent) {
element.detachEvent("on"+type, handler);
} else { // 默認DOM0方式
element["on"+type] = null;
}
}
};
// 使用
let btn = document.querySelector('#myBtn');
let handler = function() {
console.log("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
EventUtil.removeHandler(btn, "click", handler);
沒有解決的存在的問題:
- IE的作用域問題。(attachEvent()中this等於window)
- 多個事件處理程序執行順序問題。DOM2以添加順序,IE為添加順序的反向
- DOM0只支持給一個事件添加一個處理程序。(DOM0瀏覽器已經很少人使用,問題應該不大)
事件對象event
在DOM中發生事件時,所有相關信息(如事件目標元素、事件類型)都會被收集並存儲在一個名為event的對象中。
DOM事件對象
在DOM合規的瀏覽器中,event對象是傳給事件處理程序的唯一參數。不管以哪種方式(DOM0或DOM2)指定事件處理程序,都會傳入這個event對象。在通過HTML屬性指定的事件處理程序中,同樣可以使用變量event引用事件對象。
所有事件對象包含的公共屬性和方法:
- bubbles。布爾值。是否冒泡
- cancelable。布爾值。是否可以取消事件的默認行為
- currentTarget。元素。當前事件處理程序所在的元素
- defaultPrevented。布爾值。true表示已經調用preventDefault()方法。(DOM3 Events新增)
- detail。整數(??)。事件相關的其他信息
- eventPhase。整數。表示調用事件處理程序的階段:1-捕獲階段;2-到達目標;3-冒泡階段
- target。元素。事件目標元素
- trusted。布爾值。true表示事件由瀏覽器生成;false表示事件由開發者通過JavaScript創建。(DOM3 Events新增)
- type。字符串。被觸發的事件類型
- View。AbstractView。與事件相關的抽象視圖;等於事件所發生的window對象
- preventDefault()。函數。用於取消事件的默認行為 => cancelable為true時才可調用
- stopImmediatePropagation()。函數。用於取消所有後續事件捕獲或事件冒泡,並阻止調用任何後續事件處理程序。(DOM3 Events新增)
- stopPropagation()。函數。用於取消所有後續事件捕獲或事件冒泡 => bubbles為true時才可調用
在事件處理程序內部,this始終等於currentTarget。this === event.currentTarget。
如果事件處理程序直接添加在意圖的目標,則this、currentTarget和target三者相等。
type屬性在一個處理程序處理多個事件時很有用:根據事件類型,做出不同的響應。
preventDefault()可阻止特定事件的默認動作,如鏈接的默認行為是被單擊時導航到href屬性指定的URL。任何可調用preventDefault()取消默認行為的事件,其event對象的cancelable屬性都會設置為true。
stopPropagation()用於立即阻止事件流在DOM結構中傳播,取消後續的事件捕獲或冒泡。
eventPhase屬性可用於確定事件流當前所處的階段。如果事件處理程序在目標上被調用,則eventPhase等於2 => 雖然”到達目標“是在冒泡階段發生的,但eventPhase等於2。=> 當eventPhase等於2,this、currentTarget和target三者相等。
event對象只在事件處理程序執行期間存在,一旦執行完畢,就會被銷燬。
IE事件對象
IE事件對象可以基於事件處理程序被指定的方式以不同的方式來訪問。
- 如果使用DOM0方式指定,則event對象是window的一個屬性
- 如果使用attachEvent()指定,則event對象會作為唯一的參數傳給處理函數。此時event對象仍然是window的屬性,出於方便也將其作為參數傳入
- 在通過HTML屬性方式指定的事件處理程序中,同樣可以使用變量event引用事件對象。
所有IE事件對象都會包含的公共屬性和方法:
- cancelBubble。布爾值。讀/寫。true表示取消冒泡(默認false),與stopPropagation()方法效果相似
- returnValue。布爾值。讀/寫。false表示取消事件默認行為(默認true),與preventDefault()效果相同
- srcElement。元素。事件目標,即target
- type。字符串。觸發的事件類型
事件處理程序的作用域取決於指定它的方式,所以更好的方式是使用事件對象的srcElement屬性代替this。(DOM0方式下,this等於元素;attachEvent()方式下,this等於window)
與DOM不同,無法通過JavaScript確定事件是否可以被取消。
cancelBubble屬性與stopPropagation()方法用途相似,但IE8及更早版本不支持捕獲階段,所以只會取消冒泡。
跨瀏覽器事件對象
DOM事件對象中包含IE事件對象的所有信息和能力,只是形式不同。這些共性可讓兩種事件模型之間的映射成為可能。
var EventUtil = {
addHandler: function(element, type, handler) {
// ...
},
removeHandler: function(element, type, handler) {
// ...
},
getEvent: function(event) { // IE事件中以DOM0方式指定事件處理程序時,event對象是window的一個屬性
return event ? event || window.event;
},
getTarget: function(event) {
return event.target || event.srcElement;
},
preventDefault: function(event) {
if(event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},
stopPropagation: function(event) { // 可能會停止事件冒泡,也可能既會停止事件冒泡也停止事件捕獲
if(event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};
// 使用
let btn = document.querySelector('#myBtn');
btn.onclick = function(event) {
event = EventUtil.getEvent(event);
let target = EventUtil.getTarget(event);
EventUtil.preventDefault(event); // 阻止事件的默認行為
EventUtil.stopPropagation(event); // 阻止事件冒泡
};
事件類型
所發生事件的類型決定了事件對象中會保存什麼信息。DOM3 Events定義的事件類型:
- 用户界面事件(UIEvent):與BOM交互的通用瀏覽器事件
- 焦點事件(FocusEvent):元素獲得和失去焦點時觸發
- 鼠標事件(MouseEvent):鼠標在頁面上執行某些操作時觸發
- 滾輪事件(WheelEvent):使用鼠標滾輪(或類似設備)時觸發
- 輸入事件(InputEvent):向文檔中輸入文本時觸發
- 鍵盤事件(KeyboardEvent):鍵盤在頁面上執行某些操作時觸發
- 合成事件(CompositionEvent):使用某種IME(Input Method Editor,輸入法編輯器)輸入字符時觸發
- HTML5還定義了另一組事件
- 瀏覽器通常在DOM和BOM上實現專有事件:根據開發者需求,不同瀏覽器的實現可能不同
DOM3 Events在DOM2 Events基礎上重新定義了事件,並增加了新的事件類型。所有主流瀏覽器都支持DOM2 Events和DOM3Events。
用户界面事件 UIEvent
不一定跟用户操作有關。保留它們是為了向後兼容。主要有以下幾種:
- DOMActivate(DOM3 Events中已經廢棄)。元素被用户通過鼠標或鍵盤操作激活時觸發,瀏覽器實現之間存在差異
- load。window(頁面加載完成後觸發);窗套frameset(所有窗格frame都加載完成後觸發);img(圖片加載完成後觸發);object(相應對象加載完成後觸發)
- unload。window(頁面完全卸載後觸發);窗套(所有窗格都卸載完成後觸發);object(相應對象卸載完成後觸發)
- abort。object(相應對象加載完成前被用户提前終止下載時觸發)
- error。window(JavaScript報錯時觸發);img(無法加載指定圖片時觸發);object(無法加載相應對象時觸發);窗套(一個或多個窗格無法完成加載時觸發)
- select。在文本框(input或textarea)上用户選擇了一個或多個字符時觸發
- resize。window或窗格(窗口或窗格被縮放時觸發)
- scroll。當用户滾動包含滾動條的元素時在元素上觸發。body元素包含已加載頁面的滾動條
大多數HTML事件與window對象和表單控件有關。除了DOMActivate,其他在DOM2 Events中都被歸為HTML Events。(DOMActivate是UI事件)
焦點事件 FocusEvent
頁面元素獲得或失去焦點時觸發。可以與document.hasFocus()和document.activeElement一起為開發者提供用户在頁面中導航的信息。焦點事件有以下6種:
- blur。失去焦點時觸發。不冒泡,所有瀏覽器都支持
- DOMFocusIn(DOM3 Events中已經廢棄,推薦focusin)。獲得焦點時觸發。focus的冒泡版。Opera唯一支持
- DOMFocusOut(DOM3 Events中已經廢棄,推薦focusout)。失去焦點時觸發,blur的通用版。Opera唯一支持
- focus。獲得焦點時觸發。不冒泡,所有瀏覽器都支持
- focusin。獲得焦點時觸發。focus的冒泡版
- focusout。失去焦點時觸發。blur的通用版
兩個主要事件是focus和blur,它們最大的問題是不冒泡。
當焦點從頁面中的一個元素A移到另一個元素B上,會依次發生如下事件(測試,與書中不一致):
1)A:blur
2)A:focusout
3)B:focus
4)B:focusin
DOMFocusOut和DOMFocusIn未驗證
鼠標和滾輪事件MouseEvent
鼠標是用户的主要定位設備。DOM3 Events定義了9種鼠標事件:
- click。用户單擊鼠標主鍵(通常是左鍵)或按鍵盤迴車鍵時觸發。
- dblclick。用户雙擊鼠標主鍵(通常是左鍵)時觸發(DOM3 Events中標準化)
- mousedown。用户按下任意鼠標鍵時觸發。不能通過鍵盤觸發
- mouseenter。用户把鼠標光標從元素外部移到元素內部時觸發。不冒泡,也不會在光標經過後代元素時觸發。(DOM3 Events中新增)
- mouseleave。用户把鼠標光標從元素內部移到元素外部時觸發。不冒泡,也不會在光標經過後代元素時觸發。(DOM3 Events中新增)
- mousemove。鼠標光標在元素上移動時反覆觸發。不能通過鍵盤觸發
- mouseout。用户把鼠標光標從一個元素移到另一個元素上(外部元素或子元素)時觸發。不能通過鍵盤觸發
- mouseover。用户把鼠標光標從元素外部移到元素內部時觸發。不能通過鍵盤觸發
- mouseup。用户釋放鼠標鍵時觸發。不能通過鍵盤觸發
頁面中所有元素都支持鼠標事件。除了mouseenter和mouseleave,其他鼠標事件都會冒泡,都可以被取消,這會影響瀏覽器的默認行為。由於事件之間存在關係,因此取消鼠標事件的默認行為也會影響其他事件。
雙擊鼠標主鍵會按如下順序觸發事件:
1)mousedown
2)mouseup
3)click
4)mousedown
5)mouseup
6)click
7)dblclick
click和dblclick在觸發前都依賴其他事件觸發,mousedown和mouseup則不會受其他事件影響。
IE8和更早的版本的實現中存在問題,會導致雙擊事件跳過第二次mousedown和click事件。
1)mousedown
2)mouseup
3)click
4)mouseup
5)dblclick
DOM3 Events中鼠標事件對應的類型是”MouseEvent“(單數形式)
鼠標事件還有一個名為滾輪事件的子類別。滾輪事件只有一個事件mousewheel,對應鼠標滾輪或帶滾輪的類似設備上滾輪的交互。
鼠標事件event對象的一些屬性:
-
客户端座標
注:客户端座標不考慮頁面滾動
-
頁面座標
在頁面上的位置。表示事件發生時鼠標光標在頁面上的座標,通過event對象的pageX和pageY屬性獲取。
反映的是光標到頁面而非視口左邊與上邊的距離。
-
屏幕座標
鼠標光標在屏幕上的座標,通過event對象的screenX和screenY屬性獲取。
-
修飾鍵
有時要確定用户想實現的操作,還要考慮鍵盤按鍵的狀態。
鍵盤上的修飾鍵Shift、Ctrl、Alt和Meta(win的window鍵,mac的command鍵)經常用於修改鼠標事件的行為。
4個屬性來表示這幾個修飾鍵的狀態:shiftKey、ctrlKey、altKey、metaKey。(被按下為true,否則為false)
現在瀏覽器支持所有4個修飾鍵,IE8及更早版本不支持metaKey屬性。
-
相關元素
對mouseover和mouseout事件而言,還存在與事件相關的其他元素。
-
鼠標按鍵
對mousedown和mouseup事件來説,event對象上會有一個button屬性,表示按下或釋放的是哪個按鍵。DOM為button屬性定義了3個值:0-主鍵;1-中鍵(通常是滾輪鍵);2-副鍵。
IE8及更早版本也提供了button屬性,考慮了同時按多個鍵的情況。
-
額外事件信息
DOM2 Events規範在event對象上提供了detail屬性,以給出關於事件的更多信息。對鼠標事件來説,detail包含一個數值,表示在給定位置上發生了多少次單擊(連續單擊)。每次單擊會加1。連續點擊中斷會重置為0。
IE還為每個鼠標事件提供了以下額外信息:
- altLeft,布爾值,是否按下了左Alt鍵(如果為true,則altKey也為true)
- ctrlLeft,布爾值,是否按下左Ctrl鍵(如果為true,則ctrlKey也為true)
- offsetX,光標相對於目標元素邊界的x座標
- offsetY,光標相對於目標元素邊界的y座標
- shiftLeft,布爾值,是否按下了左Shift鍵(如果為true,則shiftKey也為true)
滾輪mousewheel事件
在用户使用鼠標滾輪時觸發,包括在垂直方向上任意滾動。會在任何元素上觸發,並(在IE8中)冒泡到document和(所有現代瀏覽器中)window。
event對象包含鼠標事件的所有標準信息,此外還有一個名為wheelDelta的屬性。
多數情況下只需知道滾輪滾動的方向,而這通過wheelDelta值的符號就可以知道。(向前滾動一次+120,向後滾動一次-120)
觸摸屏設備
觸摸屏通常不支持鼠標操作。
- 不支持dblclick事件。(測試一加三可以)
- 單指點觸屏幕上的可點擊元素會觸發mousemove事件。(測試一加三不行)可點擊元素是指點擊時有默認動作的元素(如鏈接)或指定了onclick事件處理程序的元素
- mousemove事件也會觸發mouseover和mouseout事件。(還未測試)
- 雙指點觸屏幕並滑動導致頁面滾動時會觸發mousewheel和scroll事件。(還未測試)
無障礙問題
如果Web應用或網站要考慮殘障人士,特別是使用屏幕閲讀器的用户,那麼必須小心使用鼠標事件(除了回車鍵可以觸發click事件,其他鼠標事件不能通過鍵盤觸發)。建議不要使用click事件之外的其他鼠標事件向用户提示功能或觸發代碼執行。=> 會嚴格妨礙盲人或視障用户使用。
幾條使用鼠標事件時應該遵循的無障礙建議:
- 使用click事件執行代碼。當使用onmousedown執行代碼時,應用程序會運行得更快,但屏幕閲讀器無法觸發mousedown事件
- 不要使用mouseover向用户顯示新選項。無法觸發。可以考慮鍵盤快捷鍵
- 不要使用dblclick執行重要的操作。無法觸發
更多網站無障礙的信息,可以參考WebAIM網站。
鍵盤與輸入事件KeyboardEvent
用户操作鍵盤時觸發。很大程度上是基於原始的DOM0實現的。
DOM3 Events為鍵盤事件提供了一個首先在IE9中完全實現的規範,其他瀏覽器也開始實現該規範,但仍存在很多遺留的實現。
包含3個事件:
- keydown,按下鍵盤上某個鍵時觸發,持續按鈕會重複觸發
- keypress(DOM3 Events已經廢棄,推薦textInput事件),按下鍵盤上某個鍵併產生字符時觸發,持續按住會重複觸發。Esc鍵也會觸發(Chrome裏測試不會觸發?)。
- keyup,用户釋放鍵盤上某個鍵時觸發。
所有元素都支持這些事件。但在文本框中輸入內容時最容易看到。
輸入事件只有一個:textInput。是對keypress事件的擴展,用於在文本顯示給用户之前更方便地截獲文本輸入。會在文本被插入到文本框之前觸發。
- 當用户按下某個字符鍵時,會觸發keydown事件,然後觸發keypress事件,最後觸發keyup事件。(keydown和keypress會在文本框出現變化之前觸發,keyup會在文本框出現變化之後觸發);如果按住不放,keydown和keypress會重複觸發,直到這個鍵被釋放。
- 當用户按下非字符鍵時,會觸發keydown事件,然後觸發keyup事件。如果按住不放,keydown會重複觸發,直到這個鍵被釋放。
注:鍵盤事件支持與鼠標事件相同的修飾鍵。
鍵盤事件event對象的一些屬性:
-
鍵碼keyCode
keydown和keyup事件,event對象的keyCode屬性會保存一個鍵碼。
對於字母和數字鍵,keyCode的值與大寫字母和數字的ASCII編碼一致。與是否按了Shift鍵無關。
DOM和IE的event對象都支持keyCode屬性
-
字符編碼charCode
keypress事件,意味着按鍵會影響屏幕上顯示的文本。對插入或移除字符的鍵,所有瀏覽器都會觸發keypress事件,其他鍵則取決於瀏覽器。
event對象上的charCode屬性,只有發生keypress事件時這個屬性才會被設置值(此時與keyCode屬性相等)。包含的是按鍵字符對應的ASCII編碼。
IE8及更早版本和Opera使用keyCode傳達字符的ASCII編碼。要以跨瀏覽器方式獲取字符編碼,首先要檢測charCode屬性是否有值,如果沒有再使用keyCode。
有了字母編碼,就可以使用String.fromCharCode()方法將其轉換為實際的字符了。
DOM3的變化
DOM3 Events規範並未規定charCode屬性,而是定義了key和char兩個新屬性。
key屬性用於替代keyCode,且包含字符串。按下字符鍵時,key等於文本字符;按下非字符鍵時,key的值是鍵名(如”Shift“或”ArrowDown“)
char屬性在按下字符鍵時與key類似,在按下非字符鍵時為null。(測試Chrome中keypress和keydown的event對象此屬性都無)
IE支持key屬性但不支持char屬性。
Safari和Chrome支持keyIdentifier屬性(測試Chrome無此屬性)。對於字符鍵,keyIdentifier返回以”U+0000“形式表示Unicode值的字符串形式的字符編碼。
由於缺乏跨瀏覽器支持,不建議使用key、keyIdentifier和char。
DOM3 Events也支持一個名為location的屬性,是一個數值,表示是在哪裏按的鍵。可能的值為:0-默認鍵;1-左邊;2-右邊;3-數字鍵盤;4-移動設備(虛擬鍵盤);5-遊戲手柄。Safari和Chrome支持一個等價的keyLocation屬性(實現有問題)
沒有得到廣泛支持,不建議在跨瀏覽器開發時使用location屬性。
給event對象增加了getModifierState()方法,接收一個參數,一個等於Shift、Control、Alt、AltGraph或Meta的字符串,表示要檢測的修飾鍵。如果給定 的修飾鍵被按鈕,則返回true。(也可直接使用event對象的shiftKey、ctrlKey、altKey或metaKey屬性獲取)
輸入textInput事件
DOM3 Events規範新增,在字符被輸入到可編輯區域時觸發。
與keypress比對:1. keypress會在任何可以獲得焦點的元素上觸發,textInput只在可編輯區域上觸發;2. textInput只在有新字符被插入時才會觸發,而keypress對任何可能影響文本的鍵都會觸發(包括退格鍵(Chrome裏測試不會觸發?))。3. 使用輸入法(搜狗)時,在觸發合成事件時不會觸發keypress,在compositionend觸發之前會先觸發textInput事件。4. 使用鍵盤輸入先觸發keypress,再觸發textInput。
該事件主要關注字符,event對象上有data屬性,為被插入的字符(非字符編碼);還有一個inputMethod的屬性(Chrome中測試無此屬性打印為undefined),表示向控件中輸入文本的手段,可以輔助驗證
設備上的鍵盤事件(非鍵盤)
任天堂Wii
合成事件
DOM3 Events中新增,用於處理通常使用IME輸入時的複雜輸入序列。IME可以讓用户輸入物理鍵盤上沒有的字符,通常需要按下多個鍵才能輸入一個字符,合成事件用於檢測和控制這種輸入。
合成事件有以下3種:
- compositionstart,表示輸入即將開始
- compositionupdate,在新字符插入輸入字段時觸發;
- compositionend,表示恢復正常鍵盤輸入
唯一增加的事件屬性是data:
- 在compositionstart中,為正在編輯的文本(默認是空串,或者是選中的文本)
- 在compositionupdate中,為要插入的新字符
- 在compositionend中,為本次合成過程中輸入的全部內容
測試得到的觸發順序:
keydown -> ...start -> ...update -> ( (keyup) -> keydown -> ...update -> (keyup))(循環觸發) -> ...update(此時data與...end事件中的data一致) -> textInput -> ...end -> keyup
變化事件
DOM2的變化事件(Mutation Events),在DOM發生變化時提供通知。
(已廢棄)
已經被Mutation Observers所取代(第14章)
HTML5事件
HTML5中得到瀏覽器較好支持的一些事件(規範未涵蓋)
-
contextmenu事件
單擊鼠標右鍵(Ctrl+單擊左鍵)。用於允許開發者取消默認的上下文菜單並提供自定義菜單。冒泡。
事件目標是觸發操作的元素,這個事件在所有瀏覽器中都可以取消(event.preventDefault()或event.returnValue設置為false)。
通常通過onclick事件處理程序觸發隱藏(自定義菜單)。
-
beforeunload事件
在window上觸發。用於給開發者提供阻止頁面被卸載的機會。在頁面即將從瀏覽器中卸載時觸發。
不能取消,否則就意味着可以把用户永久阻攔在一個頁面上。
該事件會向用户顯示一個確認框。用户可以點擊取消或者確認離開頁面。需要將event.returnValue設置為要在確認框中顯示的字符串(對於IE和FF來説)(測試FF顯示的提示文字與returnValue屬性值無關),並將其作為函數值返回(對於Safari和Chrome來説)(測試Chrome無返回值也無影響)
-
DOMContentLoaded事件
會在DOM樹構建完成後立即觸發,而不用等待圖片、JavaScript文件、CSS文件或其他資源加載完成。(可以在外部資源下載的同時指定事件處理程序,從而讓用户能夠更快地與頁面交互)
比對load事件:要等待很多外部資源加載完成。
需要給document或window添加事件處理程序(實際的事件目標是document,會冒泡到window)。
通常用於添加事件處理程序或執行其他DOM操作。這個事件始終在load事件之前觸發。
對於不支持DOMContentLoaded事件的瀏覽器,可以使用超時為0的setTimeout()函數,通過其回調來設置事件處理程序。本質上是在當前JavaScript進程執行完畢後立即執行這個回調。(與DOMContentLoaded觸發時機一致無絕對把握,最好是頁面上的第一個超時代碼)
-
readystatechange事件
IE首先定義。用於提供文檔或元素加載狀態的信息,但行為有時不穩定。
event.target或其他支持readystatechange事件的對象都有一個readyState屬性,該屬性可能為以下5個值:
- uninitialized:對象存在並尚未初始化
- loading:對象正在加載數據
- loaded:對象已經加載完數據
- interactive √:對象可以交互,但尚未加載完成
- complete √:對象加載完成
並非所有對象都會經歷所有readyState階段(Chrome測試document只經歷了兩個階段:interactive和complete)
值為”interactive“的readyState階段,時機類似於DOMContentLoaded。進入交互階段,意味着DOM樹已加載完成。(此時圖片和其他外部資源不一定都加載完成了)。
與load事件共同使用時,這個事件的觸發順序不能保證。interactive和complete的順序也不是固定的,為了搶到較早的時機,需要同時檢測交互階段和完成階段(可以保證儘可能接近使用DOMContentLoaded事件的效果)。
-
pageshow與pagehide事件
FF和Opera開發的一個名為往返緩存(bfcache,back-forward cache)的功能,旨在使用瀏覽器”前進“和”後退“按鈕時加快頁面之間的切換。不僅存儲頁面數據,也存儲DOM和JavaScript狀態,實際上是把整個頁面都保存在內存裏。
如果頁面在緩存中,導航到這個頁面時就不會觸發load事件。
- pageshow:在頁面顯示時觸發,無論是否來自往返緩存。新加載的頁面,會在load事件之後觸發;來自往返緩存的頁面,會在頁面狀態完全恢復後觸發。事件目標是document,但事件處理程序必須添加到window上。(點擊了瀏覽器的”刷新“按鈕,頁面會重新加載)。event對象中的persisted屬性為布爾值,表示頁面內容是否來自往返緩存。
-
pagehide:在頁面從瀏覽器中卸載後,在unload事件之前觸發。事件目標是document,但事件處理程序必須添加到window上。event對象中的persisted屬性為布爾值,表示頁面在卸載後是否保存在往返緩存中。
註冊了onunload事件處理程序的頁面會自動排除在往返緩存之外(測試beforeunload也會影響),因為onunload的典型場景就是撤銷onload事件發生時所做的事情,如果使用往返緩存,下一次頁面顯示時就不會觸發onload事件,這可能導致頁面無法使用。
-
hashchange事件
用於在URL散列值(#後面的部分)發生變化時通知開發者。
事件處理程序必須添加給window。event對象有兩個新屬性:oldURL和newURL,分別保存變化前後的URL,包含散列值的完整URL。如果想確定當前的散列值,最好使用location對象。
設備事件
智能手機和平板計算機=>交互的新方式
用於確定用户使用設備的方式。
-
orientationchange事件
蘋果,移動Safari瀏覽器。判斷用户的設備是處於垂直模式還是水平模式。window.orientation屬性,有3種值:0-垂直模式,90-左轉水平模式(Home鍵在右),-90-右轉水平模式(Home鍵在左)。當屬性值改變就會觸發該事件。
所有iOS設備都支持該事件和該屬性。(測試鎖定豎屏=>不會改變)
被認為是window事件,也可給body元素添加onorientationchange屬性來指定事件處理程序。
-
deviceorientation事件
DeviceOrientationEvent規範定義的事件。
如果可以獲取設備的加速計信息,且數據發生了變化,就會在window上觸發。只反應設備在空間中的朝向,與移動無關。
設備本身處於3D空間,x軸方向為從設備左側到右側,y軸方向為從設備底部到上部,z軸方向為從設備背面到正面。
event對象包含各個軸相對於設備靜置時座標值的變化,主要有5個屬性:
- alpha:0~360內的浮點值,表示圍繞z軸旋轉時y軸的度數(左右轉)
- beta:-180~180內的浮點值,表示圍繞x軸旋轉時z軸的度數(前後轉)
- gamma:-90~90內的浮點值,表示圍繞y軸旋轉時z軸的度數(扭轉)。
- absolute:布爾值,表示設備是否返回絕對值。
- compassCalibrated:布爾值,表示設備的指南針是否正確校準。
測試iPhone8(iOS11.4.1)平放在桌面上也一直監聽到變動(?),測試Android(一加三)平放在桌面上後不會變動
-
devicemotion事件
DeviceOrientationEvent規範定義的事件。
用於提示設備實際上在移動,而不僅僅是改變了朝向。event對象包含的額外屬性:
- acceleration:對象,包含x、y和z屬性,反映不考慮重力情況下各個維度的加速信息
- accelerationIncludingGravity:對象,包含x、y和z屬性,反映各個維度的加速信息,包含z軸自然重力加速度
- interval:毫秒,距離下次觸發事件的時間。此值在事件之間應為常量。
- rotationRate:對象,包含alpha、beta和gamma屬性,表示設備朝向。
如果無法提供acceleration、accelerationIncludingGravity、rotationRate信息,則屬性值為null。=> 使用之前必須先檢測
測試iPhone8(iOS11.4.1)平放在桌面上也一直監聽到變動,測試Android(一加三)平放在桌面上也一直監聽到變動
觸摸及手勢事件
只適用於觸屏設備。
Webkit為Android定製了很多專有事件,成為了事實標準,並被納入W3C的Touch Events規範。
-
觸摸事件
如下幾種:
- touchstart:手指放到屏幕上時觸發
- touchmove:手指在屏幕上滑動時連續觸發。在此事件中調用preventDefault()可以阻止滾動(測試並不能)
- touchend:手指從屏幕上移開時觸發
- touchcancel:系統停止跟蹤觸摸時觸發。文檔未明確什麼情況下停止跟蹤。
都會冒泡,都可以被取消。不屬於DOM規範,瀏覽器以兼容DOM的方式實現它們。每個觸摸事件的event對象都提供了鼠標事件的公共屬性,另外提供以下3個屬性用於跟蹤觸點:
- touches:Touch對象的數組,表示當前屏幕上的每個觸點。
- targetTouches:Touch對象的數組,表示特定於事件目標的觸點。
- changedTouches:Touch對象的數組,表示自上次用户動作之後變化的觸點
每個Touch對象包含一些屬性,可用於追蹤屏幕上的觸摸軌跡。(針對一個觸點)touchend事件觸發時touches集合中什麼也沒有,這是因為沒有滾動的觸點了。
當手指點觸屏幕上的元素時,依次觸發的事件(測試與書本不一致):
1)touchstart
2)touchend
3)mousemove
4)mousedown
5)mouseup
6)click
-
手勢事件
iOS2.0中的Safari中增加。在兩個手指觸碰屏幕且相對距離或旋轉角度變化時觸發。有如下3種:
- gesturestart:一個手指在屏幕上,再把另一手指放到屏幕上時觸發
- gesturechange:任何一個手指在屏幕上的位置發生變化時觸發
- gestureend:其中一個手指離開屏幕時觸發
都會冒泡。
只有在兩個手指同時接觸事件接收者時(目標元素邊界以內),這些事件才會觸發。
觸摸事件和手勢事件存在一定的關係。
每個手勢事件的event對象都包含所有標準的鼠標事件屬性,新增了兩個屬性是rotation和scale。
rotation:表示手指變化旋轉的度數,負值表示逆時針旋轉,正值表示順時針旋轉(從0開始);
scale:表示兩指之間距離變化(對捏)的程度,開始時為1,然後隨着距離增大或縮小相應地增大或縮小。
觸摸事件也會返回rotation和scale屬性,但只在兩個手指觸碰屏幕時才會變化。
其他一些規範中定義的瀏覽器事件
參考書本
內存與性能
在JavaScript中,頁面中事件處理程序的數量與頁面整體性能直接相關。
首先,每個函數都是對象,都佔用內存空間;其次,為指定事件處理程序所需訪問DOM的次數會先期造成整個頁面交互的延遲。
改善頁面性能?
事件委託
”過多事件處理程序“的解決方案是使用事件委託。
利用事件冒泡,可以只使用一個事件處理程序來管理一種類型的事件。只要給所有元素(需要處理某種事件的元素)共同的祖先節點添加一個事件處理程序,就可以解決問題(根據target判斷進行不同的處理)。=> 只訪問了一個DOM元素和添加了一個事件處理程序。 => 佔用內存更少,所有使用按鈕的事件(大多數鼠標事件和鍵盤事件)都適用於這個解決方案。
只要可行,就應該考慮只給document添加一個事件處理程序,通過它處理頁面中所有某種類型的事件。優點如下:
- document對象隨時可用。=> 只要頁面渲染出可點擊的元素,就可以無延遲地起作用。
- 既可以節省DOM引用,也可以節省時間(設置頁面事件處理程序的事件)。
- 減少整個頁面所需的內存,提升整體性能。
最適合使用事件委託的事件包括:click、mousedown、mouseup、keydown和keypress。
刪除事件處理程序
把事件處理程序指定給元素後,在瀏覽器代碼和負責頁面交互的JavaScript代碼之間就建立了聯繫。這種聯繫建立得越多,頁面性能就越差。除了使用事件委託減少這種聯繫外,還應及時刪除不用的事件處理程序。
很多Web應用性能不佳都是由於無用的事件處理程序長駐內存導致的。原因如下:
-
刪除帶有事件處理程序的元素。如使用方法removeChild()或replaceChild()刪除節點,或使用innerHTML整體替換頁面的某一部分。=> 被刪除的元素上若有事件處理程序,就不會被垃圾收集程序正常清理。(特別是IE8及更早版本,元素的引用和事件處理程序的引用)
如果知道某個元素會被刪除,那麼最好在刪除它之前手工刪除它的事件處理程序(或者不直接給它添加事件處理程序,使用事件委託)。=>確保內存被回收,元素也可以安全地從DOM中刪掉。
注意:在事件處理程序中刪除元素會阻止事件冒泡。只有事件目標仍然存在於文檔中時,事件才會冒泡。
-
頁面卸載導致內存中殘留引用。事件處理程序沒有被清理,會殘留在內存中。
最好在onunload事件處理程序中趁頁面尚未卸載先刪除所有事件處理程序。=> 使用事件委託的優勢:事件處理程序很少。
模擬事件
通常事件都是由用户交互或瀏覽器功能觸發。
可以通過JavaScript在任何時候觸發任意事件 => 在測試Web應用時特別有用
DOM3規範指明瞭模擬特定類型事件的方式。
DOM事件模擬
步驟:
-
使用document.createEvent()方法創建一個event對象。
createEvent()方法接收一個參數,一個表示要創建事件類型的字符串。DOM2是英文複數形式,DOM3中是英文單數形式。可用值為以下之一:
- ”UIEvents“(DOM3是”UIEvent“):通用用户界面事件(鼠標事件和鍵盤事件都繼承於此)
- ”MouseEvents“(DOM3是”MouseEvent“):通用鼠標事件
- ”HTMLEvents“(DOM3中無):通用HTML事件(已分散到其他事件大類中)
- 鍵盤事件(DOM3 Events中增加)
- “Events”:通用事件
- “CustomEvent”(DOM3中增加):自定義事件
-
使用事件相關的信息來初始化
每種類型的event對象都有特定的方法,取決於調用createEvent()時傳入的參數
-
觸發事件
事件目標調用dispatchEvent()方法。該方法存在於所有支持事件的DOM節點上。
接收一個參數,即要觸發事件的event對象
- 冒泡並觸發事件處理程序執行
不同事件類型的模擬:
-
鼠標事件
- 調用createEvent()並傳入”MouseEvents“參數
- 調用返回的event對象的initMouseEvent()方法,為新對象指定鼠標的特定信息。接收15個參數,分別對應鼠標事件會暴露的屬性,如type、bubbles、cancelable、view等,這四個是正確模擬事件唯一重要的幾個參數,因為瀏覽器要用到,其他參數則是事件處理程序要用的。
- event對象的target屬性會自動設置為調用dispatchEvent()方法的節點
所有鼠標事件都可以在DOM合規的瀏覽器中模擬出來
-
鍵盤事件
- DOM3中創建鍵盤事件的方式是給createEvent()方法傳入參數”KeyboardEvent“
- 調用返回的event對象的initKeyboardEvent()方法。接收8個參數,包括type、bubbles、cancelable、view等。
DOM3 Events中廢棄了keypress事件,因此只能通過上述方式模擬keydown和keyup事件。
在使用
document.createEvent("KeyboardEvent")之前,最好檢測一下瀏覽器對DOM3鍵盤事件的支持情況document.implementation.hasFeature("KeyboardEvents", "3.0")。測試Chrome,調用initKeyboardEvent()方法傳入的key和modifier參數與在事件處理程序中打印出來的屬性不一致,可以使用
new KeyboardEvent()(參數與在事件處理程序中打印出的一致),另,兩種模擬都不會使文本框中有內容FF限定:
- 給createEvent()傳入”KeyEvents“來創建鍵盤事件
- 調用event對象的initKeyEvent()方法。接收10個參數,包括type、bubbles、cancelable、view等。
測試:ff(88.0)顯示不支持 Uncaught DOMException: Operation is not supported
其他不支持鍵盤事件的瀏覽器:
- 創建一個通用的事件,給createEvent()方法傳入參數”Events“
- 調用event對象的initEvent()方法
- 通過event.xxx方式指定特定於鍵盤的信息
必須使用通用事件而不是用户界面事件,因為用户界面事件不允許直接給event對象添加屬性
-
其他事件
HTML事件:
- 調用createEvent()方法並傳入“HTMLEvents”
- 調用返回的event對象的initEvent()方法來初始化信息
測試:模擬focus,能監聽到事件,但是沒有光標
-
自定義DOM事件
DOM3新增自定義事件類型。不觸發原生DOM事件。
- 調用createEvent()並傳入參數“CustomEvent”
- 調用返回的event對象的initCustomEvent()方法,接收4個參數:type、bubbles、cancelable、detail
- 調用dispatchEvent()
IE事件模擬
在IE8及更早版本中模擬事件。
步驟:
- 使用document對象的createEventObject()方法來創建event對象。不接收參數,返回一個通用event對象
- 手工給返回的對象指定希望該對象具備的所有屬性。(無初始化方法)可指定任何屬性,包括IE8及更早版本不支持的屬性。這些屬性值對於事件來説並不重要,只有事件處理程序才會使用它們。
- 在事件目標上調用fireEvent()方法。接收2個參數:事件處理程序的名字和event對象。srcElement和type屬性會自動指派到event對象。
IE支持的所有事件都可以通過相同的方式來模擬。
小結
最常見的事件是在DOM3 Events規範或HTML5中定義的。
需要考慮內存與性能問題:
-
限制頁面中事件處理程序數量。=> 避免佔用過多內存導致頁面響應慢,清理起來更方便
- 使用事件委託
- 在頁面卸載或元素從頁面刪除前,清理掉相關的事件處理程序