阿里云主机折上折
  • 微信号
Current Site:Index > Implementation of an event handling system

Implementation of an event handling system

Author:Chuan Chen 阅读数:40928人阅读 分类: Vue.js

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:

  1. Event property names use camelCase (e.g., onClick).
  2. Property values directly reference methods on the component instance.
  3. 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

  1. Native DOM Events:

    • Bound directly to DOM elements.
    • Use the browser's native event system.
    • Managed via addEventListener.
  2. Component Custom Events:

    • Passed via props.
    • Managed by Vue's own event system.
    • Require explicit triggering via emit.

Performance Optimization Strategies

  1. Event Delegation: Reduces memory usage for large numbers of similar elements.
  2. Lazy Event Binding: Adds event listeners only when needed.
  3. Caching Event Handlers: Avoids recreating function objects.
  4. 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

  1. Event Binding:

    • Vue2 uses v-on directive objects.
    • Vue3 uses regular props.
  2. Modifier Handling:

    • Vue2 processes modifiers at runtime.
    • Vue3 converts them during compilation.
  3. Custom Events:

    • Vue2 uses standalone $on/$emit APIs.
    • Vue3 uses a props-based mechanism.

Key Source Code Path Analysis

  1. Compilation Phase:

    • compiler-core/src/transforms/transformOn.ts handles event directives.
    • Generates render code with onXxx properties.
  2. Runtime:

    • runtime-core/src/components/emit.ts handles component emit.
    • runtime-dom/src/modules/events.ts handles DOM events.
  3. Event Handling:

    • patchEvent in packages/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

  1. 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')
  1. 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:

  1. 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)
  }
}
  1. 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:

  1. 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)
  }
}
  1. 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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.