From 696e8ad256b3f4c00fba277f29871d46380ed79f Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 5 Jul 2023 13:16:16 +0200 Subject: [PATCH] [Perf] Teact: Optimizations --- src/lib/teact/teact-dom.ts | 331 +++++++++++++++++++++---------------- src/lib/teact/teact.ts | 215 +++++++++++++----------- 2 files changed, 307 insertions(+), 239 deletions(-) diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts index be1327346..dfc40c9b7 100644 --- a/src/lib/teact/teact-dom.ts +++ b/src/lib/teact/teact-dom.ts @@ -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( 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( 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( 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( 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( $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( } 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 = {}; + 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); + // 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) => { + props.onInput = (e: ChangeEvent) => { 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; } diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index 6c3ac85bd..b65269259 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -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; }[]; }; - 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 | undefined, StateHookSetter]; export function useState(initial: T, debugKey?: string): [T, StateHookSetter]; export function useState(initial?: T, debugKey?: string): [T, StateHookSetter] { + 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( 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(): { current: T | undefined }; // TT way (empty is `un export function useRef(initial: null): { current: T | null }; // React way (empty is `null`) // eslint-disable-next-line no-null/no-null export function useRef(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] = {