Revert "[Perf] Teact: Optimizations"

This reverts commit 691c6204c1908af0d3317cb18dc67efb566beefe.
This commit is contained in:
Alexander Zinchuk 2023-07-06 14:25:38 +02:00
parent 00caec5a31
commit 28ac8fe061
2 changed files with 238 additions and 306 deletions

View File

@ -1,4 +1,3 @@
import type { ChangeEvent } from 'react';
import type { import type {
VirtualElement, VirtualElement,
VirtualElementChildren, VirtualElementChildren,
@ -11,12 +10,16 @@ import type {
import { import {
captureImmediateEffects, captureImmediateEffects,
hasElementChanged, hasElementChanged,
isComponentElement,
isEmptyElement,
isFragmentElement,
isParentElement, isParentElement,
isTagElement,
isTextElement,
mountComponent, mountComponent,
MountState, MountState,
renderComponent, renderComponent,
unmountComponent, unmountComponent,
VirtualType,
} from './teact'; } from './teact';
import { DEBUG } from '../../config'; import { DEBUG } from '../../config';
import { addEventListener, removeAllDelegatedListeners, removeEventListener } from './dom-events'; import { addEventListener, removeAllDelegatedListeners, removeEventListener } from './dom-events';
@ -55,10 +58,10 @@ function render($element: VirtualElement | undefined, parentEl: HTMLElement) {
const runImmediateEffects = captureImmediateEffects(); const runImmediateEffects = captureImmediateEffects();
const $head = headsByElement.get(parentEl)!; const $head = headsByElement.get(parentEl)!;
const $renderedChild = renderWithVirtual(parentEl, $head.children[0], $element, $head, 0); const $newElement = renderWithVirtual(parentEl, $head.children[0], $element, $head, 0);
runImmediateEffects?.(); runImmediateEffects?.();
$head.children = $renderedChild ? [$renderedChild] : []; $head.children = $newElement ? [$newElement] : [];
if (process.env.APP_ENV === 'perf') { if (process.env.APP_ENV === 'perf') {
DEBUG_virtualTreeSize = 0; DEBUG_virtualTreeSize = 0;
@ -86,12 +89,12 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
const { skipComponentUpdate, fragment } = options; const { skipComponentUpdate, fragment } = options;
let { nextSibling } = options; let { nextSibling } = options;
const isCurrentComponent = $current && $current.type === VirtualType.Component; const isCurrentComponent = $current && isComponentElement($current);
const isNewComponent = $new && $new.type === VirtualType.Component; const isNewComponent = $new && isComponentElement($new);
const $newAsReal = $new as VirtualElementReal; const $newAsReal = $new as VirtualElementReal;
const isCurrentFragment = $current && !isCurrentComponent && $current.type === VirtualType.Fragment; const isCurrentFragment = $current && !isCurrentComponent && isFragmentElement($current);
const isNewFragment = $new && !isNewComponent && $new.type === VirtualType.Fragment; const isNewFragment = $new && !isNewComponent && isFragmentElement($new);
if ( if (
!skipComponentUpdate !skipComponentUpdate
@ -129,19 +132,12 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment }); mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment });
} else { } else {
const canSetText = $parent.children.length === 1 && $newAsReal.type === VirtualType.Text; const node = createNode($newAsReal);
$newAsReal.target = node;
insertBefore(fragment || parentEl, node, nextSibling);
if (canSetText) { if (isTagElement($newAsReal)) {
parentEl.textContent = 'value' in $newAsReal ? $newAsReal.value : ''; setElementRef($newAsReal, node as HTMLElement);
$newAsReal.target = parentEl.firstChild!;
} else {
const node = createNode($newAsReal);
$newAsReal.target = node;
insertBefore(fragment || parentEl, node, nextSibling);
if ($newAsReal.type === VirtualType.Tag) {
setElementRef($newAsReal, node as HTMLElement);
}
} }
} }
} else if ($current && !$new) { } else if ($current && !$new) {
@ -160,27 +156,12 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
remount(parentEl, $current, undefined); remount(parentEl, $current, undefined);
mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment }); mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment });
} else { } else {
const canSetText = $parent.children.length === 1 const node = createNode($newAsReal);
&& $newAsReal.type === VirtualType.Text $newAsReal.target = node;
&& ($current.type === VirtualType.Text || $current.type === VirtualType.Empty) remount(parentEl, $current, node, nextSibling);
&& (!parentEl.firstChild || parentEl.firstChild === $current.target);
if (canSetText) { if (isTagElement($newAsReal)) {
const value = 'value' in $newAsReal ? $newAsReal.value : ''; setElementRef($newAsReal, node as HTMLElement);
if (parentEl.firstChild) {
parentEl.firstChild.nodeValue = value;
} else {
parentEl.textContent = value;
}
$newAsReal.target = parentEl.firstChild!;
} else {
const node = createNode($newAsReal);
$newAsReal.target = node;
remount(parentEl, $current, node, nextSibling);
if ($newAsReal.type === VirtualType.Tag) {
setElementRef($newAsReal, node as HTMLElement);
}
} }
} }
} else { } else {
@ -188,7 +169,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
const isFragment = isCurrentFragment && isNewFragment; const isFragment = isCurrentFragment && isNewFragment;
if (isComponent || isFragment) { if (isComponent || isFragment) {
renderChildren( ($new as VirtualElementComponent | VirtualElementFragment).children = renderChildren(
$current, $current,
$new as VirtualElementComponent | VirtualElementFragment, $new as VirtualElementComponent | VirtualElementFragment,
parentEl, parentEl,
@ -202,7 +183,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
$newAsReal.target = currentTarget; $newAsReal.target = currentTarget;
$currentAsReal.target = undefined; // Help GC $currentAsReal.target = undefined; // Help GC
const isTag = $current.type === VirtualType.Tag; const isTag = isTagElement($current);
if (isTag) { if (isTag) {
const $newAsTag = $new as VirtualElementTag; const $newAsTag = $new as VirtualElementTag;
@ -214,7 +195,12 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
} }
updateAttributes($current, $newAsTag, currentTarget as HTMLElement); updateAttributes($current, $newAsTag, currentTarget as HTMLElement);
renderChildren($current, $newAsTag, currentTarget as HTMLElement);
$newAsTag.children = renderChildren(
$current,
$newAsTag,
currentTarget as HTMLElement,
);
} }
} }
} }
@ -236,8 +222,8 @@ function initComponent(
setupComponentUpdateListener(parentEl, $element, $parent, index); setupComponentUpdateListener(parentEl, $element, $parent, index);
const $firstChild = $element.children[0]; const $firstChild = $element.children[0];
if ($firstChild.type === VirtualType.Component) { if (isComponentElement($firstChild)) {
$element.children[0] = initComponent(parentEl, $firstChild, $element, 0); $element.children = [initComponent(parentEl, $firstChild, $element, 0)];
} }
} }
@ -278,54 +264,42 @@ function mountChildren(
fragment?: DocumentFragment; fragment?: DocumentFragment;
}, },
) { ) {
const { children } = $element; $element.children = $element.children.map(($child, i) => {
for (let i = 0, l = children.length; i < l; i++) { return renderWithVirtual(parentEl, undefined, $child, $element, i, options);
const $child = children[i]; });
const $renderedChild = renderWithVirtual(parentEl, undefined, $child, $element, i, options);
if ($renderedChild !== $child) {
children[i] = $renderedChild;
}
}
} }
function unmountChildren(parentEl: HTMLElement, $element: VirtualElementComponent | VirtualElementFragment) { function unmountChildren(parentEl: HTMLElement, $element: VirtualElementComponent | VirtualElementFragment) {
for (const $child of $element.children) { $element.children.forEach(($child) => {
renderWithVirtual(parentEl, $child, undefined, $element, -1); renderWithVirtual(parentEl, $child, undefined, $element, -1);
} });
} }
function createNode($element: VirtualElementReal): Node { function createNode($element: VirtualElementReal): Node {
if ($element.type === VirtualType.Empty) { if (isEmptyElement($element)) {
return document.createTextNode(''); return document.createTextNode('');
} }
if ($element.type === VirtualType.Text) { if (isTextElement($element)) {
return document.createTextNode($element.value); return document.createTextNode($element.value);
} }
const { tag, props, children } = $element; const { tag, props, children = [] } = $element;
const element = document.createElement(tag); const element = document.createElement(tag);
processControlled(tag, props); processControlled(tag, props);
// eslint-disable-next-line no-restricted-syntax Object.entries(props).forEach(([key, value]) => {
for (const key in props) {
if (!props.hasOwnProperty(key)) continue;
if (props[key] !== undefined) { if (props[key] !== undefined) {
setAttribute(element, key, props[key]); setAttribute(element, key, value);
} }
} });
processUncontrolledOnMount(element, props); processUncontrolledOnMount(element, props);
for (let i = 0, l = children.length; i < l; i++) { $element.children = children.map(($child, i) => (
const $child = children[i]; renderWithVirtual(element, undefined, $child, $element, i)
const $renderedChild = renderWithVirtual(element, undefined, $child, $element, i); ));
if ($renderedChild !== $child) {
children[i] = $renderedChild;
}
}
return element; return element;
} }
@ -336,8 +310,8 @@ function remount(
node: Node | undefined, node: Node | undefined,
componentNextSibling?: ChildNode, componentNextSibling?: ChildNode,
) { ) {
const isComponent = $current.type === VirtualType.Component; const isComponent = isComponentElement($current);
const isFragment = !isComponent && $current.type === VirtualType.Fragment; const isFragment = !isComponent && isFragmentElement($current);
if (isComponent || isFragment) { if (isComponent || isFragment) {
if (isComponent) { if (isComponent) {
@ -361,25 +335,23 @@ function remount(
} }
function unmountRealTree($element: VirtualElement) { function unmountRealTree($element: VirtualElement) {
if ($element.type === VirtualType.Component) { if (isComponentElement($element)) {
unmountComponent($element.componentInstance); unmountComponent($element.componentInstance);
} else if ($element.type !== VirtualType.Fragment) { } else if (!isFragmentElement($element)) {
if ($element.type === VirtualType.Tag) { if (isTagElement($element)) {
extraClasses.delete($element.target!); extraClasses.delete($element.target!);
setElementRef($element, undefined);
removeAllDelegatedListeners($element.target!); removeAllDelegatedListeners($element.target!);
setElementRef($element, undefined);
} }
$element.target = undefined; // Help GC $element.target = undefined; // Help GC
if ($element.type !== VirtualType.Tag) { if (!isParentElement($element)) {
return; return;
} }
} }
for (const $child of $element.children) { $element.children.forEach(unmountRealTree);
unmountRealTree($child);
}
} }
function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, nextSibling?: ChildNode) { function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, nextSibling?: ChildNode) {
@ -391,7 +363,7 @@ function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, next
} }
function getNextSibling($current: VirtualElement): ChildNode | undefined { function getNextSibling($current: VirtualElement): ChildNode | undefined {
if ($current.type === VirtualType.Component || $current.type === VirtualType.Fragment) { if (isComponentElement($current) || isFragmentElement($current)) {
const lastChild = $current.children[$current.children.length - 1]; const lastChild = $current.children[$current.children.length - 1];
return getNextSibling(lastChild); return getNextSibling(lastChild);
} }
@ -411,16 +383,13 @@ function renderChildren(
} }
if (('props' in $new) && $new.props.teactFastList) { if (('props' in $new) && $new.props.teactFastList) {
renderFastListChildren($current, $new, currentEl); return renderFastListChildren($current, $new, currentEl);
return;
} }
const currentChildren = $current.children; const currentChildrenLength = $current.children.length;
const newChildren = $new.children; const newChildrenLength = $new.children.length;
const currentChildrenLength = currentChildren.length;
const newChildrenLength = newChildren.length;
const maxLength = Math.max(currentChildrenLength, newChildrenLength); const maxLength = Math.max(currentChildrenLength, newChildrenLength);
const newChildren = [];
const fragment = newChildrenLength > currentChildrenLength ? document.createDocumentFragment() : undefined; const fragment = newChildrenLength > currentChildrenLength ? document.createDocumentFragment() : undefined;
const lastCurrentChild = $current.children[currentChildrenLength - 1]; const lastCurrentChild = $current.children[currentChildrenLength - 1];
@ -429,111 +398,111 @@ function renderChildren(
); );
for (let i = 0; i < maxLength; i++) { for (let i = 0; i < maxLength; i++) {
const $renderedChild = renderWithVirtual( const $newChild = renderWithVirtual(
currentEl, currentEl,
currentChildren[i], $current.children[i],
newChildren[i], $new.children[i],
$new, $new,
i, i,
i >= currentChildrenLength ? { fragment } : { nextSibling, forceMoveToEnd }, i >= currentChildrenLength ? { fragment } : { nextSibling, forceMoveToEnd },
); );
if ($renderedChild && $renderedChild !== newChildren[i]) { if ($newChild) {
newChildren[i] = $renderedChild; newChildren.push($newChild);
} }
} }
if (fragment) { if (fragment) {
insertBefore(currentEl, fragment, fragmentNextSibling); insertBefore(currentEl, fragment, fragmentNextSibling);
} }
return newChildren;
} }
// This function allows to prepend/append a bunch of new DOM nodes to the top/bottom of preserved ones. // This function allows to prepend/append a bunch of new DOM nodes to the top/bottom of preserved ones.
// It also allows to selectively move particular preserved nodes within their DOM list. // It also allows to selectively move particular preserved nodes within their DOM list.
function renderFastListChildren($current: VirtualElementParent, $new: VirtualElementParent, currentEl: HTMLElement) { function renderFastListChildren($current: VirtualElementParent, $new: VirtualElementParent, currentEl: HTMLElement) {
const currentChildren = $current.children; const newKeys = new Set(
const newChildren = $new.children; $new.children.map(($newChild) => {
const key = 'props' in $newChild ? $newChild.props.key : undefined;
const newKeys = new Set(); if (DEBUG && isParentElement($newChild)) {
for (const $newChild of newChildren) { // eslint-disable-next-line no-null/no-null
const key = 'props' in $newChild ? $newChild.props.key : undefined; if (key === undefined || key === null) {
// eslint-disable-next-line no-console
console.warn('Missing `key` in `teactFastList`');
}
if (DEBUG && isParentElement($newChild)) { if (isFragmentElement($newChild)) {
// eslint-disable-next-line no-null/no-null throw new Error('[Teact] Fragment can not be child of container with `teactFastList`');
if (key === undefined || key === null) { }
// eslint-disable-next-line no-console
console.warn('Missing `key` in `teactFastList`');
} }
if ($newChild.type === VirtualType.Fragment) { return key;
throw new Error('[Teact] Fragment can not be child of container with `teactFastList`'); }),
} );
}
newKeys.add(key);
}
// Build a collection of old children that also remain in the new list // Build a collection of old children that also remain in the new list
let currentRemainingIndex = 0; let currentRemainingIndex = 0;
const remainingByKey: Record<string, { $element: VirtualElement; index: number; orderKey?: number }> = {}; const remainingByKey = $current.children
for (let i = 0, l = currentChildren.length; i < l; i++) { .reduce((acc, $currentChild, i) => {
const $currentChild = currentChildren[i]; let key = 'props' in $currentChild ? $currentChild.props.key : undefined;
// eslint-disable-next-line no-null/no-null
const isKeyPresent = key !== undefined && key !== null;
let key = 'props' in $currentChild ? $currentChild.props.key : undefined; // First we process removed children
// eslint-disable-next-line no-null/no-null if (isKeyPresent && !newKeys.has(key)) {
const isKeyPresent = key !== undefined && key !== null;
// First we process removed children
if (isKeyPresent && !newKeys.has(key)) {
renderWithVirtual(currentEl, $currentChild, undefined, $new, -1);
continue;
} else if (!isKeyPresent) {
const $newChild = newChildren[i];
const newChildKey = ($newChild && 'props' in $newChild) ? $newChild.props.key : undefined;
// If a non-key element remains at the same index we preserve it with a virtual `key`
if ($newChild && !newChildKey) {
key = `${INDEX_KEY_PREFIX}${i}`;
// Otherwise, we just remove it
} else {
renderWithVirtual(currentEl, $currentChild, undefined, $new, -1); renderWithVirtual(currentEl, $currentChild, undefined, $new, -1);
continue; return acc;
} else if (!isKeyPresent) {
const $newChild = $new.children[i];
const newChildKey = ($newChild && 'props' in $newChild) ? $newChild.props.key : undefined;
// If a non-key element remains at the same index we preserve it with a virtual `key`
if ($newChild && !newChildKey) {
key = `${INDEX_KEY_PREFIX}${i}`;
// Otherwise, we just remove it
} else {
renderWithVirtual(currentEl, $currentChild, undefined, $new, -1);
return acc;
}
} }
}
// Then we build up info about remaining children // Then we build up info about remaining children
remainingByKey[key] = { acc[key] = {
$element: $currentChild, $element: $currentChild,
index: currentRemainingIndex++, index: currentRemainingIndex++,
orderKey: 'props' in $currentChild ? $currentChild.props.teactOrderKey : undefined, orderKey: 'props' in $currentChild ? $currentChild.props.teactOrderKey : undefined,
}; };
} return acc;
}, {} as Record<string, { $element: VirtualElement; index: number; orderKey?: number }>);
let newChildren: VirtualElement[] = [];
let fragmentElements: VirtualElement[] | undefined;
let fragmentIndex: number | undefined; let fragmentIndex: number | undefined;
let fragmentSize: number | undefined;
let currentPreservedIndex = 0; let currentPreservedIndex = 0;
for (let i = 0, l = newChildren.length; i < l; i++) { $new.children.forEach(($newChild, i) => {
const $newChild = newChildren[i];
const key = 'props' in $newChild ? $newChild.props.key : `${INDEX_KEY_PREFIX}${i}`; const key = 'props' in $newChild ? $newChild.props.key : `${INDEX_KEY_PREFIX}${i}`;
const currentChildInfo = remainingByKey[key]; const currentChildInfo = remainingByKey[key];
if (!currentChildInfo) { if (!currentChildInfo) {
if (fragmentSize === undefined) { if (!fragmentElements) {
fragmentElements = [];
fragmentIndex = i; fragmentIndex = i;
fragmentSize = 0;
} }
fragmentSize++; fragmentElements.push($newChild);
continue; return;
} }
// This prepends new children to the top // This prepends new children to the top
if (fragmentSize) { if (fragmentElements) {
renderFragment(fragmentIndex!, fragmentSize, currentEl, $new); newChildren = newChildren.concat(renderFragment(fragmentElements, fragmentIndex!, currentEl, $new));
fragmentSize = undefined; fragmentElements = undefined;
fragmentIndex = undefined; fragmentIndex = undefined;
} }
@ -552,44 +521,34 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle
const nextSibling = currentEl.childNodes[isMovingDown ? i + 1 : i]; const nextSibling = currentEl.childNodes[isMovingDown ? i + 1 : i];
const options = shouldMoveNode ? (nextSibling ? { nextSibling } : { forceMoveToEnd: true }) : undefined; const options = shouldMoveNode ? (nextSibling ? { nextSibling } : { forceMoveToEnd: true }) : undefined;
const $renderedChild = renderWithVirtual(currentEl, currentChildInfo.$element, $newChild, $new, i, options); newChildren.push(renderWithVirtual(currentEl, currentChildInfo.$element, $newChild, $new, i, options));
if ($renderedChild !== $newChild) { });
newChildren[i] = $renderedChild;
}
}
// This appends new children to the bottom // This appends new children to the bottom
if (fragmentSize) { if (fragmentElements) {
renderFragment(fragmentIndex!, fragmentSize, currentEl, $new); newChildren = newChildren.concat(renderFragment(fragmentElements, fragmentIndex!, currentEl, $new));
} }
return newChildren;
} }
function renderFragment( function renderFragment(
fragmentIndex: number, fragmentSize: number, parentEl: HTMLElement, $parent: VirtualElementParent, elements: VirtualElement[], fragmentIndex: number, parentEl: HTMLElement, $parent: VirtualElementParent,
) { ) {
const nextSibling = parentEl.childNodes[fragmentIndex]; const nextSibling = parentEl.childNodes[fragmentIndex];
if (fragmentSize === 1) { if (elements.length === 1) {
const $child = $parent.children[fragmentIndex]; return [renderWithVirtual(parentEl, undefined, elements[0], $parent, fragmentIndex, { nextSibling })];
const $renderedChild = renderWithVirtual(parentEl, undefined, $child, $parent, fragmentIndex, { nextSibling });
if ($renderedChild !== $child) {
$parent.children[fragmentIndex] = $renderedChild;
}
return;
} }
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
const newChildren = elements.map(($element, i) => (
for (let i = fragmentIndex; i < fragmentIndex + fragmentSize; i++) { renderWithVirtual(parentEl, undefined, $element, $parent, fragmentIndex + i, { fragment })
const $child = $parent.children[i]; ));
const $renderedChild = renderWithVirtual(parentEl, undefined, $child, $parent, i, { fragment });
if ($renderedChild !== $child) {
$parent.children[i] = $renderedChild;
}
}
insertBefore(parentEl, fragment, nextSibling); insertBefore(parentEl, fragment, nextSibling);
return newChildren;
} }
function setElementRef($element: VirtualElementTag, htmlElement: HTMLElement | undefined) { function setElementRef($element: VirtualElementTag, htmlElement: HTMLElement | undefined) {
@ -620,7 +579,7 @@ function processControlled(tag: string, props: AnyLiteral) {
} = props; } = props;
props.onChange = undefined; props.onChange = undefined;
props.onInput = (e: ChangeEvent<HTMLInputElement>) => { props.onInput = (e: React.ChangeEvent<HTMLInputElement>) => {
onInput?.(e); onInput?.(e);
onChange?.(e); onChange?.(e);
@ -665,7 +624,7 @@ function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag,
const currentEntries = Object.entries($current.props); const currentEntries = Object.entries($current.props);
const newEntries = Object.entries($new.props); const newEntries = Object.entries($new.props);
for (const [key, currentValue] of currentEntries) { currentEntries.forEach(([key, currentValue]) => {
const newValue = $new.props[key]; const newValue = $new.props[key];
if ( if (
@ -677,15 +636,15 @@ function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag,
) { ) {
removeAttribute(element, key, currentValue); removeAttribute(element, key, currentValue);
} }
} });
for (const [key, newValue] of newEntries) { newEntries.forEach(([key, newValue]) => {
const currentValue = $current.props[key]; const currentValue = $current.props[key];
if (newValue !== undefined && newValue !== currentValue) { if (newValue !== undefined && newValue !== currentValue) {
setAttribute(element, key, newValue); setAttribute(element, key, newValue);
} }
} });
} }
function setAttribute(element: HTMLElement, key: string, value: any) { function setAttribute(element: HTMLElement, key: string, value: any) {
@ -768,9 +727,9 @@ export function addExtraClass(element: Element, className: string, forceSingle =
if (!forceSingle) { if (!forceSingle) {
const classNames = className.split(' '); const classNames = className.split(' ');
if (classNames.length > 1) { if (classNames.length > 1) {
for (const cn of classNames) { classNames.forEach((cn) => {
addExtraClass(element, cn, true); addExtraClass(element, cn, true);
} });
return; return;
} }
@ -790,9 +749,9 @@ export function removeExtraClass(element: Element, className: string, forceSingl
if (!forceSingle) { if (!forceSingle) {
const classNames = className.split(' '); const classNames = className.split(' ');
if (classNames.length > 1) { if (classNames.length > 1) {
for (const cn of classNames) { classNames.forEach((cn) => {
removeExtraClass(element, cn, true); removeExtraClass(element, cn, true);
} });
return; return;
} }
@ -814,9 +773,9 @@ export function toggleExtraClass(element: Element, className: string, force?: bo
if (!forceSingle) { if (!forceSingle) {
const classNames = className.split(' '); const classNames = className.split(' ');
if (classNames.length > 1) { if (classNames.length > 1) {
for (const cn of classNames) { classNames.forEach((cn) => {
toggleExtraClass(element, cn, force, true); toggleExtraClass(element, cn, force, true);
} });
return; return;
} }

View File

@ -16,7 +16,7 @@ export type FC_withDebug =
FC FC
& { DEBUG_contentComponentName?: string }; & { DEBUG_contentComponentName?: string };
export enum VirtualType { export enum VirtualElementTypesEnum {
Empty, Empty,
Text, Text,
Tag, Tag,
@ -25,18 +25,18 @@ export enum VirtualType {
} }
interface VirtualElementEmpty { interface VirtualElementEmpty {
type: VirtualType.Empty; type: VirtualElementTypesEnum.Empty;
target?: Node; target?: Node;
} }
interface VirtualElementText { interface VirtualElementText {
type: VirtualType.Text; type: VirtualElementTypesEnum.Text;
target?: Node; target?: Node;
value: string; value: string;
} }
export interface VirtualElementTag { export interface VirtualElementTag {
type: VirtualType.Tag; type: VirtualElementTypesEnum.Tag;
target?: HTMLElement; target?: HTMLElement;
tag: string; tag: string;
props: Props; props: Props;
@ -44,14 +44,14 @@ export interface VirtualElementTag {
} }
export interface VirtualElementComponent { export interface VirtualElementComponent {
type: VirtualType.Component; type: VirtualElementTypesEnum.Component;
componentInstance: ComponentInstance; componentInstance: ComponentInstance;
props: Props; props: Props;
children: VirtualElementChildren; children: VirtualElementChildren;
} }
export interface VirtualElementFragment { export interface VirtualElementFragment {
type: VirtualType.Fragment; type: VirtualElementTypesEnum.Fragment;
children: VirtualElementChildren; children: VirtualElementChildren;
} }
@ -71,8 +71,8 @@ interface ComponentInstance {
props: Props; props: Props;
renderedValue?: any; renderedValue?: any;
mountState: MountState; mountState: MountState;
hooks?: { hooks: {
state?: { state: {
cursor: number; cursor: number;
byCursor: { byCursor: {
value: any; value: any;
@ -80,7 +80,7 @@ interface ComponentInstance {
setter: StateHookSetter<any>; setter: StateHookSetter<any>;
}[]; }[];
}; };
effects?: { effects: {
cursor: number; cursor: number;
byCursor: { byCursor: {
dependencies?: readonly any[]; dependencies?: readonly any[];
@ -89,14 +89,14 @@ interface ComponentInstance {
releaseSignals?: NoneToVoidFunction; releaseSignals?: NoneToVoidFunction;
}[]; }[];
}; };
memos?: { memos: {
cursor: number; cursor: number;
byCursor: { byCursor: {
value: any; value: any;
dependencies: any[]; dependencies: any[];
}[]; }[];
}; };
refs?: { refs: {
cursor: number; cursor: number;
byCursor: { byCursor: {
current: any; current: any;
@ -141,12 +141,28 @@ const DEBUG_SILENT_RENDERS_FOR = new Set(['TeactMemoWrapper', 'TeactNContainer',
let lastComponentId = 0; let lastComponentId = 0;
let renderingInstance: ComponentInstance; let renderingInstance: ComponentInstance;
export function isEmptyElement($element: VirtualElement): $element is VirtualElementEmpty {
return $element.type === VirtualElementTypesEnum.Empty;
}
export function isTextElement($element: VirtualElement): $element is VirtualElementText {
return $element.type === VirtualElementTypesEnum.Text;
}
export function isTagElement($element: VirtualElement): $element is VirtualElementTag {
return $element.type === VirtualElementTypesEnum.Tag;
}
export function isComponentElement($element: VirtualElement): $element is VirtualElementComponent {
return $element.type === VirtualElementTypesEnum.Component;
}
export function isFragmentElement($element: VirtualElement): $element is VirtualElementFragment {
return $element.type === VirtualElementTypesEnum.Fragment;
}
export function isParentElement($element: VirtualElement): $element is VirtualElementParent { export function isParentElement($element: VirtualElement): $element is VirtualElementParent {
return ( return isTagElement($element) || isComponentElement($element) || isFragmentElement($element);
$element.type === VirtualType.Tag
|| $element.type === VirtualType.Component
|| $element.type === VirtualType.Fragment
);
} }
function createElement( function createElement(
@ -165,7 +181,7 @@ function createElement(
function buildFragmentElement(children: any[]): VirtualElementFragment { function buildFragmentElement(children: any[]): VirtualElementFragment {
return { return {
type: VirtualType.Fragment, type: VirtualElementTypesEnum.Fragment,
children: buildChildren(children, true), children: buildChildren(children, true),
}; };
} }
@ -177,11 +193,29 @@ function createComponentInstance(Component: FC, props: Props, children: any[]):
const componentInstance: ComponentInstance = { const componentInstance: ComponentInstance = {
id: ++lastComponentId, id: ++lastComponentId,
$element: undefined as unknown as VirtualElementComponent, $element: {} as VirtualElementComponent,
Component, Component,
name: Component.name, name: Component.name,
props, props,
mountState: MountState.New, mountState: MountState.New,
hooks: {
state: {
cursor: 0,
byCursor: [],
},
effects: {
cursor: 0,
byCursor: [],
},
memos: {
cursor: 0,
byCursor: [],
},
refs: {
cursor: 0,
byCursor: [],
},
},
}; };
componentInstance.$element = buildComponentElement(componentInstance); componentInstance.$element = buildComponentElement(componentInstance);
@ -194,7 +228,7 @@ function buildComponentElement(
children?: VirtualElementChildren, children?: VirtualElementChildren,
): VirtualElementComponent { ): VirtualElementComponent {
return { return {
type: VirtualType.Component, type: VirtualElementTypesEnum.Component,
componentInstance, componentInstance,
props: componentInstance.props, props: componentInstance.props,
children: children ? buildChildren(children, true) : [], children: children ? buildChildren(children, true) : [],
@ -203,7 +237,7 @@ function buildComponentElement(
function buildTagElement(tag: string, props: Props, children: any[]): VirtualElementTag { function buildTagElement(tag: string, props: Props, children: any[]): VirtualElementTag {
return { return {
type: VirtualType.Tag, type: VirtualElementTypesEnum.Tag,
tag, tag,
props, props,
children: buildChildren(children), children: buildChildren(children),
@ -253,12 +287,12 @@ function isEmptyPlaceholder(child: any) {
function buildChildElement(child: any): VirtualElement { function buildChildElement(child: any): VirtualElement {
if (isEmptyPlaceholder(child)) { if (isEmptyPlaceholder(child)) {
return { type: VirtualType.Empty }; return { type: VirtualElementTypesEnum.Empty };
} else if (isParentElement(child)) { } else if (isParentElement(child)) {
return child; return child;
} else { } else {
return { return {
type: VirtualType.Text, type: VirtualElementTypesEnum.Text,
value: String(child), value: String(child),
}; };
} }
@ -374,20 +408,10 @@ export function renderComponent(componentInstance: ComponentInstance) {
safeExec(() => { safeExec(() => {
renderingInstance = componentInstance; renderingInstance = componentInstance;
if (componentInstance.hooks) { componentInstance.hooks.state.cursor = 0;
if (componentInstance.hooks.state) { componentInstance.hooks.effects.cursor = 0;
componentInstance.hooks.state.cursor = 0; componentInstance.hooks.memos.cursor = 0;
} componentInstance.hooks.refs.cursor = 0;
if (componentInstance.hooks.effects) {
componentInstance.hooks.effects.cursor = 0;
}
if (componentInstance.hooks.memos) {
componentInstance.hooks.memos.cursor = 0;
}
if (componentInstance.hooks.refs) {
componentInstance.hooks.refs.cursor = 0;
}
}
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
let DEBUG_startAt: number | undefined; let DEBUG_startAt: number | undefined;
@ -445,12 +469,7 @@ export function renderComponent(componentInstance: ComponentInstance) {
componentInstance.renderedValue = newRenderedValue; componentInstance.renderedValue = newRenderedValue;
const children = Array.isArray(newRenderedValue) ? newRenderedValue : [newRenderedValue]; const children = Array.isArray(newRenderedValue) ? newRenderedValue : [newRenderedValue];
componentInstance.$element = buildComponentElement(componentInstance, children);
if (componentInstance.mountState === MountState.New) {
componentInstance.$element.children = buildChildren(children, true);
} else {
componentInstance.$element = buildComponentElement(componentInstance, children);
}
return componentInstance.$element; return componentInstance.$element;
} }
@ -460,11 +479,11 @@ export function hasElementChanged($old: VirtualElement, $new: VirtualElement) {
return true; return true;
} else if ($old.type !== $new.type) { } else if ($old.type !== $new.type) {
return true; return true;
} else if ($old.type === VirtualType.Text && $new.type === VirtualType.Text) { } else if (isTextElement($old) && isTextElement($new)) {
return $old.value !== $new.value; return $old.value !== $new.value;
} else if ($old.type === VirtualType.Tag && $new.type === VirtualType.Tag) { } else if (isTagElement($old) && isTagElement($new)) {
return ($old.tag !== $new.tag) || ($old.props.key !== $new.props.key); return ($old.tag !== $new.tag) || ($old.props.key !== $new.props.key);
} else if ($old.type === VirtualType.Component && $new.type === VirtualType.Component) { } else if (isComponentElement($old) && isComponentElement($new)) {
return ( return (
$old.componentInstance.Component !== $new.componentInstance.Component $old.componentInstance.Component !== $new.componentInstance.Component
) || ( ) || (
@ -488,16 +507,14 @@ export function unmountComponent(componentInstance: ComponentInstance) {
idsToExcludeFromUpdate.add(componentInstance.id); idsToExcludeFromUpdate.add(componentInstance.id);
if (componentInstance.hooks?.effects) { componentInstance.hooks.effects.byCursor.forEach((effect) => {
for (const effect of componentInstance.hooks.effects.byCursor) { if (effect.cleanup) {
if (effect.cleanup) { safeExec(effect.cleanup);
safeExec(effect.cleanup);
}
effect.cleanup = undefined;
effect.releaseSignals?.();
} }
}
effect.cleanup = undefined;
effect.releaseSignals?.();
});
componentInstance.mountState = MountState.Unmounted; componentInstance.mountState = MountState.Unmounted;
@ -506,39 +523,27 @@ export function unmountComponent(componentInstance: ComponentInstance) {
// We need to remove all references to DOM objects. We also clean all other references, just in case // We need to remove all references to DOM objects. We also clean all other references, just in case
function helpGc(componentInstance: ComponentInstance) { function helpGc(componentInstance: ComponentInstance) {
const { componentInstance.hooks.effects.byCursor.forEach((hook) => {
effects, state, memos, refs, hook.schedule = undefined as any;
} = componentInstance.hooks || {}; hook.cleanup = undefined as any;
hook.releaseSignals = undefined as any;
hook.dependencies = undefined;
});
if (effects) { componentInstance.hooks.state.byCursor.forEach((hook) => {
for (const hook of effects.byCursor) { hook.value = undefined;
hook.schedule = undefined as any; hook.nextValue = undefined;
hook.cleanup = undefined as any; hook.setter = undefined as any;
hook.releaseSignals = undefined as any; });
hook.dependencies = undefined;
}
}
if (state) { componentInstance.hooks.memos.byCursor.forEach((hook) => {
for (const hook of state.byCursor) { hook.value = undefined as any;
hook.value = undefined; hook.dependencies = undefined as any;
hook.nextValue = undefined; });
hook.setter = undefined as any;
}
}
if (memos) { componentInstance.hooks.refs.byCursor.forEach((hook) => {
for (const hook of memos.byCursor) { hook.current = undefined as any;
hook.value = undefined as any; });
hook.dependencies = undefined as any;
}
}
if (refs) {
for (const hook of refs.byCursor) {
hook.current = undefined as any;
}
}
componentInstance.hooks = undefined as any; componentInstance.hooks = undefined as any;
componentInstance.$element = undefined as any; componentInstance.$element = undefined as any;
@ -553,11 +558,9 @@ function prepareComponentForFrame(componentInstance: ComponentInstance) {
return; return;
} }
if (componentInstance.hooks?.state) { componentInstance.hooks.state.byCursor.forEach((hook) => {
for (const hook of componentInstance.hooks.state.byCursor) { hook.value = hook.nextValue;
hook.value = hook.nextValue; });
}
}
} }
function forceUpdateComponent(componentInstance: ComponentInstance) { function forceUpdateComponent(componentInstance: ComponentInstance) {
@ -577,13 +580,6 @@ function forceUpdateComponent(componentInstance: ComponentInstance) {
export function useState<T>(): [T | undefined, StateHookSetter<T | undefined>]; export function useState<T>(): [T | undefined, StateHookSetter<T | undefined>];
export function useState<T>(initial: T, debugKey?: string): [T, StateHookSetter<T>]; export function useState<T>(initial: T, debugKey?: string): [T, StateHookSetter<T>];
export function useState<T>(initial?: T, debugKey?: string): [T, StateHookSetter<T>] { export function useState<T>(initial?: T, debugKey?: string): [T, StateHookSetter<T>] {
if (!renderingInstance.hooks) {
renderingInstance.hooks = {};
}
if (!renderingInstance.hooks.state) {
renderingInstance.hooks.state = { cursor: 0, byCursor: [] };
}
const { cursor, byCursor } = renderingInstance.hooks.state; const { cursor, byCursor } = renderingInstance.hooks.state;
const componentInstance = renderingInstance; const componentInstance = renderingInstance;
@ -636,13 +632,6 @@ function useEffectBase(
dependencies?: readonly any[], dependencies?: readonly any[],
debugKey?: string, debugKey?: string,
) { ) {
if (!renderingInstance.hooks) {
renderingInstance.hooks = {};
}
if (!renderingInstance.hooks.effects) {
renderingInstance.hooks.effects = { cursor: 0, byCursor: [] };
}
const { cursor, byCursor } = renderingInstance.hooks.effects; const { cursor, byCursor } = renderingInstance.hooks.effects;
const componentInstance = renderingInstance; const componentInstance = renderingInstance;
@ -722,7 +711,7 @@ function useEffectBase(
if (dependencies && byCursor[cursor]?.dependencies) { if (dependencies && byCursor[cursor]?.dependencies) {
if (dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies![i])) { if (dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies![i])) {
if (DEBUG && debugKey) { if (debugKey) {
const causedBy = dependencies.reduce((res, newValue, i) => { const causedBy = dependencies.reduce((res, newValue, i) => {
const prevValue = byCursor[cursor].dependencies![i]; const prevValue = byCursor[cursor].dependencies![i];
if (newValue !== prevValue) { if (newValue !== prevValue) {
@ -770,9 +759,7 @@ function useEffectBase(
} }
return () => { return () => {
for (const cleanup of cleanups) { cleanups.forEach((cleanup) => cleanup());
cleanup();
}
}; };
} }
@ -797,13 +784,6 @@ export function useMemo<T extends any>(
debugKey?: string, debugKey?: string,
debugHitRateKey?: string, debugHitRateKey?: string,
): T { ): T {
if (!renderingInstance.hooks) {
renderingInstance.hooks = {};
}
if (!renderingInstance.hooks.memos) {
renderingInstance.hooks.memos = { cursor: 0, byCursor: [] };
}
const { cursor, byCursor } = renderingInstance.hooks.memos; const { cursor, byCursor } = renderingInstance.hooks.memos;
let { value } = byCursor[cursor] || {}; let { value } = byCursor[cursor] || {};
@ -881,13 +861,6 @@ export function useRef<T>(): { current: T | undefined }; // TT way (empty is `un
export function useRef<T>(initial: null): { current: T | null }; // React way (empty is `null`) export function useRef<T>(initial: null): { current: T | null }; // React way (empty is `null`)
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
export function useRef<T>(initial?: T | null) { export function useRef<T>(initial?: T | null) {
if (!renderingInstance.hooks) {
renderingInstance.hooks = {};
}
if (!renderingInstance.hooks.refs) {
renderingInstance.hooks.refs = { cursor: 0, byCursor: [] };
}
const { cursor, byCursor } = renderingInstance.hooks.refs; const { cursor, byCursor } = renderingInstance.hooks.refs;
if (!byCursor[cursor]) { if (!byCursor[cursor]) {
byCursor[cursor] = { byCursor[cursor] = {