深入理解JavaScript 事件


  • 本文總結自《JavaScript高級程序設計》以及自己平時的經驗,針對較新瀏覽器以及 DOM3 級事件標准(2016年8月),對少部分內容作了更正,增加了各種例子及解析。
  • 如無特殊說明,本文后的文字引用和圖片引用均來自《JavaScript高級程序設計》,引用稍有改變原文,不改變意思。
  • 本文僅作鞏固基礎之用,如果有不正確的地方,還望指出。
  • 更好的排版可以點這里

事件

個人認為:不論是瀏覽器自帶的事件,還是自定義事件,都是觀察者模式的實現。更確切地說:事件流是會流動的,流到哪個節點,事件在哪里發生,事件發生時,節點便會調用在這個節點綁定的事件處理程序。節點是被觀察者,事件處理程序是觀察者,當事件流流到被觀察者時,被觀察者會對外宣稱“我這里發生了某個事件”,即通知觀察者,也就是節點調用事件處理程序。事件流是不知道被觀察者有多少個的,所以即使是0個,事件流也會繼續流,流到節點時,節點會遍歷自己注冊的事件處理程序,存在就調用。具體瀏覽器的實現和優化肯定更加復雜和精妙,但原理應該是這樣(以上為個人理解)。

事件流

事件流分為事件冒泡和事件捕獲:

  • 如果你把手指放在圓心上,那么你的手指指向的不是一個圓,而是紙上的所有圓。在瀏覽器上單擊按鈕的同時,你也單擊了按鈕的容器元素,甚至也單擊了整個頁面。事件流描述的是從頁面中接收事件的順序
  • IE開發團隊提出了事件冒泡流、Netscape開發團隊提出了事件捕獲流。

事件冒泡

  • 事件開始時由最具體的元素(文檔中嵌套層次最深的那個節點)接收,然后逐級向上傳播到較為不具體的節點,所有現代瀏覽器都支持事件冒泡,除IE5.5外,均一直冒泡到window。
  • 事件冒泡示意圖:

    事件冒泡示意圖

事件捕獲

  • 不太具體的節點應該更早接收到事件,而最具體的節點應該最后接收到事件。事件捕獲的用意在於在事件到達預定目標之前捕獲它。IE9+、Safari、Chrome、Opera和Firefox支持,且從window開始捕獲(盡管DOM2 級事件規范要求從document)。
  • 事件捕獲示意圖:

    事件捕獲示意圖
  • 由於老版本的瀏覽器不支持,因此很少有人使用事件捕獲。我們也建議讀者放心地使用事件冒泡,在有特殊需要時再使用事件捕獲
  • 為了徹底理解事件冒泡和捕獲,這里寫了個例子:

    <!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <title>test1</title> <link rel="stylesheet" href="test1.css"> </head> <body> <div id="a"> <div id="b"> <div id="c"></div> </div> </div> <script src="test1.js"></script> </body> </html>
#a{ width: 300px; height: 300px; background: pink; } #b{ width: 200px; height: 200px; background: blue; } #c{ width: 100px; height: 100px; background: yellow; }
var a = document.getElementById("a"), b = document.getElementById("b"), c = document.getElementById("c"); c.addEventListener("click", function(event){ console.log("c1") // 注意第三個參數沒有傳進 false , 因為默認傳進來的是 false,代表冒泡階段調用,個人認為處於目標階段也會調用的 }); c.addEventListener("click", function(event){ console.log("c2"); }, true); b.addEventListener("click", function(event){ console.log("b"); }, true); a.addEventListener("click", function(event){ console.log("a1"); }, true); a.addEventListener("click", function(event){ console.log("a2") }); a.addEventListener("click", function(event){ console.log("a3"); event.stopImmediatePropagation(); }, true); a.addEventListener("click", function(event){ console.log("a4"); }, true);
  • 效果如圖
    事件流示例

  • 點擊 c 或 b,輸出:a1、a3
  • stopImmediatePropagation 包含了 stopPropagation 的功能,即阻止事件傳播(捕獲或冒泡),但同時也阻止該元素上后來綁定的事件處理程序被調用,所以不輸出 a4,因為事件捕獲被攔截了,自然不會觸發 b、c 上的事件,所以不輸出 b、c1、c2,冒泡更談不上了,所以不輸出 a2。有人會覺得上面的表述有一點點問題,為什么捕獲被攔截了,c1 就不輸出了呢? c1 應該是冒泡階段被調用的呀,所以應該改為另一個表述:“...冒泡更談不上,所以不輸出 c1、a2”。但另一個表述是錯的,下面會分析到。
  • 點擊 a,輸出 a1、a2、a3
  • 不應該是 a1、a3、a2 嗎?a1、a3 可是在捕獲階段被調用的處理程序啊,a2 是在冒泡階段被調用的啊。這里正是要說明的:雖然這三個事件處理程序注冊時指定了true、false,但現在事件流是處於目標階段,不是冒泡階段、也不是捕獲階段,事件處理程序被調用的順序是注冊的順序。不論你指定的是 true or false. 這也解釋了上面提到的“另一種表述”為什么是錯誤的。
  • 更深一步解釋是:要區分事件流和事件處理程序,不論事件處理程序存不存在,事件流都會傳播。這是一個觀察者模式,綁定事件的節點是被觀察者、事件處理程序是觀察者,事件流是不知道觀察者的存在的,所以你點擊頁面的時候,事件流一定要傳播,傳播到某一個節點時,節點去通知所有觀察者,也就是調用事件處理程序(有可能觀察者不存在)。
  • 當一個事件流來到一個節點時,事件流可能在捕獲階段(正在流向最深層次的節點)、可能在處於目標階段(已經流到了目標,也就是event.target)、也可能在冒泡階段(正在流向最外層節點)。而事件處理程序是這么處理的:① 注冊時第三個參數指定為 true 時,事件流到來,如果事件流是捕獲階段或處於目標階段,則調用該事件處理程序。②注冊時第三個參數指定為 false 時,當事件流到來,如果事件流是處於目標階段或冒泡階段,則調用該事件處理程序。
  • 所以當事件流是處於目標階段,那么不管事件處理程序第三個參數指定的true or false,事件處理程序都會被調用,調用順序按照注冊順序。所以點擊a,輸出 a1、a2、a3,而不是a1、a3、a2。
  • 注釋掉 event.stopImmediatePropagation,點擊 c,輸出 a1、a3、a4、b、c1、c2、a2

  • 另外,如果同一個事件處理程序(指針相同,比如用 handler 保存的事件處理程序),用 addEventListener 或 attachEvent 綁定多次,如果第三個參數是相同的話,也只會被調用一次。但如果第三個參數一個設置為true,另一個設置為false,那么會被調用兩次。

DOM事件流

  • “DOM2級事件”規定的事件流包括三個階段:事件捕獲階段、處於目標階段和事件冒泡階段。首先發生的是事件捕獲,為截獲事件提供了機會。然后是實際的目標接收到事件。最后一個階段是冒泡階段。(事件處理中“處於目標階段”被看成冒泡階段的一部分)。
  • IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都會在捕獲階段觸發事件對象上的事件,就是有兩個機會在目標對象上面操作事件。(盡管DOM2級事件規范明確要求捕獲階段不涉及事件目標)。

事件處理程序

HTML 事件處理程序

簡單來講,HTML 事件處理程序是直接在HTML中綁定事件,如下

<input type="button" value="Click Me" onclick="alert(&quot;Clicked&quot;)" />

注意事項:

  • 不能在其中使用未經轉義的HTML語法字符,如&“”<>,因為這是在HTML中綁定的,會造成瀏覽器解析DOM結構錯誤。
  • 擴展函數作用域,來看下面的代碼:

    <!-- 輸出 "Click Me、lzh" --> <form method="post"> <input type="text" name="username" value="lzh"> <input type="button" value="Click Me" onclick="alert(value);alert(username.value);"> </form>

    如果當前元素是一個表單輸入元素,瀏覽器內部大概是這樣實現的:

    function () { with (document) { with (this.form) { with (this) { //元素屬性值 } } } }
    如果沒有form元素,調用username會報錯,所以不論是服務端渲染還是Ajax請求回來數據再渲染,最好還是把form結構寫完整。
    擴展作用域有三個缺點:
  1. 函數被調用時還沒定義會報錯,只好try{}catch(ex){},分離的寫法可以在DOMContentLoaded之后再綁定。
  2. 擴展的作用域鏈在不同瀏覽器中會導致不同的結果。
  3. HTML 與 JavaScript 代碼緊密耦合,如果要更換事件處理程序,需要改動 HTML 代碼和 JavaScript代碼。

DOM0級事件處理程序

  • 每個元素(包括window 和document)都有自己的事件處理程序屬性,這些屬性通常全部小寫。使用 DOM0 級指定的事件處理程序被認為是元素的方法。this 引用當前元素。通過 this 可以訪問元素的任何屬性和方法。DOM0 級事件處理程序在冒泡階段被處理。
var btn = document.getElementById("myBtn"); btn.onclick = function () { alert(this.id); //"myBtn" };

DOM2級事件處理程序

  • addEventListener() 包含三個參數,要處理的事件名、事件處理函數、布爾值,布爾值為true,表示在捕獲階段調用事件處理程序,反之在冒泡階段調用。
  • DOM2 級事件處理程序中的 this 也指向 addEventListener 的那個元素。
  • 可以添加多個事件處理程序,按添加順序依次調用。
  • removeEventListener 無法移除匿名函數的事件處理程序。
var btn = document.getElementById("myBtn"); var handler = function () { alert(this.id); }; btn.addEventListener("click", handler, false); //這里省略了其他代碼 btn.removeEventListener("click", handler, false); // 有效!
  • IE9、Firefox、Safari、Chrome 和Opera 支持DOM2 級事件處理程序。

IE事件處理程序

  • attachEvent detachEvent 接收兩個參數,事件處理程序名稱、事件處理程序函數。由於IE8及更早版本只支持事件冒泡,所以該事件處理程序只支持事件冒泡。
  • 老版本的Opera支持這種方法,但現在Opera已經改用blink內核,IE11已經不支持這種方法,注意 IE9 就已經支持 DOM2 級事件處理程序了。
  • 特別要注意:第一個參數包含on,比如onclick。
  • 區別於DOM0 級事件處理程序,this 指向 'window'。
  • 也可以添加多個事件處理程序。

跨瀏覽器的事件處理程序

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 { 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 { element["on" + type] = null; } } };
  • 存在問題:
  1. IE事件處理程序 中的 this 指向 window
  2. 只支持 DOM0 級的瀏覽器不能多次添加事件處理程序,不過這種瀏覽器應該不多了,即使是IE8 也支持attachEvent。
  3. 會不會有一些事件,在瀏覽器支持 DOM2 級事件處理程序的情況下,那些事件只能用 on + name 的形式呢? 之前一直懷疑 (1).xhr.onreadystatechange() 和 (2).DOMNodeInserted 事件,這里我多慮了,經過驗證,(1).是支持 DOM2 級事件的,(2).天生就是 DOM2 級的。這里只是為了打消我的疑慮,記錄下來。

事件對象

DOM 中的事件對象

  • 兼容 DOM 的瀏覽器會將一個 event 對象傳入事件處理程序, IE9 及更高版本可以。無論指定事件處理程序時使用什么方法(DOM0 級 DOM2 級),HTML 事件處理程序可以通過訪問 event 變量得到 event 對象。
  • event 中的屬性和方法都是只讀的
  • 常用屬性:
  1. target 事件的目標
  2. currentTarget 綁定事件的元素,與 'this' 的指向相同
  3. stopPropagation() 取消事件的進一步捕獲或冒泡。如果bubbles為true,則可以使用這個方法
  4. stopImmediatePropagation() 取消事件的進一步捕獲或冒泡,同時阻止任何事件處理程序被調用(DOM3級事件中新增)
  5. preventDefault() 取消事件的默認行為,比如點擊鏈接跳轉。如果 cancelable 是 true,則可以使用這個方法
  6. type 被觸發的事件類型
  7. eventPhase 調用事件處理程序的階段:1表示捕獲階段,2表示“處於目標”,3表示冒泡階段
  • this target currentTarget 舉例:
document.body.onclick = function(event){ alert(event.currentTarget === document.body); //true alert(this === document.body); //true alert(event.target === document.getElementById("myBtn")); //true };
  • 通過 event.type 與 switch case 組合,可以通過一個函數處理多個事件。
  • 只有在事件處理程序執行期間,event 對象才會存在;一旦事件處理程序執行完成,event 對象就會被銷毀。

IE 中的事件對象

  • DOM0 級的事件處理程序,event 作為 window 的一個屬性存在。(從 IE9 開始,event 可以從參數中獲得)
  • attachEvent 添加的事件處理程序,event 作為參數傳入,也可以通過 window 來訪問 event 對象。
  • HTML 事件處理程序依然可以通過訪問 event 變量得到 event 對象。
  • 屬性和方法:
  1. cancelBubble 設置 true or false 可以取消事件冒泡
  2. returnValue 設置 true or false 可以取消事件的默認行為。
  3. srcElement 事件的目標(與DOM中的 target 相同)
  • 注意事項:
  1. attachEvent 中的 event.srcElement === this 嗎? 答案是否定的,因為前面說到過 attachEvent 中 this 指向 window, DOM0 級、DOM2 級 事件處理程序 this 才指向 event.target / window.event.srcElement

跨瀏覽器的事件對象

var EventUtil = { getEvent: function(event){ return event ? event : window.event; // window.event DOM0級時IE }, getTarget: function(event){ return event.target || event.srcElement; // event.srcElement for IE }, preventDefault: function(event){ if (event.preventDefault){ event.preventDefault(); } else { event.returnValue = false; // IE } }, stopPropagation: function(event){ if (event.stopPropagation){ event.stopPropagation(); } else { event.cancelBubble = true; // IE } } };

事件類型

  • DOM3 級事件規定了幾類事件;HTML5 也定義了一組事件;還有一些事件沒有規范,瀏覽器的實現不一致。
  • DOM3 級事件模塊在 DOM2 級事件模塊基礎上重新定義了這些事件,也添加了一些新事件。包括 IE9 在內的所有主流瀏覽器都支持 DOM2 級事件。IE9 也支持 DOM3 級事件。

這里只總結一些常見的事件類型

UI事件類型

  • load 事件,當頁面完全加載后(包括所有圖像、JavaScript 文件、CSS 文件等外部資源),就會觸發 window 上面的 load 事件。
EventUtil.addHandler(window, "load", function(){ var image = document.createElement("img"); EventUtil.addHandler(image, "load", function(event){ event = EventUtil.getEvent(event); alert(EventUtil.getTarget(event).src); }); document.body.appendChild(image); image.src = "smile.gif"; //在此之前要先指定事件處理程序 });
  1. script 元素也會觸發 load 事件,據此可以判斷動態加載的 JavaScript 文件是否加載完畢。與圖像不同,只有在設置了 script 元素的 src 屬性並將該元素添加到文檔后,才會開始下載 JavaScript 文件
  2. IE8 及更早版本不支持 script 元素上的 load 事件。
  3. 在不屬於 DOM 文檔的圖像(包括未添加到文檔的 img 元素和 Image 對象)上觸發 load 事件時,IE8 及之前版本不會生成 event 對象。IE9 修復了這個問題。
  • resize 事件
  1. 瀏覽器窗口大小發生變化時會觸發該事件,這個事件在 window 上觸發,IE、Safari、Chrome 和 Opera 會在瀏覽器窗口變化了 1 像素時就觸發 resize 事件,然后隨着變化不斷重復觸發。Firefox 則只會在用戶停止調整窗口大小時才會觸發。
  2. 注意不要在這個事件的處理程序中加入大計算量的代碼,或者采用函數節流的方式優化性能。
  3. 瀏覽器窗口最小化或最大化時也會觸發 resize 事件。
  • scroll 事件
  1. 該事件在 window 上發生,此處和書上講的有點不一樣,webkit 內核或 blink 內核的瀏覽器(Chrome、Opera、Safari)可以通過 document.body.scrollTop 獲取頁面被卷去的高度,而 Trident、Gecko (IE、火狐)可以通過 document.documentElement.scrollTop來獲取該值。
  2. 另外標准模式、混雜模式這兩種方法還有出入,此處不討論。
  3. 所以最好通過 document.body.scrollTop + document.documentElement.scrollTop 的方式獲取 scrollTop 的值,因為兩者之一會等於0,或者使用 document.body.scrollTop || document.documentElement.scrollTop,兩者效果一致。

焦點事件

  1. 這里忽略 DOMFocusIn、DOMFocusOut,因為只有 Opera 支持這個事件,且 DOM3 級事件廢棄了它們。
  2. blur:在元素失去焦點時觸發。這個事件不會冒泡;所有瀏覽器都支持它。
  3. focus:在元素獲得焦點時觸發。這個事件不會冒泡;所有瀏覽器都支持它。
  4. focusin:與 focus 等價,但它冒泡。
  5. focusout:與 blur 等價,也冒泡。
  6. 支持 focusin、focusout 的瀏覽器有:IE5.5+、Safari 5.1+、Opera 11.5+和Chrome。但只支持 DOM2 級事件處理程序
  7. Firefox 不支持 focusin、focusout
  8. blur、focusout 的事件目標是失去焦點的元素;focus、focusin 的事件目標是獲得焦點的元素

鼠標與滾輪事件

  • click 在用戶單擊住鼠標按鈕或按下回車鍵時觸發。 觸發順序 mousedown mouseup click,如果 mousedown、mouseup 其中之一被取消,就不會觸發 click 事件。
  • dblclick 觸發順序 mousedown mouseup click mousedown mouseup click dblclick, 如果中間有事件被取消,dblclick 也不會被觸發
  • mousedown 用戶按下了任意鼠標按鈕時觸發。
  • mouseup 用戶釋放按鈕時觸發
  • mouseenter 在鼠標光標從元素外部首次移動到元素范圍之內時觸發。不冒泡,而且在光標移動到后代元素上不會觸發。DOM2 級事件並沒有定義這個事,但 DOM3 級事件將它納入了規范。IE、Firefox9+和Opera支持這個事件。
  • mouseleave 在位於元素上方的鼠標光標移動到元素范圍之外時觸發。不冒泡,而且在光標移動到后代元素上不會觸發。DOM2 級事件並沒有定義這個事,但 DOM3 級事件將它納入了規范。IE、Firefox9+ 和 Opera 支持這個事件。
  • mouseover 在鼠標指針位於一個元素外部,然后用戶將其首次移入另一個元素邊界之內時觸發。不能通過鍵盤觸發這個事件。
  • mouseout 在鼠標指針位於一個元素上方,然后用戶將其移入另一個元素時觸發。又移入的另一個元素可能位於前一個元素的外部,也可能是這個元素的子元素。不能通過鍵盤觸發這個事件。
  • 用代碼說明一下 mouseenter、mouseleave 和 mouseover、mouseout 的區別:
<!DOCTYPE html> <html lang="zh-cn"> <head> <title>test1</title> <meta charset="utf-8"> <link rel="stylesheet" type="text/css" href="test1.css"> </head> <body> <div class="mouseover"> <div class="sub-mouseover"> </div> </div> <div class="mouseenter"> <div class="sub-mouseenter"> </div> </div> <script src="test1.js"></script> </body> </html>
.wrap { width: 200px; height: 100px; } .mouseover { background: pink; } .mouseenter { margin-top: 30px; background: gray; } .sub-mouseover, .sub-mouseenter { width: 100px; height: 50px; background: #AE81FF; }
var div1 = document.querySelector(".mouseover"), div2 = document.querySelector(".mouseenter"); div1.addEventListener("mouseover", function(){ console.log("div1 mouseover"); }); div1.addEventListener("mouseout", function(){ console.log("div1 mouseout"); }) div2.addEventListener("mouseenter", function(){ console.log("div2 mouseenter"); }) div2.addEventListener("mouseleave", function(){ console.log("div2 mouseleave"); })
  • 效果圖
    mouseenter-mouseover

  • 鼠標由左側從上到下依次經過所有 div 的情況,輸出 div1 mouseover div1 mouseout div1 mouseover div1 mouseout div2 mouseenter div2 mouseleave

  • mousemove 當鼠標指針在元素內部移動時重復地觸發。不能通過鍵盤觸發這個事件。
  • 除了 mouseenter、mousedleave,所有鼠標事件都會冒泡,取消鼠標事件將會影響瀏覽器的默認行為,也會影響其它事件,因為鼠標事件與其它事件是密不可分的。
  • 關於 dblclick IE8 及之前版本中的實現有一個小bug,因此在雙擊事件中,會跳過第二個mousedown 和click事件,其順序如下:mousedown mouseup click mouseup dblclick,但還是會觸發 dblclick 事件
  • 客戶區坐標位置:鼠標事件中的 event 都有 clientX clientY 屬性,表示在視口中客戶區的坐標位置,這些值不包括頁面滾動的距離,因此這個位置並不表示鼠標在頁面上的位置:
    坐標示例
  • 頁面坐標位置:pageX、pageY,這兩個屬性表示鼠標光標在頁面中的位置,在頁面沒有滾動的情況下,pageX 和 pageY 的值與 clientX、clientY 的值相等。IE8 及更早版本不支持事件對象上的頁面坐標,不過使用客戶區坐標和滾動信息可以計算出來。這時候需要用到document.body(混雜模式)或document.documentElement(標准模式)中的scrollLeft 和scrollTop 屬性。計算過程如下所示:
var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "click", function(event){ event = EventUtil.getEvent(event); var pageX = event.pageX, pageY = event.pageY; if (pageX === undefined){ pageX = event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft); } if (pageY === undefined){ pageY = event.clientY + (document.body.scrollTop || document.documentElement.scrollTop); } alert("Page coordinates: " + pageX + "," + pageY); });
  • 屏幕坐標位置:screenX、screenY
  • 修改鍵 用戶按住Shift、Ctrl、Alt、Meta(Windows或Cmd,cmd(mac))時觸發鼠標事件,可以在 event 中獲得修改鍵。
var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "click", function(event){ event = EventUtil.getEvent(event); var keys = new Array(); if (event.shiftKey){ keys.push("shift"); } if (event.ctrlKey){ keys.push("ctrl"); } if (event.altKey){ keys.push("alt"); } if (event.metaKey){ keys.push("meta"); } alert("Keys: " + keys.join(",")); });
  • IE9、Firefox、Safari、Chrome 和Opera 都支持這4 個鍵。IE8 及之前版本不支持metaKey 屬性。另外,舊版本的 IE 有自己的一套寫法。
  • 相關元素 mouseover mouseout 時的 event.relatedTarget,不做詳細記錄。
  • 鼠標按鈕 mousedown mouseup 是在按下/釋放任意鼠標按鈕時觸發的,所以通過 event.button: 0(左) 1(中) 2(右) 可以判斷按的是哪個鍵,但是IE8 及更低版本的瀏覽器不支持,有兼容寫法,此處不詳細敘述。EventUtil.getButton 有詳細實現。
  • mousewheel event.whellDelta 為正數時,向前滾動(回到頂部、頁面向下滑動),負數則反過來,這個值是120的倍數,Opera低版本中正負相反,火狐中有自己的一套方法,這里不做詳細記錄。
  • 觸摸設備
  • 不支持dblclick 事件。雙擊瀏覽器窗口會放大畫面,而且沒有辦法改變該行為。
  • 輕擊可單擊元素會觸發mousemove 事件。如果此操作會導致內容變化,將不再有其他事件發生;如果屏幕沒有因此變化,那么會依次發生mousedown、mouseup 和click 事件。輕擊不可單擊的元素不會觸發任何事件。可單擊的元素是指那些單擊可產生默認操作的元素(如鏈接),或者那些已經被指定了onclick 事件處理程序的元素。
  • mousemove 事件也會觸發mouseover 和mouseout 事件。
  • 兩個手指放在屏幕上且頁面隨手指移動而滾動時會觸發mousewheel 和scroll 事件。
  • 無障礙性問題
  • 如果需要考慮這個問題,不建議使用 click 之外的鼠標事件。因為這個不能通過鍵盤觸發,不利於屏幕閱讀器訪問。此處不詳細記錄。

鍵盤與文本事件

  • keydown: 當用戶按下鍵盤上的任意鍵時觸發,而且如果按住不放的話,會重復觸發此事件。
  • keypress 當用戶按下鍵盤上的字符鍵時觸發,而且如果按住不放的話,會重復觸發此事件。按下Esc 鍵也會觸發這個事件。Safari 3.1 之前的版本也會在用戶按下非字符鍵時觸發keypress事件。
  • keyup:當用戶釋放鍵盤上的鍵時觸發。
  • 觸發順序:keydownkeypresskeyupkeydownkeypress 都是在文本框發生變化之前被觸發的; keyup 事件則是在文本框已經發生變化之后被觸發的。
  • 如果用戶按下了一個字符鍵不放,就會重復觸發 keydown 和keypress 事件,直到用戶松開該鍵為止。
  • 鍵盤事件也支持修改鍵(ctrl等)
  • keydown、keyup 中的 event 有 keyCode, 與ASCII 碼中對應小寫字母或數字的編碼相同。
  • keypress 中的 event 有 charCode,這個值是按下的那個鍵所代表字符的 ASCII 編碼,用 String.fromCharCode() 可以轉換成實際的字符
  • DOM3 級中,有 key 和 char,其中 key 可以直接得到 "k"、"K"、"Shift" 等, char 屬性在按下字符鍵時行為與 key 相同,在按下非字符鍵時為 null,但是支持還不完整,chrome 總是輸出 undefined。
  • keyIdentifier Chrome 已經不推薦使用
  • 表示按下的按鍵在鍵盤的位置,比如按下左右側的shift鍵,這個值就不同,Chrome 和 Safari 的實現有 bug。
  • textInput: 在文本插入文本框之前會觸發textInput 事件。目的是代替keypress,退格鍵不會觸發textInput,但是會觸發keypress(只要改變文本),只有真正可以編輯的區域才會觸發textInput,但是keypress獲得焦點即可觸發。event.data中包含用戶的輸入,拼音輸入法中輸入過程的拼音不會觸發該事件。
  • inputMethod 代表用戶是怎樣輸入的,比如通過粘貼的方式,但是支持的瀏覽器很少。

變動事件

DOM2 級的變動(mutation)事件能在 DOM 中的某一部分發生變化時給出提示,比如 DOM 節點的插入、移除、特性被修改等等

HTML5 事件

  1. contextmenu 事件
EventUtil.addHandler(window, "load", function(event){ var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "contextmenu", function(event){ event = EventUtil.getEvent(event); EventUtil.preventDefault(event); var menu = document.getElementById("myMenu"); menu.style.left = event.clientX + "px"; menu.style.top = event.clientY + "px"; menu.style.visibility = "visible"; }); EventUtil.addHandler(document, "click", function(event){ document.getElementById("myMenu").style.visibility = "hidden"; }); });
  1. beforeunload 事件,用戶關閉標簽頁時提示
EventUtil.addHandler(window, "beforeunload", function(event){ event = EventUtil.getEvent(event); var message = "I'm really going to miss you if you go."; event.returnValue = message; return message; });
  1. DOMContentLoaded 在形成完整DOM樹之后就會觸發,不理會圖像、JavaScript 文件、CSS 文件或其它資源是否已經下載完畢。其實更應該使用 DOMContentLoaded 而不是 window.onload:
EventUtil.addHandler(window, "DOMContentLoaded", function(event){ alert("Content loaded."); }); EventUtil.addHandler(window, "load", function(event){ alert("Window loaded."); });
  • IE9+、Firefox、Chrome、Safari 3.1+ 和 Opera9+ 都支持 DOMContentLoaded 事件。
  1. readystatechange 事件,略。
  2. pageshow 和 pagehide 事件,此處要了解 Firefox 和 Opera 有一個特性叫 “往返緩存”(back-forward cache/bfcache),用戶點擊“前進”、“后退”按鈕時,會將頁面緩存在內存。不重新加載,JavaScript的狀態會保留。但是無論頁面是否來自 bfcache,都會觸發 pageshow 事件,pageshow 的事件處理程序的 event 對象中有 event.persisted 屬性,為 true 代表頁面來自bfcache,同樣 pagehide 事件觸發時,如果頁面被保存到 bfcache 中,則該屬性為 true。支持pageshow、pagehide 事件的瀏覽器有 Firefox、Safari5+、Chrome 和 Opera。 IE9 及以前的版本不支持這兩個事件。指定了 onunload 事件處理程序的頁面會被自動排除在 bfcache 之外。
  3. hashchange 事件。在 window 上觸發,event 包含 oldURL、newURL 兩個屬性。支持該事件的有 IE8+、Firefox3.6+、Safari5+、Chrome 和 Opera10.6+,但oldURL、newURL只有Firefox6+、Chrome和Opera支持。所以最好用 location 來指定當前的 hash:
EventUtil.addHandler(window, "hashchange", function(event){ console.log(location.hash); })

設備事件

  • orientationchange 事件,屏幕轉動。

觸摸與手勢事件

  • touchstart: 當手指觸摸屏幕時觸發;即使已經有一個手指放在了屏幕上也會觸發。
  • touchmove: 當手指在屏幕上滑動時連續地觸發。在這個事件發生期間,調用preventDefault() 可以阻止滾動。
  • touchend:當手指從屏幕上移開時觸發。
  • touchcancel:當系統停止跟蹤觸摸時觸發。關於此事件的確切觸發時間,文檔中沒有明確說明。
  • event 對象中包含的常見 DOM 屬性有:bubbles、cancelable、view、clientX、clientY、screenX、screenY、detail、altKey、shiftKey、ctrlKey 和metaKey。
  • event 對象中還包含以下用於跟蹤觸摸的屬性:
  1. touches:表示當前跟蹤的觸摸操作的Touch 對象的數組。
  2. targetTouchs:特定於事件目標的Touch 對象的數組。
  3. changeTouches:表示自上次觸摸以來發生了什么改變的Touch 對象的數組。每個Touch 對象包含下列屬性:clientX、clientY、pageX、pageY、screenX、screenY、target、identifier(標識觸摸的唯一ID)
function handleTouchEvent(event) { //only for one touch if (event.touches.length == 1) { var output = document.getElementById("output"); switch (event.type) { case "touchstart": output.innerHTML = "Touch started (" + event.touches[0].clientX + "," + event.touches[0].clientY + ")"; break; case "touchend": output.innerHTML += "<br>Touch ended (" + event.changedTouches[0].clientX + "," + event.changedTouches[0].clientY + ")"; break; case "touchmove": event.preventDefault(); //prevent scrolling output.innerHTML += "<br>Touch moved (" + event.changedTouches[0].clientX + "," + event.changedTouches[0].clientY + ")"; break; } } }
  • 一次觸摸的事件觸發順序為:touchstart、mouseover、mousemove(一次)、mousedown、mouseup、click、touchend
  • 手勢事件:
  1. gesturestart:當一個手指已經按在屏幕上而另一個手指又觸摸屏幕時觸發。
  2. gesturechange:當觸摸屏幕的任何一個手指的位置發生變化時觸發。
  3. gestureend:當任何一個手指從屏幕上面移開時觸發。
  • 屬性有標准的鼠標事件屬性,還有兩個:rotation(正值表示順時針)和scale(從1開始)

內存和性能

  • 每個函數都是對象,都會占用內存;內存中的對象越多,性能就越差。
  • 必須事先指定所有事件處理程序而導致的 DOM 訪問次數,會延遲整個頁面的交互就緒時間。

事件委托

<body> <ul id="myLinks"> <li id="goSomewhere">Go somewhere</li> <li id="doSomething">Do something</li> <li id="sayHi">Say hi</li> </ul> <script type="text/javascript"> (function(){ var list = document.getElementById("myLinks"); EventUtil.addHandler(list, "click", function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); switch(target.id){ case "doSomething": document.title = "I changed the document's title"; break; case "goSomewhere": location.href = "http://www.wrox.com"; break; case "sayHi": alert("hi"); break; } }); })(); </script> </body>
  • 上面的方法只取得了一個 DOM 元素,只添加了一個事件處理程序,占用的內存更少。
  • 如果將事件委托到 document 中,會更有優勢:
  1. document 對象很快就可以訪問,而且可以在頁面生命周期的任何時點上為它添加事件處理程序(無需等待 DOMContentLoaded 或 load 事件)。
  2. 在頁面中設置事件處理程序所需的時間少。只添加一個事件處理程序所需的 DOM 引用更少,所花的時間也更少。
  3. 整個頁面占用的內存空間更少,能夠提升整體性能。
  • 最適合采用事件委托技術的事件包塊 clickmousedownmouseupkeydownkeyup 和 keypress

移除事件處理程序

  • 如果你知道某個元素即將被移除,那么最好手工移除事件處理程序,因為有的瀏覽器(尤其是 IE)不會作出恰當地處理,它們很有可能會將對元素和對事件處理程序的引用都保存在內存中。
  • IE8 及更早的版本在頁面被卸載(刷新,切換頁面)之前沒有清理干凈事件處理程序,它們會滯留在內存中,可以通過 onunload 事件處理程序移除所有事件處理程序。

模擬事件

  • 在測試 Web 應用程序,模擬觸發事件是一種極其有用的技術。DOM2 級規范為此規定了模擬特定事件的方式,IE9、Opera、Firefox、Chrome 和 Safari 都支持這種方式。IE有它自己模擬事件的方式(IE8 及以下才要用到)

DOM 中的事件模擬

  • 可以在 document 對象上使用 createEvent 方法創建 event 對象。這個方法接收一個參數,即表示要創建的事件類型的字符串。在 DOM2 級中,所有這些字符串都使用英文復數形式,而在 DOM3 級中變成了單數。這個字符串可以是下列幾個字符串之一:
  1. UIEvents,DOM3 級中是 UIEvent
  2. MouseEvents: 一般化的鼠標事件,DOM3 級中是 MouseEvent
  3. MutationEvents: 一般化的 DOM 變動事件。 ...
  4. HTMLEvents 一般化的 HTML 事件。沒有對應的 DOM3 級事件(HTML 事件被分割到其他類別中)

模擬鼠標事件

  • createEvent 方法返回的 event 對象中,有 initMouseEvent() 方法,需要傳 15 個參數。type(比如"click"),bubbles(Boolean) 是否冒泡,應該設置為 true, cancelable(Boolean) 應該設置為 true,view(幾乎總是document.defaultView), detail(通常設置為0), screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button(表示按下了哪個鼠標,默認0), relatedTarget(只有在模擬 mouseover 或 mouseout時使用)
  • 將 event 對象傳給 DOM 節點的 dispatchEvent 方法即可觸發事件,如下:
<body> <input type="button" value="Click me" id="myBtn" /> <input type="button" value="Send click to the other button" id="myBtn2" /> <p>This example works in DOM-compliant browsers (not IE).</p> <script type="text/javascript"> (function(){ var btn = document.getElementById("myBtn"); var btn2 = document.getElementById("myBtn2"); EventUtil.addHandler(btn, "click", function(event){ alert("Clicked!"); alert(event.screenX); //100 }); EventUtil.addHandler(btn2, "click", function(event){ //create event object var event = document.createEvent("MouseEvents"); //initialize the event object event.initMouseEvent("click", true, true, document.defaultView, 0, 100, 0, 0, 0, false, false, false, false, 0, btn2); //fire the event btn.dispatchEvent(event); }); })(); </script> </body>

模擬鍵盤事件

  • "DOM2 級事件"的草案中本來包含了鍵盤事件,但在定稿前又被刪除了;Firefox 根據其草案實現了鍵盤事件。但跟 "DOM3 級事件"中的鍵盤事件有很大區別。
  • DOM3 級規定,調用 createEvent() 並傳入 "KeyboardEvent" ,返回鍵盤事件,有 initKeyEvent() 方法。這個方法接收一下參數
  • type, bubbles, cancelable, view, key(按下的鍵的鍵碼), location(按下了哪里的鍵,0:主鍵盤,1:左,2:右,3:數字鍵盤,4:虛擬鍵盤,5:手柄), modifiers: 空格分隔的修改鍵列表,如 "Shift", repeat(在一行中按了這個鍵多少次)
    DOM3 級不提倡 keypress 事件, 因此只能模擬 keydown keyup

IE 中的事件模擬

第一步:document.createEventObject()
第二步: 通過賦值的方式初始化事件對象,就是 event.screenX = 0 這些
第三步:btn.fireEvent("onclick", event);

關於標准

    • 由於標准在變,現在 DOM3 級事件已經不推薦使用 document.createEvent 的方式,也不推薦通過 event 對象 initKeyEvent或者 initKeybordEvent,書中的跨瀏覽器代碼在狐火中報錯了,因為火狐開始支持 DOM3 級事件,標准又在變,現在 DOM3 級標准推薦通過構造函數的方式初始化模擬事件,但這也還是草案。

    • 關於跨瀏覽器模擬事件,粗略了解一下 jQuery 的做法,使用了很多 hack,讓本來不冒泡的 focus、blur 可以做事件委托,里面的內容還是很多,得另外總結一下。
    • 期待標准被普及的一天:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM