diff --git a/src/lib/teact/dom-events.ts b/src/lib/teact/dom-events.ts index 2cde1a040..1298f5a80 100644 --- a/src/lib/teact/dom-events.ts +++ b/src/lib/teact/dom-events.ts @@ -1,102 +1,103 @@ import { DEBUG } from '../../config'; type Handler = (e: Event) => void; +type DelegationRegistry = Map; const NON_BUBBLEABLE_EVENTS = new Set(['scroll', 'mouseenter', 'mouseleave']); -const delegationRegistry: Record> = {}; -const delegatedEventsByElement = new Map>(); const documentEventCounters: Record = {}; +const delegationRegistryByEventType: Record = {}; +const delegatedEventTypesByElement = new Map>(); export function addEventListener(element: HTMLElement, propName: string, handler: Handler, asCapture = false) { - const eventName = resolveEventName(propName, element); - if (canUseEventDelegation(eventName, element, asCapture)) { - addDelegatedListener(eventName, element, handler); + const eventType = resolveEventType(propName, element); + if (canUseEventDelegation(eventType, element, asCapture)) { + addDelegatedListener(eventType, element, handler); } else { - element.addEventListener(eventName, handler, asCapture); + element.addEventListener(eventType, handler, asCapture); } } export function removeEventListener(element: HTMLElement, propName: string, handler: Handler, asCapture = false) { - const eventName = resolveEventName(propName, element); - if (canUseEventDelegation(eventName, element, asCapture)) { - removeDelegatedListener(eventName, element); + const eventType = resolveEventType(propName, element); + if (canUseEventDelegation(eventType, element, asCapture)) { + removeDelegatedListener(eventType, element); } else { - element.removeEventListener(eventName, handler, asCapture); + element.removeEventListener(eventType, handler, asCapture); } } -function resolveEventName(propName: string, element: HTMLElement) { - const eventName = propName +function resolveEventType(propName: string, element: HTMLElement) { + const eventType = propName .replace(/^on/, '') .replace(/Capture$/, '').toLowerCase(); - if (eventName === 'change' && element.tagName !== 'SELECT') { + if (eventType === 'change' && element.tagName !== 'SELECT') { // React behavior repeated here. // https://stackoverflow.com/questions/38256332/in-react-whats-the-difference-between-onchange-and-oninput return 'input'; } - if (eventName === 'doubleclick') { + if (eventType === 'doubleclick') { return 'dblclick'; } // Replace focus/blur by their "bubbleable" versions - if (eventName === 'focus') { + if (eventType === 'focus') { return 'focusin'; } - if (eventName === 'blur') { + if (eventType === 'blur') { return 'focusout'; } - return eventName; + return eventType; } -function canUseEventDelegation(realEventName: string, element: HTMLElement, asCapture: boolean) { +function canUseEventDelegation(realEventType: string, element: HTMLElement, asCapture: boolean) { return ( !asCapture - && !NON_BUBBLEABLE_EVENTS.has(realEventName) + && !NON_BUBBLEABLE_EVENTS.has(realEventType) && element.tagName !== 'VIDEO' && element.tagName !== 'IFRAME' ); } -function addDelegatedListener(eventName: string, element: HTMLElement, handler: Handler) { - if (!documentEventCounters[eventName]) { - documentEventCounters[eventName] = 0; - document.addEventListener(eventName, handleEvent); +function addDelegatedListener(eventType: string, element: HTMLElement, handler: Handler) { + if (!documentEventCounters[eventType]) { + documentEventCounters[eventType] = 0; + document.addEventListener(eventType, handleEvent); } - resolveDelegationRegistryForName(eventName).set(element, handler); - resolveDelegatedEventsForElement(element).add(eventName); - documentEventCounters[eventName]++; + resolveDelegationRegistry(eventType).set(element, handler); + resolveDelegatedEventTypes(element).add(eventType); + documentEventCounters[eventType]++; } -function removeDelegatedListener(eventName: string, element: HTMLElement) { - documentEventCounters[eventName]--; - if (!documentEventCounters[eventName]) { +function removeDelegatedListener(eventType: string, element: HTMLElement) { + documentEventCounters[eventType]--; + if (!documentEventCounters[eventType]) { // Synchronous deletion on 0 will cause perf degradation in the case of 1 element // which is not a real case, so it's ok to do it this way - document.removeEventListener(eventName, handleEvent); + document.removeEventListener(eventType, handleEvent); } - delegationRegistry[eventName].delete(element); - delegatedEventsByElement.get(element)!.delete(eventName); + delegationRegistryByEventType[eventType].delete(element); + delegatedEventTypesByElement.get(element)!.delete(eventType); } export function removeAllDelegatedListeners(element: HTMLElement) { - const eventNames = delegatedEventsByElement.get(element); - if (!eventNames) { + const eventTypes = delegatedEventTypesByElement.get(element); + if (!eventTypes) { return; } - eventNames.forEach((eventName) => removeDelegatedListener(eventName, element)); - delegatedEventsByElement.delete(element); + eventTypes.forEach((eventType) => removeDelegatedListener(eventType, element)); + delegatedEventTypesByElement.delete(element); } function handleEvent(realEvent: Event) { - const events = delegationRegistry[realEvent.type]; + const events = delegationRegistryByEventType[realEvent.type]; if (events) { let furtherCallsPrevented = false; @@ -142,29 +143,46 @@ function handleEvent(realEvent: Event) { } } -function resolveDelegationRegistryForName(eventName: string) { - if (!delegationRegistry[eventName]) { - delegationRegistry[eventName] = new Map(); +function resolveDelegationRegistry(eventType: string) { + if (!delegationRegistryByEventType[eventType]) { + delegationRegistryByEventType[eventType] = new Map(); } - return delegationRegistry[eventName]; + return delegationRegistryByEventType[eventType]; } -function resolveDelegatedEventsForElement(element: HTMLElement) { - const existing = delegatedEventsByElement.get(element); +function resolveDelegatedEventTypes(element: HTMLElement) { + const existing = delegatedEventTypesByElement.get(element); if (existing) { return existing; } const newSet = new Set(); - delegatedEventsByElement.set(element, newSet); + delegatedEventTypesByElement.set(element, newSet); return newSet; } if (DEBUG) { document.addEventListener('dblclick', () => { + const documentListenersCount = Object.keys(documentEventCounters).length; + const delegatedHandlersCount1 = Object.values(documentEventCounters) + .reduce((acc, counter) => acc + counter, 0); + const delegationRegistriesCount = Object.keys(delegationRegistryByEventType).length; + const delegatedHandlersCount2 = Object.values(delegationRegistryByEventType) + .reduce((acc, delegationRegistry) => acc + delegationRegistry.size, 0); + const delegationElementsCount = delegatedEventTypesByElement.size; + const delegatedEventTypesCount = Array.from(delegatedEventTypesByElement.values()) + .reduce((acc, eventTypes) => acc + eventTypes.size, 0); + // eslint-disable-next-line no-console - console.log('DELEGATED EVENTS', { delegationRegistry, delegatedEventsByElement, documentEventCounters }); + console.warn('DELEGATED EVENTS STATS', { + delegatedHandlersCount1, + delegatedHandlersCount2, + delegatedEventTypesCount, + delegationRegistriesCount, + delegationElementsCount, + documentListenersCount, + }); }); } diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index 28157274b..18f74b60d 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -250,7 +250,7 @@ const DEBUG_components: AnyLiteral = {}; document.addEventListener('dblclick', () => { // eslint-disable-next-line no-console - console.log('COMPONENTS', orderBy(Object.values(DEBUG_components), 'renderCount', 'desc')); + console.warn('COMPONENTS', orderBy(Object.values(DEBUG_components), 'renderCount', 'desc')); }); export function renderComponent(componentInstance: ComponentInstance) { diff --git a/src/lib/teact/teactn.tsx b/src/lib/teact/teactn.tsx index 58cfc5643..fee570dc3 100644 --- a/src/lib/teact/teactn.tsx +++ b/src/lib/teact/teactn.tsx @@ -221,7 +221,7 @@ if (DEBUG) { document.addEventListener('dblclick', () => { // eslint-disable-next-line no-console - console.log( + console.warn( 'GLOBAL CONTAINERS', orderBy( Array.from(containers.values())