개요
이벤트 시스템은 에디터 코어와 플러그인 간의 통신 레이어입니다. 플러그인이 에디터 상태 변경, 사용자 상호작용, 생명주기 이벤트를 수신하고 반응할 수 있게 합니다.
잘 설계된 이벤트 시스템은 타입 안전성, 효율적인 이벤트 전파, 유연한 이벤트 처리 패턴을 제공합니다.
이벤트 버블링 및 캡처
버블링 구현
DOM 이벤트와 유사하게 이벤트 버블링을 구현하여 부모 핸들러가 자식보다 먼저 이벤트를 처리할 수 있게 합니다:
class EventEmitter {
#listeners = new Map();
on(event, handler, options) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(handler);
}
emit(event, data, bubbles = true) {
const handlers = this.#listeners.get(event);
if (handlers) {
handlers.forEach(handler => {
handler(data);
});
}
// Bubble up to parent
if (bubbles && this.#parent) {
this.#parent.emit(event, data, true);
}
}
}
// Usage: Node-level events bubble to document
class DocumentNode {
constructor(parent) {
this.#parent = parent;
this.#emitter = new EventEmitter();
}
emit(event, data) {
// Emit on this node
this.#emitter.emit(event, data, false);
// Bubble to parent
if (this.#parent) {
this.#parent.emit(event, data);
}
}
}
// Handler on document receives all node events
document.on('textInsert', (data) => {
console.log('Text inserted anywhere in document:', data);
});캡처 단계
버블링 전에 처리되어야 하는 이벤트를 위한 캡처 단계를 지원합니다:
class EventEmitter {
#captureListeners = new Map();
#bubbleListeners = new Map();
on(event, handler, options = {}) {
const listeners = options.capture
? this.#captureListeners
: this.#bubbleListeners;
if (!listeners.has(event)) {
listeners.set(event, new Set());
}
listeners.get(event).add(handler);
}
emit(event, data) {
// Capture phase: parent to child
this.#emitCapture(event, data);
// Target phase: current node
this.#emitTarget(event, data);
// Bubble phase: child to parent
this.#emitBubble(event, data);
}
#emitCapture(event, data) {
// Emit to capture listeners
const handlers = this.#captureListeners.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
// Continue to children
this.#children.forEach(child => {
child.#emitCapture(event, data);
});
}
#emitBubble(event, data) {
// Emit to bubble listeners
const handlers = this.#bubbleListeners.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
// Continue to parent
if (this.#parent) {
this.#parent.#emitBubble(event, data);
}
}
}이벤트 위임 패턴
위임의 이점
이벤트 위임은 메모리 사용을 줄이고 성능을 향상시킵니다:
- 개별 노드에 많은 리스너 대신 컨테이너에 단일 리스너
- 동적으로 추가된 노드에서도 작동
- 이벤트 리스너 오버헤드 감소
- 이벤트 관리 단순화
위임 구현
이벤트를 효율적으로 처리하기 위해 이벤트 위임을 사용합니다:
class Editor {
constructor(container) {
this.#container = container;
// Single listener on container
this.#container.addEventListener('click', (e) => {
this.#handleClick(e);
});
this.#container.addEventListener('input', (e) => {
this.#handleInput(e);
});
}
#handleClick(e) {
const target = e.target;
// Find the model node for this DOM element
const node = this.#findNodeForElement(target);
if (!node) return;
// Dispatch to appropriate handler based on node type
switch (node.type) {
case 'link':
this.#handleLinkClick(node, e);
break;
case 'image':
this.#handleImageClick(node, e);
break;
}
}
#handleInput(e) {
// Convert DOM input to model operation
const operation = this.#domToOperation(e);
this.applyOperation(operation);
}
#findNodeForElement(element) {
// Walk up DOM tree to find node marker
let current = element;
while (current && current !== this.#container) {
const nodeId = current.getAttribute('data-node-id');
if (nodeId) {
return this.#getNodeById(nodeId);
}
current = current.parentElement;
}
return null;
}
}커스텀 이벤트 시스템
타입화된 이벤트
타입화된 이벤트로 커스텀 이벤트 시스템을 구현합니다:
// Define event types
const EditorEvents = {
OPERATION: 'operation',
SELECTION_CHANGE: 'selectionChange',
DOCUMENT_CHANGE: 'documentChange',
ERROR: 'error'
};
// Typed event emitter
class TypedEventEmitter {
#listeners = new Map();
on(event, handler) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(handler);
}
off(event, handler) {
const handlers = this.#listeners.get(event);
if (handlers) {
handlers.delete(handler);
}
}
emit(event, data) {
const handlers = this.#listeners.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
}
// Usage
const editor = new TypedEventEmitter();
editor.on(EditorEvents.OPERATION, (data) => {
console.log('Operation:', data.operation);
});
editor.on(EditorEvents.ERROR, (data) => {
console.error('Error:', data.error);
});
editor.emit(EditorEvents.OPERATION, {
operation: { type: 'insertText', text: 'Hello' }
});이벤트 우선순위
순서가 있는 이벤트 처리를 위한 이벤트 우선순위를 지원합니다:
class PriorityEventEmitter {
#listeners = new Map();
on(event, handler, priority = 0) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
const listeners = this.#listeners.get(event);
listeners.push({ handler, priority });
// Sort by priority (higher priority first)
listeners.sort((a, b) => b.priority - a.priority);
}
emit(event, data) {
const listeners = this.#listeners.get(event);
if (listeners) {
listeners.forEach(({ handler }) => {
handler(data);
});
}
}
}
// Usage
const emitter = new PriorityEventEmitter();
// High priority handler (runs first)
emitter.on('operation', (data) => {
console.log('High priority:', data);
}, 100);
// Normal priority handler
emitter.on('operation', (data) => {
console.log('Normal priority:', data);
}, 0);
// Low priority handler (runs last)
emitter.on('operation', (data) => {
console.log('Low priority:', data);
}, -100);이벤트 생명주기
완전한 이벤트 생명주기 관리:
class EventLifecycle {
#preHandlers = new Map();
#handlers = new Map();
#postHandlers = new Map();
on(event, handler, phase = 'main') {
const map = phase === 'pre'
? this.#preHandlers
: phase === 'post'
? this.#postHandlers
: this.#handlers;
if (!map.has(event)) {
map.set(event, new Set());
}
map.get(event).add(handler);
}
emit(event, data) {
// Pre-handlers
this.#callHandlers(this.#preHandlers, event, data);
// Main handlers
const result = this.#callHandlers(this.#handlers, event, data);
// Post-handlers
this.#callHandlers(this.#postHandlers, event, data);
return result;
}
#callHandlers(map, event, data) {
const handlers = map.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
// Prevent default behavior
preventDefault(event) {
event.defaultPrevented = true;
}
// Stop propagation
stopPropagation(event) {
event.propagationStopped = true;
}
}