[Perf] Teact: Optimizations

This commit is contained in:
Alexander Zinchuk 2023-07-05 13:16:16 +02:00
parent 31bd0d909d
commit 696e8ad256
2 changed files with 307 additions and 239 deletions

View File

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

View File

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