Vue 事件綁定機制
Vue 將事件系統拆分為原生 DOM 事件與自定義組件事件兩套正交實現,前者對接瀏覽器事件循環,後者基於發佈–訂閲模型。本文以 v-on(縮寫 @)為線索,結合運行時源碼路徑,給出端到端的實現剖析。
一、架構概覽
Vue 的事件綁定分為兩條主線:
-
原生事件綁定
通過
@click或v-on:click直接作用於普通 DOM 元素,最終調用瀏覽器的addEventListener。 -
組件事件綁定
通過
@click作用於子組件標籤時,實際上是父組件監聽子組件的自定義事件,由子組件通過$emit觸發,不經過 DOM。
二、原生事件綁定:從 AST 到 addEventListener
1.編譯階段
模板中的 @click="handler" 經模板編譯器解析後,生成 AST,最終轉化為 VNode 的 data.on = { click: handler }。
2.運行時掛載
首次渲染時,patch 過程會調用 createElm,為真實 DOM 節點執行 invokeCreateHooks,其中 cbs.create 包含 updateDOMListeners(位於 src/platforms/web/runtime/modules/events.js)。
updateDOMListeners 的職責:
- 歸一化事件名,處理 IE 兼容性差異。
- 生成包裹函數,處理
.once、.passive、.capture等修飾符。 - 調用
updateListeners→add→target.addEventListener(type, wrappedHandler, useCapture)。
3.更新階段
當組件更新時,patch 再次調用 updateDOMListeners,通過 sameVnode 判斷事件差異,按需移除舊事件並重新綁定新事件。
三、組件事件綁定:on + events + emit
1.父組件編譯
<Child @click="handleClick" /> 編譯後,VNode 的 componentOptions.listeners = { click: handleClick },不會出現在 DOM 屬性上。
2.子組件初始化
子組件實例化時:
initInternalComponent將父級listeners注入到vm.$options._parentListeners。initEvents創建_events = Object.create(null)作為事件中心。- 若
_parentListeners非空,執行updateComponentListeners(vm, _parentListeners),內部通過$on註冊事件:
function add(event, fn) {
vm.$on(event, fn)
}
3.手動觸發
子組件內部調用 this.$emit('click', payload) 時,執行:
const cbs = vm._events[event]
if (cbs) {
cbs.forEach(cb => cb(payload))
}
整個過程與瀏覽器事件體系完全隔離,因此可跨層級通信,且參數可控。
四、.native:在組件根節點強制使用原生事件
<Child @click.native="handler" /> 編譯為 nativeOn 而非 on,運行時由 updateDOMListeners 讀取 nativeOn,流程與原生事件一致,綁定在組件根 DOM 上。
五、事件修飾符實現細節
.stop:包裹函數內調用e.stopPropagation()。.prevent:包裹函數內調用e.preventDefault()。.once:綁定後立即移除監聽器,並標記_withOnce。.passive:調用addEventListener(type, fn, { passive: true })。.capture:第三個參數傳入useCapture: true。
六、性能與內存考量
- 原生事件由瀏覽器託管,Vue 僅在 VNode 銷燬時執行
removeEventListener,無額外開銷。 - 組件事件存儲在 JS 對象,組件銷燬時統一
$off,防止內存泄漏。
結論
Vue 事件系統通過“編譯期轉換 + 運行時調度”實現高度抽象:
- 原生事件:AST → VNode → patch → addEventListener,完全對齊瀏覽器。
- 組件事件:父子間通過 VNode.listeners → vm.events → emit,脱離 DOM,實現跨組件通信。
理解這一分層設計,有助於在複雜場景(服務端渲染、微前端、自定義渲染器)中精準定位事件相關問題。