The internal mechanism of the component event system
Internal Mechanism of the Component Event System
Vue 3's component event system is built on the publish-subscribe pattern, where custom events are triggered via the emit
method, and parent components listen using the v-on
or @
syntax. Behind this mechanism lies a sophisticated design, encompassing the complete chain of event registration, dispatch, and callback handling.
Event Registration and Dispatch Process
When a component instance is created, the event system is initialized, with core logic located in componentEmits.ts
. Each component maintains an emits
object that stores all registered event handlers:
// Simplified component instance structure
interface ComponentInternalInstance {
emits: Record<string, Array<Function>> | null
props: Record<string, any>
}
When a parent component uses @event="handler"
, Vue compiles it into an onEvent
property in the props. For example:
<Child @custom-event="handleEvent" />
The compiled runtime code is equivalent to:
h(Child, {
onCustomEvent: handleEvent
})
Implementation Principle of the emit
Method
The emit
method inside a component does not directly trigger events but processes them through the following steps:
- Convert the event name:
myEvent
→onMyEvent
- Look up the corresponding handler in the props
- Execute the handler and pass arguments
Key implementation in the source code:
function emit(instance, event: string, ...args: any[]) {
const props = instance.vnode.props || EMPTY_OBJ
let handler = props[toHandlerKey(event)]
if (!handler && event.indexOf('update:') === 0) {
handler = props[toHandlerKey(camelize(event))]
}
if (handler) {
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
}
Underlying Handling of Event Modifiers
Vue provides modifiers like .once
and .passive
for the event system, which are converted into corresponding DOM event options during the compilation phase:
<button @click.once="doThis"></button>
The compiled code adds an _once
flag:
_createVNode(button, {
onClick: {
handler: doThis,
once: true
}
})
The event handler execution logic checks these flags:
function invoker(e: Event) {
if (once) {
remove(event, invoker, undefined, true)
}
callWithAsyncErrorHandling(handler, args)
}
Custom Events vs. Native Event Dispatch
Vue 3 distinguishes between native DOM events and component custom events via event names. For example, with onClick
:
if (event.startsWith('on') && !isReservedProp(event)) {
// Handle custom events
const eventName = event[2].toLowerCase() + event.slice(3)
if (!instance.emitsOptions || hasOwn(instance.emitsOptions, eventName)) {
// Component custom event logic
} else {
// Native DOM event logic
}
}
Event Validation Mechanism
In development mode, Vue validates the configuration of the emits
option:
const Comp = {
emits: {
submit: (payload: { email: string, password: string }) => {
return payload.email.includes('@') && payload.password.length > 0
}
}
}
If validation fails, a warning is logged to the console. The core validation logic:
function validateEvent(
event: string,
args: unknown[],
emitsOptions: ObjectEmitsOptions | null
): boolean {
if (emitsOptions) {
const validator = emitsOptions[event]
if (isFunction(validator)) {
return validator(...args)
}
}
return true
}
Interaction Mechanism with v-model
v-model
is essentially syntactic sugar, relying on the event system under the hood. For example:
<input v-model="text" />
<!-- Equivalent to -->
<input
:value="text"
@input="text = $event.target.value"
/>
For custom components, v-model
is converted into a modelValue
property and an update:modelValue
event:
// Compiler transformation logic
if (dir.name === 'model') {
props.push(
createObjectProperty(`modelValue`, dir.exp),
createObjectProperty(`onUpdate:modelValue`,
createSimpleExpression(`$event => ${genAssignmentCode(dir.exp, '$event')}`, false)
)
)
}
Event Caching Optimization Strategy
Vue 3 caches event handlers to avoid unnecessary updates. During each render, it compares old and new event handlers:
function patchEvent(
el: Element,
rawName: string,
prevValue: EventValue | null,
nextValue: EventValue | null
) {
const invoker = prevValue && prevValue.invoker
if (nextValue && invoker) {
// Reuse existing invoker
invoker.value = nextValue
} else {
// Create new invoker
const name = rawName.slice(2).toLowerCase()
const invoker = createInvoker(nextValue)
el.addEventListener(name, invoker)
}
}
Cross-Component Event Propagation Pattern
Cross-level event propagation can be achieved via provide/inject
, similar to React's Context:
// Ancestor component
provide('eventBus', {
emit: (event) => instance.emit(event)
})
// Descendant component
const bus = inject('eventBus')
bus.emit('custom-event')
This pattern is commonly used in component libraries for global notifications, though dedicated libraries like mitt are recommended.
Performance Optimization Designs
The event system includes several performance optimizations:
- Lazy Event Registration: DOM event listeners are added only on first trigger.
- Event Delegation: A single top-level listener is used for similar events.
- Bitwise Flags: Binary bits store event states.
For example, bitwise operations determine whether an event needs updating:
const shouldUpdate =
(patchFlag & PatchFlags.PROPS) &&
!(patchFlag & PatchFlags.HYDRATE_EVENTS)
Integration with the Composition API
In the setup
function, the event system can be accessed via context.emit
:
setup(props, { emit }) {
const handleClick = () => {
emit('click', { position: 'header' })
}
return { handleClick }
}
The compiler converts @click
in the template into an emit
call in the setup context during the code generation phase:
// Compiled output
_createVNode(button, {
onClick: _ctx[0] || (_ctx[0] = $event => emit('click', $event))
})
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:作用域插槽的实现原理
下一篇:自定义元素的支持方式