UI Event Order
我一直以來都對於 DOM 事件的順序抱有一種不確定的感覺,舉例來說,當使用者點滑鼠時,你可以預期到會有mousedown
、mouseup
、click
事件,但是它們的順序是什麼呢?可以確定的是 down 一定是第一個,up 一定在 down 之後,那click
是在中間還是最後呢?更進一步,點在可以有 focus 的元件上,那focus
事件的順序呢?如果已經有其他元件有 focus,那它的blur
事件又是在focus
的前面還後面呢?除了滑鼠事件外,鍵盤事件又更複雜,除了keydown
、keyup
之外還有input
、change
和 IME 的 composition 事件等,總之最近實在是太在意了,就認真的弄了個測試網頁自己測試,之後又查找了相關的標準規範,這篇就來記錄一下這些事件的順序。
首先就來說滑鼠(指標裝置)相關的事件順序吧,第一個是滑鼠點擊,就是mousedown
、mouseup
、click
,是在mouseup
後才接著有click
事件,其實仔細想了一下也是蠻合理的,要有 up 事件才代表完成了click
事件。然後這順序其實是有定義在 UI Events 5.3.3 之中的,其中的最後一個表格就是一個標準的點擊時事件的順序,而且這個表格還包括了mousemove
和dblclick
事件,這個章節中的其他部分則是滑鼠移動經過不同、多層的 DOM node 時,不同 node 上的mouseover
、mouseout
事件的順序。
除了 Mouse Events 之外,其實現在瀏覽器的實作應該是都實作 Pointer Events 了,Pointer Events 涵蓋了各種指標式的控制方式,包括了滑鼠、觸控、觸控筆等,所以可以說 Mouse Events 只能算是子集,相對應的事件名稱基本上就是把mouse
換成pointer
,例如:pointerdown
、pointerup
,現在主流的瀏覽器也都已經有支援 Pointer Events 了,然後這時候問題就來了,Pointer Event 和 Mouse Events 誰先誰後?根據測試的結果,是 Pointer 先然後才 Mouse,所以更完整的順序是:
pointerdown
mousedown
focus
focusin
pointerup
mouseup
click
至於這個順序是怎麼決定的呢?根據 Pointer Events 的 11 章「Compatibility mapping with mouse events」 所述,瀏覽器開發時可以考慮同時發佈古時候的 Mouse Events,其中 11.2、11.3 就有提到先發佈 Pointer Events 接著再發佈對應的 Mouse Event。
然後還有一個細節是,根據 Pointer Event 4.2.3 The pointerdown event,如果在pointerdown
事件裡面呼叫event.preventDefault()
取消事件的話,後面的mousedown
和mouseup
就都不會觸發。
我上面的那段事件順序,其實還多列了一個focusin
,這其實是一系列的新(相較於 DOM2)事件,包括了:
focusin
對應focus
focusout
對應blur
mouseenter
對應mouseover
mouseleave
對應mouseout
其實mouseenter
/mouseleave
是 IE5.5 時微軟先提出的,ppk 也有文章介紹過它解決了什麼問題,jQuery 當年也有支援,後來進入了 W3C 標準,現在是放到 UI Events 裡面,這些事件最大的差異就是它們不會 bubble 到外層,減少了很多問題;focusin
/focusout
則是剛好相反,它們會 bubble 而focus
/blur
不會,一樣 ppk 也有文章介紹到,簡單說就是focus
/blur
不會 bubble 只有 capture(外往內到 event target 的過程),但是 IE 不是用 addEventListener 所以沒有 capture 階段,所以會無法實作出 event delegation,然後只能用當年微軟 IE 特有的focusin
/focusout
事件,現在也是標準化,收到 DOM 3 Events 裡面;而除了這四組之外,其實 Pointer Events 也有 enter 和 leave,一樣順序是在 over 和 out 的後面,然後這些成對的事件,都是比較早定義的那些先發生,才接後來定義的,和有沒有 bubble 無關。
接著來說 Keyboard Events,這邊就針對 input 節點上的,一個鍵盤按鍵按壓的動作,會產生keydown
、keypress
、keyup
三個基本的事件,通常按鍵盤按鍵就是為了輸入東西,所以會有個也是比較新的input
事件,會在keypress
之後,input
則還有一個成對的beforeinput
,如果在beforeinput
內叫preventDefault()
的話則可以阻止文字的輸入,總之順序如下:
keydown
keypress
beforeinput
input
keyup
change
事件則是要在blur
時才會有,順序是先change
才blur
:
change
blur
focusout
這個順序是定義在 HTML 的 User Interaction 一章的 6.6.4 Processing model 裡面,明確的寫下要先 commit change 後才blur
。
如果有用 IME 的話,事情就很不單純了,還會有 Composition Events,順序是在beforeinput
前面,剛開始組字會同時有compositionstart
和compositionupdate
兩個事件,然後沒有keypress
,之後的輸入組字則就是只有update
:
keydown
compositionstart
compositionupdate
beforeinput
input
keyup
Composition Events 現在標準的文件是放在 UI Events 裡面,不過文件中範例的順序和實際測試的結果並不一致,目前的文件草稿中compositionupdate
是在beforeinput
之後,不過其實現在瀏覽器的順序是在 UI Events 的 Issue 354 提出的變動,只是目前文件還沒有發布更新的內容。
結束組字的時候,不同瀏覽器的行為就有差異了,首先是按下 Enter 完成組字時,Firefox 的話不會有多一個update
,直接就是compositionend
,然後是在beforeinput
和input
之間:
keydown
beforeinput
compositionend
input
keyup
Chrome(v123) 的話就會有多一個compositionupdate
然後compositionend
順序也不一樣,會在input
的後面:
keydown
compositionupdate
beforeinput
input
compositionend
keyup
其實 UI Events 文件 5.7.5 內的範例是 Firefox 那種沒有多compositionupdate
的順序,這個 5.7 章節內其實還有不少其他情境下的事件順序,像是手寫輸入,取消輸入等等。
如果是還沒完成組字,直接切換輸入法強迫結束組字,事件順序都和按下 Enter 都一樣,如果是組字到一半直接切換視窗,就是標準沒定義到的狀況了,Firefox 的行為比較接近上面的樣子,先完成input
才blur
:
beforeinput
compositionend
input
blur
focusout
Chrome 則是會先產生blur
然後接compositionend
:
change
blur
focusout
compositionend
以上,大概紀錄了我一直以來都有疑惑的使用者行為所產生的事件們的順序,最後附上我用的程式碼(超單純):
const events = [
"keydown",
"keypress",
"keyup",
"change",
"compositionstart",
"compositionupdate",
"compositionend",
"beforeinput",
"input",
"click",
"auxclick",
"contextmenu",
"mousedown",
"mouseup",
"focus",
"focusin",
"focusout",
"blur",
"pointerdown",
"pointerup",
"paste",
];
const elem1 = document.getElementById("target-1");
const elem2 = document.getElementById("target-2");
events.forEach((event) => {
elem1.addEventListener(
`${event}`,
() => {
console.log(event.target.id, event);
},
false
);
elem2.addEventListener(
`${event}`,
() => {
console.log(event.target.id, event);
},
false
);
});