Revert "Revert "[Perf] Teact: Optimizations""

This reverts commit 8c892278660e633a5675b4e7d6c22aadd55578c4.
This commit is contained in:
Alexander Zinchuk 2023-07-20 15:57:57 +02:00
parent 255fb11d94
commit b6982dcd46
2 changed files with 307 additions and 239 deletions

View File

@ -1,3 +1,4 @@
import type { ChangeEvent } from 'react';
import type { import type {
VirtualElement, VirtualElement,
VirtualElementChildren, VirtualElementChildren,
@ -10,16 +11,12 @@ 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';
@ -58,10 +55,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 $newElement = renderWithVirtual(parentEl, $head.children[0], $element, $head, 0); const $renderedChild = renderWithVirtual(parentEl, $head.children[0], $element, $head, 0);
runImmediateEffects?.(); runImmediateEffects?.();
$head.children = $newElement ? [$newElement] : []; $head.children = $renderedChild ? [$renderedChild] : [];
if (process.env.APP_ENV === 'perf') { if (process.env.APP_ENV === 'perf') {
DEBUG_virtualTreeSize = 0; DEBUG_virtualTreeSize = 0;
@ -89,12 +86,12 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
const { skipComponentUpdate, fragment } = options; const { skipComponentUpdate, fragment } = options;
let { nextSibling } = options; let { nextSibling } = options;
const isCurrentComponent = $current && isComponentElement($current); const isCurrentComponent = $current && $current.type === VirtualType.Component;
const isNewComponent = $new && isComponentElement($new); const isNewComponent = $new && $new.type === VirtualType.Component;
const $newAsReal = $new as VirtualElementReal; const $newAsReal = $new as VirtualElementReal;
const isCurrentFragment = $current && !isCurrentComponent && isFragmentElement($current); const isCurrentFragment = $current && !isCurrentComponent && $current.type === VirtualType.Fragment;
const isNewFragment = $new && !isNewComponent && isFragmentElement($new); const isNewFragment = $new && !isNewComponent && $new.type === VirtualType.Fragment;
if ( if (
!skipComponentUpdate !skipComponentUpdate
@ -132,12 +129,19 @@ 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 node = createNode($newAsReal); const canSetText = $parent.children.length === 1 && $newAsReal.type === VirtualType.Text;
$newAsReal.target = node;
insertBefore(fragment || parentEl, node, nextSibling);
if (isTagElement($newAsReal)) { if (canSetText) {
setElementRef($newAsReal, node as HTMLElement); parentEl.textContent = 'value' in $newAsReal ? $newAsReal.value : '';
$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) {
@ -156,12 +160,27 @@ 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 node = createNode($newAsReal); const canSetText = $parent.children.length === 1
$newAsReal.target = node; && $newAsReal.type === VirtualType.Text
remount(parentEl, $current, node, nextSibling); && ($current.type === VirtualType.Text || $current.type === VirtualType.Empty)
&& (!parentEl.firstChild || parentEl.firstChild === $current.target);
if (isTagElement($newAsReal)) { if (canSetText) {
setElementRef($newAsReal, node as HTMLElement); const value = 'value' in $newAsReal ? $newAsReal.value : '';
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 {
@ -169,7 +188,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
const isFragment = isCurrentFragment && isNewFragment; const isFragment = isCurrentFragment && isNewFragment;
if (isComponent || isFragment) { if (isComponent || isFragment) {
($new as VirtualElementComponent | VirtualElementFragment).children = renderChildren( renderChildren(
$current, $current,
$new as VirtualElementComponent | VirtualElementFragment, $new as VirtualElementComponent | VirtualElementFragment,
parentEl, parentEl,
@ -183,7 +202,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 = isTagElement($current); const isTag = $current.type === VirtualType.Tag;
if (isTag) { if (isTag) {
const $newAsTag = $new as VirtualElementTag; const $newAsTag = $new as VirtualElementTag;
@ -195,12 +214,7 @@ 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,
);
} }
} }
} }
@ -222,8 +236,8 @@ function initComponent(
setupComponentUpdateListener(parentEl, $element, $parent, index); setupComponentUpdateListener(parentEl, $element, $parent, index);
const $firstChild = $element.children[0]; const $firstChild = $element.children[0];
if (isComponentElement($firstChild)) { if ($firstChild.type === VirtualType.Component) {
$element.children = [initComponent(parentEl, $firstChild, $element, 0)]; $element.children[0] = initComponent(parentEl, $firstChild, $element, 0);
} }
} }
@ -264,42 +278,54 @@ function mountChildren(
fragment?: DocumentFragment; fragment?: DocumentFragment;
}, },
) { ) {
$element.children = $element.children.map(($child, i) => { const { children } = $element;
return renderWithVirtual(parentEl, undefined, $child, $element, i, options); for (let i = 0, l = children.length; i < l; i++) {
}); 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) {
$element.children.forEach(($child) => { for (const $child of $element.children) {
renderWithVirtual(parentEl, $child, undefined, $element, -1); renderWithVirtual(parentEl, $child, undefined, $element, -1);
}); }
} }
function createNode($element: VirtualElementReal): Node { function createNode($element: VirtualElementReal): Node {
if (isEmptyElement($element)) { if ($element.type === VirtualType.Empty) {
return document.createTextNode(''); return document.createTextNode('');
} }
if (isTextElement($element)) { if ($element.type === VirtualType.Text) {
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);
Object.entries(props).forEach(([key, value]) => { // eslint-disable-next-line no-restricted-syntax
for (const key in props) {
if (!props.hasOwnProperty(key)) continue;
if (props[key] !== undefined) { if (props[key] !== undefined) {
setAttribute(element, key, value); setAttribute(element, key, props[key]);
} }
}); }
processUncontrolledOnMount(element, props); processUncontrolledOnMount(element, props);
$element.children = children.map(($child, i) => ( for (let i = 0, l = children.length; i < l; i++) {
renderWithVirtual(element, undefined, $child, $element, i) const $child = children[i];
)); const $renderedChild = renderWithVirtual(element, undefined, $child, $element, i);
if ($renderedChild !== $child) {
children[i] = $renderedChild;
}
}
return element; return element;
} }
@ -310,8 +336,8 @@ function remount(
node: Node | undefined, node: Node | undefined,
componentNextSibling?: ChildNode, componentNextSibling?: ChildNode,
) { ) {
const isComponent = isComponentElement($current); const isComponent = $current.type === VirtualType.Component;
const isFragment = !isComponent && isFragmentElement($current); const isFragment = !isComponent && $current.type === VirtualType.Fragment;
if (isComponent || isFragment) { if (isComponent || isFragment) {
if (isComponent) { if (isComponent) {
@ -335,23 +361,25 @@ function remount(
} }
function unmountRealTree($element: VirtualElement) { function unmountRealTree($element: VirtualElement) {
if (isComponentElement($element)) { if ($element.type === VirtualType.Component) {
unmountComponent($element.componentInstance); unmountComponent($element.componentInstance);
} else if (!isFragmentElement($element)) { } else if ($element.type !== VirtualType.Fragment) {
if (isTagElement($element)) { if ($element.type === VirtualType.Tag) {
extraClasses.delete($element.target!); extraClasses.delete($element.target!);
removeAllDelegatedListeners($element.target!);
setElementRef($element, undefined); setElementRef($element, undefined);
removeAllDelegatedListeners($element.target!);
} }
$element.target = undefined; // Help GC $element.target = undefined; // Help GC
if (!isParentElement($element)) { if ($element.type !== VirtualType.Tag) {
return; return;
} }
} }
$element.children.forEach(unmountRealTree); for (const $child of $element.children) {
unmountRealTree($child);
}
} }
function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, nextSibling?: ChildNode) { function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, nextSibling?: ChildNode) {
@ -363,7 +391,7 @@ function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, next
} }
function getNextSibling($current: VirtualElement): ChildNode | undefined { function getNextSibling($current: VirtualElement): ChildNode | undefined {
if (isComponentElement($current) || isFragmentElement($current)) { if ($current.type === VirtualType.Component || $current.type === VirtualType.Fragment) {
const lastChild = $current.children[$current.children.length - 1]; const lastChild = $current.children[$current.children.length - 1];
return getNextSibling(lastChild); return getNextSibling(lastChild);
} }
@ -383,13 +411,16 @@ function renderChildren(
} }
if (('props' in $new) && $new.props.teactFastList) { if (('props' in $new) && $new.props.teactFastList) {
return renderFastListChildren($current, $new, currentEl); renderFastListChildren($current, $new, currentEl);
return;
} }
const currentChildrenLength = $current.children.length; const currentChildren = $current.children;
const newChildrenLength = $new.children.length; const newChildren = $new.children;
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];
@ -398,111 +429,111 @@ function renderChildren(
); );
for (let i = 0; i < maxLength; i++) { for (let i = 0; i < maxLength; i++) {
const $newChild = renderWithVirtual( const $renderedChild = renderWithVirtual(
currentEl, currentEl,
$current.children[i], currentChildren[i],
$new.children[i], newChildren[i],
$new, $new,
i, i,
i >= currentChildrenLength ? { fragment } : { nextSibling, forceMoveToEnd }, i >= currentChildrenLength ? { fragment } : { nextSibling, forceMoveToEnd },
); );
if ($newChild) { if ($renderedChild && $renderedChild !== newChildren[i]) {
newChildren.push($newChild); newChildren[i] = $renderedChild;
} }
} }
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 newKeys = new Set( const currentChildren = $current.children;
$new.children.map(($newChild) => { const newChildren = $new.children;
const key = 'props' in $newChild ? $newChild.props.key : undefined;
if (DEBUG && isParentElement($newChild)) { const newKeys = new Set();
// eslint-disable-next-line no-null/no-null for (const $newChild of newChildren) {
if (key === undefined || key === null) { const key = 'props' in $newChild ? $newChild.props.key : undefined;
// eslint-disable-next-line no-console
console.warn('Missing `key` in `teactFastList`');
}
if (isFragmentElement($newChild)) { if (DEBUG && isParentElement($newChild)) {
throw new Error('[Teact] Fragment can not be child of container with `teactFastList`'); // eslint-disable-next-line no-null/no-null
} if (key === undefined || key === null) {
// eslint-disable-next-line no-console
console.warn('Missing `key` in `teactFastList`');
} }
return key; if ($newChild.type === VirtualType.Fragment) {
}), 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 = $current.children const remainingByKey: Record<string, { $element: VirtualElement; index: number; orderKey?: number }> = {};
.reduce((acc, $currentChild, i) => { for (let i = 0, l = currentChildren.length; i < l; i++) {
let key = 'props' in $currentChild ? $currentChild.props.key : undefined; const $currentChild = currentChildren[i];
// eslint-disable-next-line no-null/no-null
const isKeyPresent = key !== undefined && key !== null;
// First we process removed children let key = 'props' in $currentChild ? $currentChild.props.key : undefined;
if (isKeyPresent && !newKeys.has(key)) { // eslint-disable-next-line no-null/no-null
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);
return acc; continue;
} 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
acc[key] = { remainingByKey[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;
$new.children.forEach(($newChild, i) => { for (let i = 0, l = newChildren.length; i < l; 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 (!fragmentElements) { if (fragmentSize === undefined) {
fragmentElements = [];
fragmentIndex = i; fragmentIndex = i;
fragmentSize = 0;
} }
fragmentElements.push($newChild); fragmentSize++;
return; continue;
} }
// This prepends new children to the top // This prepends new children to the top
if (fragmentElements) { if (fragmentSize) {
newChildren = newChildren.concat(renderFragment(fragmentElements, fragmentIndex!, currentEl, $new)); renderFragment(fragmentIndex!, fragmentSize, currentEl, $new);
fragmentElements = undefined; fragmentSize = undefined;
fragmentIndex = undefined; fragmentIndex = undefined;
} }
@ -521,34 +552,44 @@ 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;
newChildren.push(renderWithVirtual(currentEl, currentChildInfo.$element, $newChild, $new, i, options)); const $renderedChild = renderWithVirtual(currentEl, currentChildInfo.$element, $newChild, $new, i, options);
}); if ($renderedChild !== $newChild) {
newChildren[i] = $renderedChild;
// This appends new children to the bottom }
if (fragmentElements) {
newChildren = newChildren.concat(renderFragment(fragmentElements, fragmentIndex!, currentEl, $new));
} }
return newChildren; // This appends new children to the bottom
if (fragmentSize) {
renderFragment(fragmentIndex!, fragmentSize, currentEl, $new);
}
} }
function renderFragment( function renderFragment(
elements: VirtualElement[], fragmentIndex: number, parentEl: HTMLElement, $parent: VirtualElementParent, fragmentIndex: number, fragmentSize: number, parentEl: HTMLElement, $parent: VirtualElementParent,
) { ) {
const nextSibling = parentEl.childNodes[fragmentIndex]; const nextSibling = parentEl.childNodes[fragmentIndex];
if (elements.length === 1) { if (fragmentSize === 1) {
return [renderWithVirtual(parentEl, undefined, elements[0], $parent, fragmentIndex, { nextSibling })]; const $child = $parent.children[fragmentIndex];
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) => (
renderWithVirtual(parentEl, undefined, $element, $parent, fragmentIndex + i, { fragment }) for (let i = fragmentIndex; i < fragmentIndex + fragmentSize; i++) {
)); 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) {
@ -579,7 +620,7 @@ function processControlled(tag: string, props: AnyLiteral) {
} = props; } = props;
props.onChange = undefined; props.onChange = undefined;
props.onInput = (e: React.ChangeEvent<HTMLInputElement>) => { props.onInput = (e: ChangeEvent<HTMLInputElement>) => {
onInput?.(e); onInput?.(e);
onChange?.(e); onChange?.(e);
@ -624,7 +665,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);
currentEntries.forEach(([key, currentValue]) => { for (const [key, currentValue] of currentEntries) {
const newValue = $new.props[key]; const newValue = $new.props[key];
if ( if (
@ -636,15 +677,15 @@ function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag,
) { ) {
removeAttribute(element, key, currentValue); removeAttribute(element, key, currentValue);
} }
}); }
newEntries.forEach(([key, newValue]) => { for (const [key, newValue] of newEntries) {
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) {
@ -727,9 +768,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) {
classNames.forEach((cn) => { for (const cn of classNames) {
addExtraClass(element, cn, true); addExtraClass(element, cn, true);
}); }
return; return;
} }
@ -749,9 +790,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) {
classNames.forEach((cn) => { for (const cn of classNames) {
removeExtraClass(element, cn, true); removeExtraClass(element, cn, true);
}); }
return; return;
} }
@ -773,9 +814,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) {
classNames.forEach((cn) => { for (const cn of classNames) {
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 VirtualElementTypesEnum { export enum VirtualType {
Empty, Empty,
Text, Text,
Tag, Tag,
@ -25,18 +25,18 @@ export enum VirtualElementTypesEnum {
} }
interface VirtualElementEmpty { interface VirtualElementEmpty {
type: VirtualElementTypesEnum.Empty; type: VirtualType.Empty;
target?: Node; target?: Node;
} }
interface VirtualElementText { interface VirtualElementText {
type: VirtualElementTypesEnum.Text; type: VirtualType.Text;
target?: Node; target?: Node;
value: string; value: string;
} }
export interface VirtualElementTag { export interface VirtualElementTag {
type: VirtualElementTypesEnum.Tag; type: VirtualType.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: VirtualElementTypesEnum.Component; type: VirtualType.Component;
componentInstance: ComponentInstance; componentInstance: ComponentInstance;
props: Props; props: Props;
children: VirtualElementChildren; children: VirtualElementChildren;
} }
export interface VirtualElementFragment { export interface VirtualElementFragment {
type: VirtualElementTypesEnum.Fragment; type: VirtualType.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,28 +141,12 @@ 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 isTagElement($element) || isComponentElement($element) || isFragmentElement($element); return (
$element.type === VirtualType.Tag
|| $element.type === VirtualType.Component
|| $element.type === VirtualType.Fragment
);
} }
function createElement( function createElement(
@ -181,7 +165,7 @@ function createElement(
function buildFragmentElement(children: any[]): VirtualElementFragment { function buildFragmentElement(children: any[]): VirtualElementFragment {
return { return {
type: VirtualElementTypesEnum.Fragment, type: VirtualType.Fragment,
children: buildChildren(children, true), children: buildChildren(children, true),
}; };
} }
@ -193,29 +177,11 @@ function createComponentInstance(Component: FC, props: Props, children: any[]):
const componentInstance: ComponentInstance = { const componentInstance: ComponentInstance = {
id: ++lastComponentId, id: ++lastComponentId,
$element: {} as VirtualElementComponent, $element: undefined as unknown 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);
@ -228,7 +194,7 @@ function buildComponentElement(
children?: VirtualElementChildren, children?: VirtualElementChildren,
): VirtualElementComponent { ): VirtualElementComponent {
return { return {
type: VirtualElementTypesEnum.Component, type: VirtualType.Component,
componentInstance, componentInstance,
props: componentInstance.props, props: componentInstance.props,
children: children ? buildChildren(children, true) : [], children: children ? buildChildren(children, true) : [],
@ -237,7 +203,7 @@ function buildComponentElement(
function buildTagElement(tag: string, props: Props, children: any[]): VirtualElementTag { function buildTagElement(tag: string, props: Props, children: any[]): VirtualElementTag {
return { return {
type: VirtualElementTypesEnum.Tag, type: VirtualType.Tag,
tag, tag,
props, props,
children: buildChildren(children), children: buildChildren(children),
@ -287,12 +253,12 @@ function isEmptyPlaceholder(child: any) {
function buildChildElement(child: any): VirtualElement { function buildChildElement(child: any): VirtualElement {
if (isEmptyPlaceholder(child)) { if (isEmptyPlaceholder(child)) {
return { type: VirtualElementTypesEnum.Empty }; return { type: VirtualType.Empty };
} else if (isParentElement(child)) { } else if (isParentElement(child)) {
return child; return child;
} else { } else {
return { return {
type: VirtualElementTypesEnum.Text, type: VirtualType.Text,
value: String(child), value: String(child),
}; };
} }
@ -408,10 +374,20 @@ export function renderComponent(componentInstance: ComponentInstance) {
safeExec(() => { safeExec(() => {
renderingInstance = componentInstance; renderingInstance = componentInstance;
componentInstance.hooks.state.cursor = 0; if (componentInstance.hooks) {
componentInstance.hooks.effects.cursor = 0; if (componentInstance.hooks.state) {
componentInstance.hooks.memos.cursor = 0; componentInstance.hooks.state.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;
@ -469,7 +445,12 @@ 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;
} }
@ -479,11 +460,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 (isTextElement($old) && isTextElement($new)) { } else if ($old.type === VirtualType.Text && $new.type === VirtualType.Text) {
return $old.value !== $new.value; return $old.value !== $new.value;
} else if (isTagElement($old) && isTagElement($new)) { } else if ($old.type === VirtualType.Tag && $new.type === VirtualType.Tag) {
return ($old.tag !== $new.tag) || ($old.props.key !== $new.props.key); return ($old.tag !== $new.tag) || ($old.props.key !== $new.props.key);
} else if (isComponentElement($old) && isComponentElement($new)) { } else if ($old.type === VirtualType.Component && $new.type === VirtualType.Component) {
return ( return (
$old.componentInstance.Component !== $new.componentInstance.Component $old.componentInstance.Component !== $new.componentInstance.Component
) || ( ) || (
@ -507,14 +488,16 @@ export function unmountComponent(componentInstance: ComponentInstance) {
idsToExcludeFromUpdate.add(componentInstance.id); idsToExcludeFromUpdate.add(componentInstance.id);
componentInstance.hooks.effects.byCursor.forEach((effect) => { if (componentInstance.hooks?.effects) {
if (effect.cleanup) { for (const effect of componentInstance.hooks.effects.byCursor) {
safeExec(effect.cleanup); if (effect.cleanup) {
} safeExec(effect.cleanup);
}
effect.cleanup = undefined; effect.cleanup = undefined;
effect.releaseSignals?.(); effect.releaseSignals?.();
}); }
}
componentInstance.mountState = MountState.Unmounted; componentInstance.mountState = MountState.Unmounted;
@ -523,27 +506,39 @@ 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) {
componentInstance.hooks.effects.byCursor.forEach((hook) => { const {
hook.schedule = undefined as any; effects, state, memos, refs,
hook.cleanup = undefined as any; } = componentInstance.hooks || {};
hook.releaseSignals = undefined as any;
hook.dependencies = undefined;
});
componentInstance.hooks.state.byCursor.forEach((hook) => { if (effects) {
hook.value = undefined; for (const hook of effects.byCursor) {
hook.nextValue = undefined; hook.schedule = undefined as any;
hook.setter = undefined as any; hook.cleanup = undefined as any;
}); hook.releaseSignals = undefined as any;
hook.dependencies = undefined;
}
}
componentInstance.hooks.memos.byCursor.forEach((hook) => { if (state) {
hook.value = undefined as any; for (const hook of state.byCursor) {
hook.dependencies = undefined as any; hook.value = undefined;
}); hook.nextValue = undefined;
hook.setter = undefined as any;
}
}
componentInstance.hooks.refs.byCursor.forEach((hook) => { if (memos) {
hook.current = undefined as any; for (const hook of memos.byCursor) {
}); 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;
@ -558,9 +553,11 @@ function prepareComponentForFrame(componentInstance: ComponentInstance) {
return; return;
} }
componentInstance.hooks.state.byCursor.forEach((hook) => { if (componentInstance.hooks?.state) {
hook.value = hook.nextValue; for (const hook of componentInstance.hooks.state.byCursor) {
}); hook.value = hook.nextValue;
}
}
} }
function forceUpdateComponent(componentInstance: ComponentInstance) { function forceUpdateComponent(componentInstance: ComponentInstance) {
@ -580,6 +577,13 @@ 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;
@ -632,6 +636,13 @@ 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;
@ -711,7 +722,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 (debugKey) { if (DEBUG && 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) {
@ -759,7 +770,9 @@ function useEffectBase(
} }
return () => { return () => {
cleanups.forEach((cleanup) => cleanup()); for (const cleanup of cleanups) {
cleanup();
}
}; };
} }
@ -784,6 +797,13 @@ 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] || {};
@ -861,6 +881,13 @@ 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] = {