一次 DOM 事件會先由外層祖先節點的 capture listener 往 target 下行。抵達 target 後,註冊在該節點的 capture listener 會先於 bubble listener 執行,雖然兩者的觀測都屬於 target 階段。若事件支援冒泡,bubble listener 接著由 target 沿祖先節點往上執行。依這個順序閱讀觀測點,就能找出最早可能改變或截斷路徑的 listener。
target 表示 listener 所看見的事件起點,currentTarget 則表示該筆觀測中正在執行 listener 的節點。composedPath() 提供目前執行環境可見的有序節點路徑;本 trace 由 target 往根節點呈現。閱讀 capture 時反向看,閱讀 bubble 時順向看。遇到 Shadow DOM 時,可見的 target 可能被重新定向,路徑也可能略過 closed shadow 內部,因此應以記錄到的路徑為準,不要假設它等同一般 DOM 的 parent 鏈。
stopPropagation 會阻止事件繼續前往後續節點。同一 currentTarget、同一次 capture 或 bubble 執行輪次中的其他 listener 仍可執行;但若 target 上的 capture listener 呼叫它,接下來 target 的 bubble 輪次會被略過。stopImmediatePropagation 連同一輪次尚未執行的 listener 也會阻止。一次性 probe 會記錄停止呼叫的方法、階段與 currentTarget,因此應將停止點和最後一筆觀測對照,不必只靠註冊程式碼猜測。修改 handler 後,再擷取一次相同事件,確認路徑是否已抵達預期的祖先節點。
關於這個主題的常見疑問與實用解答。
capture listener 由外層祖先節點往 target 執行。在 target 上,capture listener 先於 bubble listener 執行,兩者都會回報為 target 階段。若 bubbles 為 true,bubble listener 再從 target 沿祖先節點往上執行。同一節點、同一類型的 listener 之間,則依註冊順序執行。
先檢查 event.bubbles,因為有些事件類型本來就不會冒泡。若其值為 true,再尋找父元素之前是否呼叫了 stopPropagation 或 stopImmediatePropagation,確認父元素確實位於 composedPath(),並檢查 Shadow DOM 或框架邊界是否改變可見路徑。trace 會呈現路徑、最後觀測點與記錄到的停止呼叫,可用來區分這些情況。
event.target 是 listener 所看見的事件起點,通常在傳遞過程中維持不變;event.currentTarget 是目前正在執行 listener 的節點,因此會隨每筆觀測改變。使用事件委派時,以 target 判斷被點擊的後代節點,以 currentTarget 判斷擁有 handler 的元素。
不一定。同一 currentTarget、同一次 capture 或 bubble 執行輪次中的其他 listener 仍可執行;但若 target 上的 capture listener 呼叫 stopPropagation,接下來 target 的 bubble 輪次會被略過。stopImmediatePropagation 除了停止後續傳遞,也會阻止同一輪次尚未執行的 listener。probe 會記錄實際呼叫的方法,避免混淆這些情況。
選擇事件類型,複製一次性 probe,並在發生問題的頁面 DevTools 中執行。重現一次事件後,把產生的 JSON trace 貼回本工具。probe 會在擷取後移除 listener 並還原暫時修改的方法,只記錄這一個事件,也不會透過網路傳送 trace。
事件傳遞描述一個 DOM 事件沿哪些節點移動,以及路徑上的 capture、target、bubble listener 如何執行;事件迴圈則透過呼叫堆疊、microtask 與 macrotask 決定 JavaScript 工作何時執行。一次 click 會以任務進入事件迴圈;開始分派該事件後,事件傳遞才決定這次事件內部的 listener 順序。