Vue 事件綁定機制

Vue 將事件系統拆分為原生 DOM 事件與自定義組件事件兩套正交實現,前者對接瀏覽器事件循環,後者基於發佈–訂閲模型。本文以 v-on(縮寫 @)為線索,結合運行時源碼路徑,給出端到端的實現剖析。

一、架構概覽

Vue 的事件綁定分為兩條主線:

  • 原生事件綁定

    通過 @clickv-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 等修飾符。
  • 調用 updateListenersaddtarget.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,實現跨組件通信。

理解這一分層設計,有助於在複雜場景(服務端渲染、微前端、自定義渲染器)中精準定位事件相關問題。