Implementation of an event handling system
Implementation of the Event Handling System
Vue3's event handling system is based on template parsing during the compilation phase and a runtime proxy mechanism. When directives like @click
or v-on
appear in templates, the compiler converts them into specific render function code, while the runtime handles event binding through proxy objects. This design makes event handling both efficient and flexible.
Event Handling During Template Compilation
When the compiler encounters event directives, it generates corresponding render function code. For example, the following template:
<button @click="handleClick">Click</button>
is compiled into:
import { createElementVNode as _createElementVNode } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createElementVNode("button", {
onClick: _ctx.handleClick
}, "Click", 8 /* PROPS */, ["onClick"]))
}
Key points include:
- Event property names use camelCase (e.g.,
onClick
). - Property values directly reference methods on the component instance.
- The
PROPS
flag and dynamic property array are marked.
Runtime Event Binding
During the patchProp
phase, when processing event properties starting with on
, the patchEvent
method is called:
const patchEvent = (el: Element, key: string, value: any) => {
const invokers = el._vei || (el._vei = {})
const existingInvoker = invokers[key]
if (value && existingInvoker) {
existingInvoker.value = value
} else {
const eventName = key.slice(2).toLowerCase()
if (value) {
// Add event
const invoker = (invokers[key] = createInvoker(value))
el.addEventListener(eventName, invoker)
} else {
// Remove event
el.removeEventListener(eventName, existingInvoker)
invokers[key] = undefined
}
}
}
createInvoker
creates a wrapper function that allows dynamic updates to event handlers without removing/re-adding event listeners:
function createInvoker(initialValue) {
const invoker = (e: Event) => {
invoker.value(e)
}
invoker.value = initialValue
return invoker
}
Implementation of Event Modifiers
Vue3 supports event modifiers like .stop
and .prevent
, which are processed during compilation. For example:
<button @click.stop="handleClick">Stop Bubbling</button>
is compiled into:
_createElementVNode("button", {
onClick: withModifiers(_ctx.handleClick, ["stop"])
}, "Stop Bubbling")
The withModifiers
implementation:
const withModifiers = (fn: Function, modifiers: string[]) => {
return (event: Event) => {
for (let i = 0; i < modifiers.length; i++) {
const modifier = modifiers[i]
if (modifier === 'stop') event.stopPropagation()
if (modifier === 'prevent') event.preventDefault()
// Other modifier handling...
}
return fn(event)
}
}
Custom Event System
Custom events between components are implemented via the emit
method. A child component triggers an event:
const emit = defineEmits(['submit'])
function onClick() {
emit('submit', { data: 123 })
}
The parent component listens for the event:
<Child @submit="handleSubmit" />
The compiled parent component render function:
_createVNode(Child, {
onSubmit: _ctx.handleSubmit
})
Core implementation of emit
:
function emit(instance, event: string, ...args: any[]) {
const props = instance.vnode.props || {}
let handler = props[`on${capitalize(event)}`]
if (handler) {
handler(...args)
}
}
Event Caching Optimization
Vue3 optimizes event handling functions by caching them. The same event handler is reused across multiple renders:
export function render(_ctx, _cache) {
return (_openBlock(), _createElementVNode("button", {
onClick: _cache[1] || (_cache[1] = ($event) => (_ctx.handleClick($event)))
}, "Click"))
}
Differences Between Native DOM Events and Component Events
-
Native DOM Events:
- Bound directly to DOM elements.
- Use the browser's native event system.
- Managed via
addEventListener
.
-
Component Custom Events:
- Passed via props.
- Managed by Vue's own event system.
- Require explicit triggering via
emit
.
Performance Optimization Strategies
- Event Delegation: Reduces memory usage for large numbers of similar elements.
- Lazy Event Binding: Adds event listeners only when needed.
- Caching Event Handlers: Avoids recreating function objects.
- Compile-Time Modifier Processing: Converts modifiers into direct event handling code.
// Event delegation example
function createProxyHandler(el) {
return function handler(e) {
const target = e.target
if (target.matches('.item')) {
// Handle specific item click
}
}
}
parentEl.addEventListener('click', createProxyHandler(parentEl))
Differences from Vue2
-
Event Binding:
- Vue2 uses
v-on
directive objects. - Vue3 uses regular props.
- Vue2 uses
-
Modifier Handling:
- Vue2 processes modifiers at runtime.
- Vue3 converts them during compilation.
-
Custom Events:
- Vue2 uses standalone
$on/$emit
APIs. - Vue3 uses a props-based mechanism.
- Vue2 uses standalone
Key Source Code Path Analysis
-
Compilation Phase:
compiler-core/src/transforms/transformOn.ts
handles event directives.- Generates render code with
onXxx
properties.
-
Runtime:
runtime-core/src/components/emit.ts
handles componentemit
.runtime-dom/src/modules/events.ts
handles DOM events.
-
Event Handling:
patchEvent
inpackages/runtime-dom/src/modules/events.ts
.- Uses
_vei
(Vue Event Invokers) to cache event invokers.
Practical Example
Implementing a draggable component:
<template>
<div
@mousedown="startDrag"
@mousemove.passive="onDrag"
@mouseup="stopDrag"
:style="style"
>
Drag Me
</div>
</template>
<script setup>
import { ref } from 'vue'
const position = ref({ x: 0, y: 0 })
const isDragging = ref(false)
const startPos = ref({ x: 0, y: 0 })
const style = computed(() => ({
position: 'absolute',
left: `${position.value.x}px`,
top: `${position.value.y}px`,
cursor: isDragging.value ? 'grabbing' : 'grab'
}))
function startDrag(e) {
isDragging.value = true
startPos.value = {
x: e.clientX - position.value.x,
y: e.clientY - position.value.y
}
}
function onDrag(e) {
if (!isDragging.value) return
position.value = {
x: e.clientX - startPos.value.x,
y: e.clientY - startPos.value.y
}
}
function stopDrag() {
isDragging.value = false
}
</script>
Advanced Event Patterns
- Alternative to Global Event Bus:
// eventBus.ts
import { ref, watchEffect } from 'vue'
type Handler<T = any> = (event: T) => void
type EventMap = Record<string, Handler[]>
const events: EventMap = {}
export function on<T = any>(event: string, handler: Handler<T>) {
if (!events[event]) {
events[event] = []
}
events[event].push(handler)
return () => {
events[event] = events[event].filter(h => h !== handler)
}
}
export function emit<T = any>(event: string, payload?: T) {
if (events[event]) {
events[event].forEach(handler => handler(payload))
}
}
// Usage example
const unsubscribe = on('message', (msg) => {
console.log(msg)
})
emit('message', 'Hello Vue3')
- Custom Directive for Event Handling:
const vLongpress = {
mounted(el, binding) {
let timer
const handler = binding.value
const start = (e) => {
if (e.button !== 0) return
timer = setTimeout(() => {
handler(e)
}, 1000)
}
const cancel = () => {
clearTimeout(timer)
}
el._longpressHandlers = { start, cancel }
el.addEventListener('mousedown', start)
el.addEventListener('mouseup', cancel)
el.addEventListener('mouseleave', cancel)
},
unmounted(el) {
const { start, cancel } = el._longpressHandlers
el.removeEventListener('mousedown', start)
el.removeEventListener('mouseup', cancel)
el.removeEventListener('mouseleave', cancel)
}
}
Testing Event Handling
Writing tests to verify event behavior:
import { mount } from '@vue/test-utils'
test('click event', async () => {
const onClick = jest.fn()
const wrapper = mount({
template: '<button @click="onClick">Click</button>',
setup() {
return { onClick }
}
})
await wrapper.find('button').trigger('click')
expect(onClick).toHaveBeenCalled()
})
test('custom event', async () => {
const wrapper = mount({
emits: ['submit'],
template: '<button @click="$emit(\'submit\', 123)">Submit</button>'
})
const onSubmit = jest.fn()
wrapper.vm.$emit('submit', onSubmit)
await wrapper.find('button').trigger('click')
expect(onSubmit).toHaveBeenCalledWith(123)
})
Extensibility of the Event System
Vue3's event system design allows for easy extension:
- Adding Custom Modifiers:
import { withModifiers } from 'vue'
function withCustomModifiers(fn: Function, modifiers: string[]) {
const handler = withModifiers(fn, modifiers)
return (e: Event) => {
if (modifiers.includes('double')) {
// Custom double-click logic
}
return handler(e)
}
}
- Integrating Third-Party Event Libraries:
import { onMounted, onUnmounted } from 'vue'
import Hammer from 'hammerjs'
export function useGesture(elRef, handlers) {
onMounted(() => {
const hammer = new Hammer(elRef.value)
Object.entries(handlers).forEach(([event, handler]) => {
hammer.on(event, handler)
})
})
onUnmounted(() => {
if (hammer) {
hammer.destroy()
}
})
}
Browser Compatibility Handling
Vue3 internally handles common event compatibility issues:
- Passive Event Detection:
let supportsPassive = false
try {
const opts = Object.defineProperty({}, 'passive', {
get() {
supportsPassive = true
}
})
window.addEventListener('test', null, opts)
} catch (e) {}
function addEventListener(
el: Element,
event: string,
handler: Function,
options?: AddEventListenerOptions
) {
if (event === 'touchstart' && supportsPassive) {
el.addEventListener(event, handler, {
passive: true,
...options
})
} else {
el.addEventListener(event, handler, options)
}
}
- Event Object Normalization:
function normalizeEvent(event: Event) {
if (event instanceof MouseEvent) {
// Normalize mouse event properties
} else if (event instanceof KeyboardEvent) {
// Normalize keyboard event properties
}
return event
}
Performance Monitoring and Debugging
During development, event performance can be monitored:
function createProfiledHandler(handler, eventName) {
return function profiledHandler(e) {
const start = performance.now()
handler(e)
const duration = performance.now() - start
if (duration > 10) {
console.warn(`[Perf] ${eventName} handler took ${duration.toFixed(2)}ms`)
}
}
}
// Wrap original event handler
const profiledClick = createProfiledHandler(handleClick, 'click')
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn