Revert "Revert "[Perf] Teact: Optimizations""
This reverts commit 8c892278660e633a5675b4e7d6c22aadd55578c4.
This commit is contained in:
parent
255fb11d94
commit
b6982dcd46
@ -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;
|
||||
}
|
||||
|
||||
@ -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] = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user