事件处理系统的实现
事件处理系统的实现
Vue3的事件处理系统基于编译阶段的模板解析和运行时的代理机制。当模板中出现@click
或v-on
指令时,编译器会将其转换为特定的渲染函数代码,运行时则通过代理对象处理事件绑定。这种设计使得事件处理既高效又灵活。
模板编译阶段的事件处理
编译器遇到事件指令时会生成对应的渲染函数代码。例如以下模板:
<button @click="handleClick">点击</button>
会被编译为:
import { createElementVNode as _createElementVNode } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createElementVNode("button", {
onClick: _ctx.handleClick
}, "点击", 8 /* PROPS */, ["onClick"]))
}
关键点在于:
- 事件属性名使用驼峰命名(onClick)
- 属性值直接引用组件实例上的方法
- 标记PROPS和动态属性数组
运行时的事件绑定
在patchProp
阶段,当处理以on
开头的事件属性时,会调用patchEvent
方法:
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) {
// 添加事件
const invoker = (invokers[key] = createInvoker(value))
el.addEventListener(eventName, invoker)
} else {
// 移除事件
el.removeEventListener(eventName, existingInvoker)
invokers[key] = undefined
}
}
}
createInvoker
创建了一个包装函数,允许动态更新事件处理器而不需要移除/重新添加事件监听:
function createInvoker(initialValue) {
const invoker = (e: Event) => {
invoker.value(e)
}
invoker.value = initialValue
return invoker
}
事件修饰符的实现
Vue3支持.stop
、.prevent
等事件修饰符,这些在编译阶段会被处理。例如:
<button @click.stop="handleClick">停止冒泡</button>
编译结果为:
_createElementVNode("button", {
onClick: withModifiers(_ctx.handleClick, ["stop"])
}, "停止冒泡")
withModifiers
实现如下:
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()
// 其他修饰符处理...
}
return fn(event)
}
}
自定义事件系统
组件间的自定义事件通过emit
方法实现。子组件触发事件:
const emit = defineEmits(['submit'])
function onClick() {
emit('submit', { data: 123 })
}
父组件监听:
<Child @submit="handleSubmit" />
编译后的父组件渲染函数:
_createVNode(Child, {
onSubmit: _ctx.handleSubmit
})
emit
的核心实现:
function emit(instance, event: string, ...args: any[]) {
const props = instance.vnode.props || {}
let handler = props[`on${capitalize(event)}`]
if (handler) {
handler(...args)
}
}
事件缓存优化
Vue3对事件处理函数进行了缓存优化。同一个事件处理函数在多次渲染中会被复用:
export function render(_ctx, _cache) {
return (_openBlock(), _createElementVNode("button", {
onClick: _cache[1] || (_cache[1] = ($event) => (_ctx.handleClick($event)))
}, "点击"))
}
原生DOM事件与组件事件的区别
-
原生DOM事件:
- 直接绑定到DOM元素
- 使用浏览器原生事件系统
- 通过
addEventListener
管理
-
组件自定义事件:
- 通过props传递
- 由Vue自己的事件系统管理
- 需要显式通过
emit
触发
性能优化策略
- 事件代理:对于大量相似元素,使用事件代理减少内存占用
- 惰性事件绑定:只在需要时添加事件监听
- 缓存事件处理函数:避免重复创建函数对象
- 修饰符编译时处理:将修饰符转换为直接的事件处理代码
// 事件代理示例
function createProxyHandler(el) {
return function handler(e) {
const target = e.target
if (target.matches('.item')) {
// 处理具体项目点击
}
}
}
parentEl.addEventListener('click', createProxyHandler(parentEl))
与Vue2的差异
-
事件绑定方式:
- Vue2使用
v-on
指令对象 - Vue3使用普通props传递
- Vue2使用
-
修饰符处理:
- Vue2在运行时处理
- Vue3在编译时转换
-
自定义事件:
- Vue2使用独立的
$on/$emit
API - Vue3使用基于props的机制
- Vue2使用独立的
源码关键路径分析
-
编译阶段:
compiler-core/src/transforms/transformOn.ts
处理事件指令- 生成带有
onXxx
属性的渲染代码
-
运行时:
runtime-core/src/components/emit.ts
处理组件emitruntime-dom/src/modules/events.ts
处理DOM事件
-
事件处理:
packages/runtime-dom/src/modules/events.ts
中的patchEvent
- 使用
_vei
(vue event invokers)缓存事件调用器
实际应用示例
实现一个可拖拽组件:
<template>
<div
@mousedown="startDrag"
@mousemove.passive="onDrag"
@mouseup="stopDrag"
:style="style"
>
拖拽我
</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>
高级事件模式
- 全局事件总线替代方案:
// 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))
}
}
// 使用示例
const unsubscribe = on('message', (msg) => {
console.log(msg)
})
emit('message', 'Hello Vue3')
- 自定义指令处理事件:
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)
}
}
测试事件处理
编写测试验证事件行为:
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)
})
事件系统的可扩展性
Vue3的事件系统设计允许轻松扩展:
- 添加自定义修饰符:
import { withModifiers } from 'vue'
function withCustomModifiers(fn: Function, modifiers: string[]) {
const handler = withModifiers(fn, modifiers)
return (e: Event) => {
if (modifiers.includes('double')) {
// 自定义双击逻辑
}
return handler(e)
}
}
- 集成第三方事件库:
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()
}
})
}
浏览器兼容性处理
Vue3内部处理了常见的事件兼容性问题:
- 被动事件检测:
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)
}
}
- 事件对象标准化:
function normalizeEvent(event: Event) {
if (event instanceof MouseEvent) {
// 标准化鼠标事件属性
} else if (event instanceof KeyboardEvent) {
// 标准化键盘事件属性
}
return event
}
性能监控与调试
开发时可以监控事件性能:
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`)
}
}
}
// 包装原始事件处理函数
const profiledClick = createProfiledHandler(handleClick, 'click')
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn