[Refactoring] Teact: Refactor dom-events and better debug
This commit is contained in:
parent
846b47cb32
commit
f156be13ce
@ -1,102 +1,103 @@
|
||||
import { DEBUG } from '../../config';
|
||||
|
||||
type Handler = (e: Event) => void;
|
||||
type DelegationRegistry = Map<HTMLElement, Handler>;
|
||||
|
||||
const NON_BUBBLEABLE_EVENTS = new Set(['scroll', 'mouseenter', 'mouseleave']);
|
||||
|
||||
const delegationRegistry: Record<string, Map<HTMLElement, Handler>> = {};
|
||||
const delegatedEventsByElement = new Map<HTMLElement, Set<string>>();
|
||||
const documentEventCounters: Record<string, number> = {};
|
||||
const delegationRegistryByEventType: Record<string, DelegationRegistry> = {};
|
||||
const delegatedEventTypesByElement = new Map<HTMLElement, Set<string>>();
|
||||
|
||||
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<string>();
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user