Teact: Support components returning fragments
This commit is contained in:
parent
b2d4f78c65
commit
fa6eec434f
74
src/components/test/TestNoContainer.tsx
Normal file
74
src/components/test/TestNoContainer.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React, { useState } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
const INTERACTIVE = 'cursor: pointer; text-decoration: underline;';
|
||||||
|
|
||||||
|
const TestA = () => {
|
||||||
|
const [shouldRender, setShouldRender] = useState<boolean>(true);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
setShouldRender(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setShouldRender(true);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldRender) {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span onClick={handleClick} style={INTERACTIVE}>4</span>
|
||||||
|
<span onClick={handleClick} style={INTERACTIVE}>5</span>
|
||||||
|
<span onClick={handleClick} style={INTERACTIVE}>6</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestB = () => {
|
||||||
|
const [shouldRender, setShouldRender] = useState<boolean>(true);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
setShouldRender(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setShouldRender(true);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{shouldRender && <span>7</span>}
|
||||||
|
<span onClick={handleClick} style={INTERACTIVE}>8</span>
|
||||||
|
{shouldRender && (
|
||||||
|
<>
|
||||||
|
<span>9</span>
|
||||||
|
<span>10</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestNoContainer = () => {
|
||||||
|
const [aKey, setAKey] = useState(1);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
setAKey((current) => (current === 1 ? 0 : 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span onClick={handleClick} style={INTERACTIVE}>1</span>
|
||||||
|
<span>2</span>
|
||||||
|
<span>3</span>
|
||||||
|
<TestA key={aKey} />
|
||||||
|
<TestB />
|
||||||
|
<span>11</span>
|
||||||
|
<span>12</span>
|
||||||
|
<span>13</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestNoContainer;
|
||||||
@ -8,7 +8,7 @@ function tick() {
|
|||||||
<h2>It is {new Date().toLocaleTimeString()}.</h2>
|
<h2>It is {new Date().toLocaleTimeString()}.</h2>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
TeactDOM.render(element, document.getElementById('root'));
|
TeactDOM.render(element, document.getElementById('root')!);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(tick, 1000);
|
setInterval(tick, 1000);
|
||||||
|
|||||||
@ -30,7 +30,7 @@ updateWebmanifest();
|
|||||||
|
|
||||||
TeactDOM.render(
|
TeactDOM.render(
|
||||||
<App />,
|
<App />,
|
||||||
document.getElementById('root'),
|
document.getElementById('root')!,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
|||||||
@ -1,24 +1,25 @@
|
|||||||
import type {
|
import type {
|
||||||
VirtualElement,
|
VirtualElement,
|
||||||
VirtualElementComponent,
|
VirtualElementComponent,
|
||||||
VirtualRealElement,
|
VirtualElementTag,
|
||||||
|
VirtualElementParent,
|
||||||
VirtualElementChildren,
|
VirtualElementChildren,
|
||||||
|
VirtualElementReal,
|
||||||
} from './teact';
|
} from './teact';
|
||||||
import {
|
import {
|
||||||
hasElementChanged,
|
hasElementChanged,
|
||||||
isComponentElement,
|
isComponentElement,
|
||||||
isEmptyElement,
|
isTagElement,
|
||||||
isRealElement,
|
isParentElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
|
isEmptyElement,
|
||||||
mountComponent,
|
mountComponent,
|
||||||
renderComponent,
|
renderComponent,
|
||||||
unmountTree,
|
unmountComponent,
|
||||||
getTarget,
|
|
||||||
setTarget,
|
|
||||||
} from './teact';
|
} from './teact';
|
||||||
import generateIdFor from '../../util/generateIdFor';
|
import generateIdFor from '../../util/generateIdFor';
|
||||||
import { DEBUG } from '../../config';
|
import { DEBUG } from '../../config';
|
||||||
import { addEventListener, removeEventListener } from './dom-events';
|
import { addEventListener, removeAllDelegatedListeners, removeEventListener } from './dom-events';
|
||||||
import { unique } from '../../util/iteratees';
|
import { unique } from '../../util/iteratees';
|
||||||
|
|
||||||
type VirtualDomHead = {
|
type VirtualDomHead = {
|
||||||
@ -37,11 +38,7 @@ const headsByElement: Record<string, VirtualDomHead> = {};
|
|||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
let DEBUG_virtualTreeSize = 1;
|
let DEBUG_virtualTreeSize = 1;
|
||||||
|
|
||||||
function render($element?: VirtualElement, parentEl?: HTMLElement | null) {
|
function render($element: VirtualElement | undefined, parentEl: HTMLElement) {
|
||||||
if (!parentEl) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let headId = parentEl.getAttribute('data-teact-head-id');
|
let headId = parentEl.getAttribute('data-teact-head-id');
|
||||||
if (!headId) {
|
if (!headId) {
|
||||||
headId = generateIdFor(headsByElement);
|
headId = generateIdFor(headsByElement);
|
||||||
@ -50,7 +47,8 @@ function render($element?: VirtualElement, parentEl?: HTMLElement | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const $head = headsByElement[headId];
|
const $head = headsByElement[headId];
|
||||||
$head.children = [renderWithVirtual(parentEl, $head.children[0], $element, $head, 0) as VirtualElement];
|
const $newElement = renderWithVirtual(parentEl, $head.children[0], $element, $head, 0);
|
||||||
|
$head.children = $newElement ? [$newElement] : [];
|
||||||
|
|
||||||
if (process.env.APP_ENV === 'perf') {
|
if (process.env.APP_ENV === 'perf') {
|
||||||
DEBUG_virtualTreeSize = 0;
|
DEBUG_virtualTreeSize = 0;
|
||||||
@ -62,38 +60,36 @@ function render($element?: VirtualElement, parentEl?: HTMLElement | null) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderWithVirtual(
|
function renderWithVirtual<T extends VirtualElement | undefined>(
|
||||||
parentEl: HTMLElement,
|
parentEl: HTMLElement,
|
||||||
$current: VirtualElement | undefined,
|
$current: VirtualElement | undefined,
|
||||||
$new: VirtualElement | undefined,
|
$new: T,
|
||||||
$parent: VirtualRealElement | VirtualDomHead,
|
$parent: VirtualElementParent | VirtualDomHead,
|
||||||
index: number,
|
index: number,
|
||||||
{
|
options: {
|
||||||
skipComponentUpdate = false,
|
|
||||||
forceIndex = false,
|
|
||||||
fragment,
|
|
||||||
moveDirection,
|
|
||||||
}: {
|
|
||||||
skipComponentUpdate?: boolean;
|
skipComponentUpdate?: boolean;
|
||||||
forceIndex?: boolean;
|
nextSibling?: ChildNode;
|
||||||
fragment?: DocumentFragment;
|
fragment?: DocumentFragment;
|
||||||
moveDirection?: 'up' | 'down';
|
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
): T {
|
||||||
|
const { skipComponentUpdate, fragment } = options;
|
||||||
|
let { nextSibling } = options;
|
||||||
|
|
||||||
const isCurrentComponent = $current && isComponentElement($current);
|
const isCurrentComponent = $current && isComponentElement($current);
|
||||||
const isNewComponent = $new && isComponentElement($new);
|
const isNewComponent = $new && isComponentElement($new);
|
||||||
|
const $newAsReal = $new as VirtualElementReal;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!skipComponentUpdate
|
!skipComponentUpdate
|
||||||
&& isCurrentComponent && isNewComponent
|
&& isCurrentComponent && isNewComponent
|
||||||
&& !hasElementChanged($current!, $new!)
|
&& !hasElementChanged($current!, $new!)
|
||||||
) {
|
) {
|
||||||
$new = updateComponent($current as VirtualElementComponent, $new as VirtualElementComponent);
|
$new = updateComponent($current, $new as VirtualElementComponent) as typeof $new;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent element may have changed, so we need to update the listener closure.
|
// Parent element may have changed, so we need to update the listener closure.
|
||||||
if (!skipComponentUpdate && isNewComponent && ($new as VirtualElementComponent).componentInstance.isMounted) {
|
if (!skipComponentUpdate && isNewComponent && ($new as VirtualElementComponent).componentInstance.isMounted) {
|
||||||
setupComponentUpdateListener($new as VirtualElementComponent, $parent, index, parentEl);
|
setupComponentUpdateListener(parentEl, $new as VirtualElementComponent, $parent, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($current === $new) {
|
if ($current === $new) {
|
||||||
@ -101,72 +97,72 @@ function renderWithVirtual(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG && $new) {
|
if (DEBUG && $new) {
|
||||||
const newTarget = getTarget($new);
|
const newTarget = 'target' in $new && $new.target;
|
||||||
if (newTarget && (!$current || newTarget !== getTarget($current))) {
|
if (newTarget && (!$current || ('target' in $current && newTarget !== $current.target))) {
|
||||||
throw new Error('[Teact] Cached virtual element was moved within tree');
|
throw new Error('[Teact] Cached virtual element was moved within tree');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$current && $new) {
|
if (!$current && $new) {
|
||||||
if (isNewComponent) {
|
if (isNewComponent) {
|
||||||
$new = initComponent($new as VirtualElementComponent, $parent, index, parentEl);
|
$new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new;
|
||||||
}
|
mountComponentChildren(parentEl, $new as VirtualElementComponent, { nextSibling, fragment });
|
||||||
|
|
||||||
const node = createNode($new);
|
|
||||||
setTarget($new, node);
|
|
||||||
|
|
||||||
if (forceIndex && parentEl.childNodes[index]) {
|
|
||||||
parentEl.insertBefore(node, parentEl.childNodes[index]);
|
|
||||||
} else {
|
} else {
|
||||||
(fragment || parentEl).appendChild(node);
|
const node = createNode($newAsReal);
|
||||||
|
$newAsReal.target = node;
|
||||||
|
insertBefore(fragment || parentEl, node, nextSibling);
|
||||||
}
|
}
|
||||||
} else if ($current && !$new) {
|
} else if ($current && !$new) {
|
||||||
parentEl.removeChild(getTarget($current)!);
|
remount(parentEl, $current, undefined);
|
||||||
unmountTree($current);
|
|
||||||
} else if ($current && $new) {
|
} else if ($current && $new) {
|
||||||
if (hasElementChanged($current, $new)) {
|
if (hasElementChanged($current, $new)) {
|
||||||
|
if (!nextSibling) {
|
||||||
|
nextSibling = getNextSibling($current);
|
||||||
|
}
|
||||||
|
|
||||||
if (isNewComponent) {
|
if (isNewComponent) {
|
||||||
$new = initComponent($new as VirtualElementComponent, $parent, index, parentEl);
|
$new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as typeof $new;
|
||||||
|
remount(parentEl, $current, undefined);
|
||||||
|
mountComponentChildren(parentEl, $new as VirtualElementComponent, { nextSibling, fragment });
|
||||||
|
} else {
|
||||||
|
const node = createNode($newAsReal);
|
||||||
|
$newAsReal.target = node;
|
||||||
|
remount(parentEl, $current, node, nextSibling);
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = createNode($new);
|
|
||||||
setTarget($new, node);
|
|
||||||
parentEl.replaceChild(node, getTarget($current)!);
|
|
||||||
unmountTree($current);
|
|
||||||
} else {
|
} else {
|
||||||
const areComponents = isCurrentComponent && isNewComponent;
|
const isComponent = isCurrentComponent && isNewComponent;
|
||||||
const currentTarget = getTarget($current);
|
if (isComponent) {
|
||||||
|
($new as VirtualElementComponent).children = renderChildren(
|
||||||
|
$current,
|
||||||
|
$new as VirtualElementComponent,
|
||||||
|
parentEl,
|
||||||
|
nextSibling,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const $currentAsReal = $current as VirtualElementReal;
|
||||||
|
const currentTarget = $currentAsReal.target!;
|
||||||
|
|
||||||
if (!areComponents) {
|
$newAsReal.target = currentTarget;
|
||||||
setTarget($new, currentTarget!);
|
$currentAsReal.target = undefined; // Help GC
|
||||||
setTarget($current, undefined as any); // Help GC
|
|
||||||
|
|
||||||
if ('props' in $current && 'props' in $new) {
|
const isTag = isTagElement($current);
|
||||||
$new.props.ref = $current.props.ref;
|
if (isTag) {
|
||||||
}
|
const $newAsTag = $new as VirtualElementTag;
|
||||||
}
|
|
||||||
|
|
||||||
if (isRealElement($new)) {
|
$newAsTag.props.ref = $current.props.ref;
|
||||||
if (moveDirection) {
|
|
||||||
const node = currentTarget!;
|
|
||||||
const nextSibling = parentEl.childNodes[moveDirection === 'up' ? index : index + 1];
|
|
||||||
|
|
||||||
if (nextSibling) {
|
if (nextSibling) {
|
||||||
parentEl.insertBefore(node, nextSibling);
|
insertBefore(parentEl, currentTarget, nextSibling);
|
||||||
} else {
|
|
||||||
(fragment || parentEl).appendChild(node);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!areComponents) {
|
updateAttributes($current, $newAsTag, currentTarget as HTMLElement);
|
||||||
updateAttributes(($current as VirtualRealElement), $new, currentTarget as HTMLElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
$new.children = renderChildren(
|
$newAsTag.children = renderChildren(
|
||||||
($current as VirtualRealElement),
|
$current,
|
||||||
$new,
|
$newAsTag,
|
||||||
areComponents ? parentEl : currentTarget as HTMLElement,
|
currentTarget as HTMLElement,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -175,21 +171,20 @@ function renderWithVirtual(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initComponent(
|
function initComponent(
|
||||||
$element: VirtualElementComponent, $parent: VirtualRealElement | VirtualDomHead, index: number, parentEl: HTMLElement,
|
parentEl: HTMLElement,
|
||||||
|
$element: VirtualElementComponent,
|
||||||
|
$parent: VirtualElementParent | VirtualDomHead,
|
||||||
|
index: number,
|
||||||
) {
|
) {
|
||||||
if (!isComponentElement($element)) {
|
|
||||||
return $element;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { componentInstance } = $element;
|
const { componentInstance } = $element;
|
||||||
|
|
||||||
if (!componentInstance.isMounted) {
|
if (!componentInstance.isMounted) {
|
||||||
$element = mountComponent(componentInstance);
|
$element = mountComponent(componentInstance);
|
||||||
setupComponentUpdateListener($element, $parent, index, parentEl);
|
setupComponentUpdateListener(parentEl, $element, $parent, index);
|
||||||
|
|
||||||
const $firstChild = $element.children[0];
|
const $firstChild = $element.children[0];
|
||||||
if (isComponentElement($firstChild)) {
|
if (isComponentElement($firstChild)) {
|
||||||
$element.children = [initComponent($firstChild, $element, 0, parentEl)];
|
$element.children = [initComponent(parentEl, $firstChild, $element, 0)];
|
||||||
}
|
}
|
||||||
|
|
||||||
componentInstance.isMounted = true;
|
componentInstance.isMounted = true;
|
||||||
@ -205,7 +200,10 @@ function updateComponent($current: VirtualElementComponent, $new: VirtualElement
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupComponentUpdateListener(
|
function setupComponentUpdateListener(
|
||||||
$element: VirtualElementComponent, $parent: VirtualRealElement | VirtualDomHead, index: number, parentEl: HTMLElement,
|
parentEl: HTMLElement,
|
||||||
|
$element: VirtualElementComponent,
|
||||||
|
$parent: VirtualElementParent | VirtualDomHead,
|
||||||
|
index: number,
|
||||||
) {
|
) {
|
||||||
const { componentInstance } = $element;
|
const { componentInstance } = $element;
|
||||||
|
|
||||||
@ -217,11 +215,26 @@ function setupComponentUpdateListener(
|
|||||||
$parent,
|
$parent,
|
||||||
index,
|
index,
|
||||||
{ skipComponentUpdate: true },
|
{ skipComponentUpdate: true },
|
||||||
) as VirtualElementComponent;
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNode($element: VirtualElement): Node {
|
function mountComponentChildren(parentEl: HTMLElement, $element: VirtualElementComponent, options: {
|
||||||
|
nextSibling?: ChildNode;
|
||||||
|
fragment?: DocumentFragment;
|
||||||
|
}) {
|
||||||
|
$element.children = $element.children.map(($child, i) => {
|
||||||
|
return renderWithVirtual(parentEl, undefined, $child, $element, i, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmountComponentChildren(parentEl: HTMLElement, $element: VirtualElementComponent) {
|
||||||
|
$element.children.forEach(($child) => {
|
||||||
|
renderWithVirtual(parentEl, $child, undefined, $element, -1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNode($element: VirtualElementReal): Node {
|
||||||
if (isEmptyElement($element)) {
|
if (isEmptyElement($element)) {
|
||||||
return document.createTextNode('');
|
return document.createTextNode('');
|
||||||
}
|
}
|
||||||
@ -230,10 +243,6 @@ function createNode($element: VirtualElement): Node {
|
|||||||
return document.createTextNode($element.value);
|
return document.createTextNode($element.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isComponentElement($element)) {
|
|
||||||
return createNode($element.children[0] as VirtualElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tag, props, children = [] } = $element;
|
const { tag, props, children = [] } = $element;
|
||||||
const element = document.createElement(tag);
|
const element = document.createElement(tag);
|
||||||
|
|
||||||
@ -248,14 +257,83 @@ function createNode($element: VirtualElement): Node {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$element.children = children.map(($child, i) => (
|
$element.children = children.map(($child, i) => (
|
||||||
renderWithVirtual(element, undefined, $child, $element, i) as VirtualElement
|
renderWithVirtual(element, undefined, $child, $element, i)
|
||||||
));
|
));
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function remount(
|
||||||
|
parentEl: HTMLElement,
|
||||||
|
$current: VirtualElement,
|
||||||
|
node: Node | undefined,
|
||||||
|
componentNextSibling?: ChildNode,
|
||||||
|
) {
|
||||||
|
if (isComponentElement($current)) {
|
||||||
|
unmountComponent($current.componentInstance);
|
||||||
|
unmountComponentChildren(parentEl, $current);
|
||||||
|
|
||||||
|
if (node) {
|
||||||
|
insertBefore(parentEl, node, componentNextSibling);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (node) {
|
||||||
|
parentEl.replaceChild(node, $current.target!);
|
||||||
|
} else {
|
||||||
|
parentEl.removeChild($current.target!);
|
||||||
|
}
|
||||||
|
|
||||||
|
unmountRealTree($current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unmountRealTree($element: VirtualElement) {
|
||||||
|
if (isComponentElement($element)) {
|
||||||
|
unmountComponent($element.componentInstance);
|
||||||
|
} else {
|
||||||
|
if (isTagElement($element)) {
|
||||||
|
if ($element.target) {
|
||||||
|
removeAllDelegatedListeners($element.target as HTMLElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($element.props.ref) {
|
||||||
|
$element.props.ref.current = undefined; // Help GC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($element.target) {
|
||||||
|
$element.target = undefined; // Help GC
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isParentElement($element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$element.children.forEach(unmountRealTree);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, nextSibling?: ChildNode) {
|
||||||
|
if (nextSibling) {
|
||||||
|
parentEl.insertBefore(node, nextSibling);
|
||||||
|
} else {
|
||||||
|
parentEl.appendChild(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextSibling($current: VirtualElement): ChildNode | undefined {
|
||||||
|
if (isComponentElement($current)) {
|
||||||
|
const lastChild = $current.children[$current.children.length - 1];
|
||||||
|
return getNextSibling(lastChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = $current.target!;
|
||||||
|
const { nextSibling } = target;
|
||||||
|
return nextSibling || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function renderChildren(
|
function renderChildren(
|
||||||
$current: VirtualRealElement, $new: VirtualRealElement, currentEl: HTMLElement,
|
$current: VirtualElementParent, $new: VirtualElementParent, currentEl: HTMLElement, nextSibling?: ChildNode,
|
||||||
) {
|
) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
DEBUG_checkKeyUniqueness($new.children);
|
DEBUG_checkKeyUniqueness($new.children);
|
||||||
@ -269,7 +347,12 @@ function renderChildren(
|
|||||||
const newChildrenLength = $new.children.length;
|
const newChildrenLength = $new.children.length;
|
||||||
const maxLength = Math.max(currentChildrenLength, newChildrenLength);
|
const maxLength = Math.max(currentChildrenLength, newChildrenLength);
|
||||||
const newChildren = [];
|
const newChildren = [];
|
||||||
const fragment = newChildrenLength > currentChildrenLength + 1 ? document.createDocumentFragment() : undefined;
|
|
||||||
|
const fragment = newChildrenLength > currentChildrenLength ? document.createDocumentFragment() : undefined;
|
||||||
|
const lastCurrentChild = $current.children[currentChildrenLength - 1];
|
||||||
|
const fragmentNextSibling = nextSibling || (
|
||||||
|
newChildrenLength > currentChildrenLength && lastCurrentChild ? getNextSibling(lastCurrentChild) : undefined
|
||||||
|
);
|
||||||
|
|
||||||
for (let i = 0; i < maxLength; i++) {
|
for (let i = 0; i < maxLength; i++) {
|
||||||
const $newChild = renderWithVirtual(
|
const $newChild = renderWithVirtual(
|
||||||
@ -278,7 +361,7 @@ function renderChildren(
|
|||||||
$new.children[i],
|
$new.children[i],
|
||||||
$new,
|
$new,
|
||||||
i,
|
i,
|
||||||
i >= currentChildrenLength ? { fragment } : undefined,
|
i >= currentChildrenLength ? { fragment } : { nextSibling },
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($newChild) {
|
if ($newChild) {
|
||||||
@ -287,7 +370,7 @@ function renderChildren(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fragment) {
|
if (fragment) {
|
||||||
currentEl.appendChild(fragment);
|
insertBefore(currentEl, fragment, fragmentNextSibling);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newChildren;
|
return newChildren;
|
||||||
@ -295,13 +378,13 @@ function renderChildren(
|
|||||||
|
|
||||||
// This function allows to prepend/append a bunch of new DOM nodes to the top/bottom of preserved ones.
|
// This function allows to prepend/append a bunch of new DOM nodes to the top/bottom of preserved ones.
|
||||||
// It also allows to selectively move particular preserved nodes within their DOM list.
|
// It also allows to selectively move particular preserved nodes within their DOM list.
|
||||||
function renderFastListChildren($current: VirtualRealElement, $new: VirtualRealElement, currentEl: HTMLElement) {
|
function renderFastListChildren($current: VirtualElementParent, $new: VirtualElementParent, currentEl: HTMLElement) {
|
||||||
const newKeys = new Set(
|
const newKeys = new Set(
|
||||||
$new.children.map(($newChild) => {
|
$new.children.map(($newChild) => {
|
||||||
const key = 'props' in $newChild && $newChild.props.key;
|
const key = 'props' in $newChild && $newChild.props.key;
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
if (DEBUG && isRealElement($newChild) && (key === undefined || key === null)) {
|
if (DEBUG && isParentElement($newChild) && (key === undefined || key === null)) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('Missing `key` in `teactFastList`');
|
console.warn('Missing `key` in `teactFastList`');
|
||||||
}
|
}
|
||||||
@ -388,9 +471,9 @@ function renderFastListChildren($current: VirtualRealElement, $new: VirtualRealE
|
|||||||
|
|
||||||
newChildren.push(
|
newChildren.push(
|
||||||
renderWithVirtual(currentEl, currentChildInfo.$element, $newChild, $new, i, {
|
renderWithVirtual(currentEl, currentChildInfo.$element, $newChild, $new, i, {
|
||||||
forceIndex: true,
|
// `+ 1` is needed because before moving down the node still takes place above
|
||||||
moveDirection: shouldMoveNode ? (isMovingDown ? 'down' : 'up') : undefined,
|
nextSibling: shouldMoveNode ? currentEl.childNodes[isMovingDown ? i + 1 : i] : undefined,
|
||||||
})!,
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -403,27 +486,25 @@ function renderFastListChildren($current: VirtualRealElement, $new: VirtualRealE
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderFragment(
|
function renderFragment(
|
||||||
elements: VirtualElement[], fragmentIndex: number, parentEl: HTMLElement, $parent: VirtualRealElement,
|
elements: VirtualElement[], fragmentIndex: number, parentEl: HTMLElement, $parent: VirtualElementParent,
|
||||||
) {
|
) {
|
||||||
|
const nextSibling = parentEl.childNodes[fragmentIndex];
|
||||||
|
|
||||||
if (elements.length === 1) {
|
if (elements.length === 1) {
|
||||||
return [renderWithVirtual(parentEl, undefined, elements[0], $parent, fragmentIndex, { forceIndex: true })!];
|
return [renderWithVirtual(parentEl, undefined, elements[0], $parent, fragmentIndex, { nextSibling })];
|
||||||
}
|
}
|
||||||
|
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
const newChildren = elements.map(($element) => (
|
const newChildren = elements.map(($element, i) => (
|
||||||
renderWithVirtual(parentEl, undefined, $element, $parent, fragmentIndex, { fragment })!
|
renderWithVirtual(parentEl, undefined, $element, $parent, fragmentIndex + i, { fragment })
|
||||||
));
|
));
|
||||||
|
|
||||||
if (parentEl.childNodes[fragmentIndex]) {
|
insertBefore(parentEl, fragment, nextSibling);
|
||||||
parentEl.insertBefore(fragment, parentEl.childNodes[fragmentIndex]);
|
|
||||||
} else {
|
|
||||||
parentEl.appendChild(fragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newChildren;
|
return newChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAttributes($current: VirtualRealElement, $new: VirtualRealElement, element: HTMLElement) {
|
function updateAttributes($current: VirtualElementParent, $new: VirtualElementParent, element: HTMLElement) {
|
||||||
const currentEntries = Object.entries($current.props);
|
const currentEntries = Object.entries($current.props);
|
||||||
const newEntries = Object.entries($new.props);
|
const newEntries = Object.entries($new.props);
|
||||||
|
|
||||||
@ -492,11 +573,11 @@ function removeAttribute(element: HTMLElement, key: string, value: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
function DEBUG_addToVirtualTreeSize($current: VirtualRealElement | VirtualDomHead) {
|
function DEBUG_addToVirtualTreeSize($current: VirtualElementParent | VirtualDomHead) {
|
||||||
DEBUG_virtualTreeSize += $current.children.length;
|
DEBUG_virtualTreeSize += $current.children.length;
|
||||||
|
|
||||||
$current.children.forEach(($child) => {
|
$current.children.forEach(($child) => {
|
||||||
if (isRealElement($child)) {
|
if (isParentElement($child)) {
|
||||||
DEBUG_addToVirtualTreeSize($child);
|
DEBUG_addToVirtualTreeSize($child);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
import { orderBy } from '../../util/iteratees';
|
import { orderBy } from '../../util/iteratees';
|
||||||
import { getUnequalProps } from '../../util/arePropsShallowEqual';
|
import { getUnequalProps } from '../../util/arePropsShallowEqual';
|
||||||
import { handleError } from '../../util/handleError';
|
import { handleError } from '../../util/handleError';
|
||||||
import { removeAllDelegatedListeners } from './dom-events';
|
|
||||||
|
|
||||||
export type Props = AnyLiteral;
|
export type Props = AnyLiteral;
|
||||||
export type FC<P extends Props = any> = (props: P) => any;
|
export type FC<P extends Props = any> = (props: P) => any;
|
||||||
@ -98,13 +97,18 @@ export type VirtualElement =
|
|||||||
| VirtualElementText
|
| VirtualElementText
|
||||||
| VirtualElementTag
|
| VirtualElementTag
|
||||||
| VirtualElementComponent;
|
| VirtualElementComponent;
|
||||||
export type VirtualRealElement =
|
export type VirtualElementParent =
|
||||||
VirtualElementTag
|
VirtualElementTag
|
||||||
| VirtualElementComponent;
|
| VirtualElementComponent;
|
||||||
export type VirtualElementChildren = VirtualElement[];
|
export type VirtualElementChildren = VirtualElement[];
|
||||||
|
export type VirtualElementReal = Exclude<VirtualElement, VirtualElementComponent>;
|
||||||
|
|
||||||
// Compatibility with JSX types
|
// Compatibility with JSX types
|
||||||
export type TeactNode = ReactElement | string | number | boolean;
|
export type TeactNode =
|
||||||
|
ReactElement
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean;
|
||||||
|
|
||||||
const Fragment = Symbol('Fragment');
|
const Fragment = Symbol('Fragment');
|
||||||
|
|
||||||
@ -130,7 +134,7 @@ export function isComponentElement($element: VirtualElement): $element is Virtua
|
|||||||
return $element.type === VirtualElementTypesEnum.Component;
|
return $element.type === VirtualElementTypesEnum.Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRealElement($element: VirtualElement): $element is VirtualRealElement {
|
export function isParentElement($element: VirtualElement): $element is VirtualElementParent {
|
||||||
return isTagElement($element) || isComponentElement($element);
|
return isTagElement($element) || isComponentElement($element);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +142,7 @@ function createElement(
|
|||||||
source: string | FC | typeof Fragment,
|
source: string | FC | typeof Fragment,
|
||||||
props: Props,
|
props: Props,
|
||||||
...children: any[]
|
...children: any[]
|
||||||
): VirtualRealElement | VirtualElementChildren {
|
): VirtualElementParent | VirtualElementChildren {
|
||||||
if (!props) {
|
if (!props) {
|
||||||
props = {};
|
props = {};
|
||||||
}
|
}
|
||||||
@ -202,13 +206,13 @@ function buildComponentElement(
|
|||||||
componentInstance: ComponentInstance,
|
componentInstance: ComponentInstance,
|
||||||
children: VirtualElementChildren = [],
|
children: VirtualElementChildren = [],
|
||||||
): VirtualElementComponent {
|
): VirtualElementComponent {
|
||||||
const { props } = componentInstance;
|
const builtChildren = dropEmptyTail(children).map(buildChildElement);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
componentInstance,
|
|
||||||
type: VirtualElementTypesEnum.Component,
|
type: VirtualElementTypesEnum.Component,
|
||||||
props,
|
componentInstance,
|
||||||
children,
|
props: componentInstance.props,
|
||||||
|
children: builtChildren.length ? builtChildren : [buildEmptyElement()],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,7 +246,7 @@ function isEmptyPlaceholder(child: any) {
|
|||||||
function buildChildElement(child: any): VirtualElement {
|
function buildChildElement(child: any): VirtualElement {
|
||||||
if (isEmptyPlaceholder(child)) {
|
if (isEmptyPlaceholder(child)) {
|
||||||
return buildEmptyElement();
|
return buildEmptyElement();
|
||||||
} else if (isRealElement(child)) {
|
} else if (isParentElement(child)) {
|
||||||
return child;
|
return child;
|
||||||
} else {
|
} else {
|
||||||
return buildTextElement(child);
|
return buildTextElement(child);
|
||||||
@ -325,8 +329,8 @@ export function renderComponent(componentInstance: ComponentInstance) {
|
|||||||
|
|
||||||
componentInstance.renderedValue = newRenderedValue;
|
componentInstance.renderedValue = newRenderedValue;
|
||||||
|
|
||||||
const newChild = buildChildElement(newRenderedValue);
|
const children = Array.isArray(newRenderedValue) ? newRenderedValue : [newRenderedValue];
|
||||||
componentInstance.$element = buildComponentElement(componentInstance, [newChild]);
|
componentInstance.$element = buildComponentElement(componentInstance, children);
|
||||||
|
|
||||||
return componentInstance.$element;
|
return componentInstance.$element;
|
||||||
}
|
}
|
||||||
@ -351,39 +355,13 @@ export function hasElementChanged($old: VirtualElement, $new: VirtualElement) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unmountTree($element: VirtualElement) {
|
|
||||||
if (isComponentElement($element)) {
|
|
||||||
unmountComponent($element.componentInstance);
|
|
||||||
} else {
|
|
||||||
if (isTagElement($element)) {
|
|
||||||
if ($element.target) {
|
|
||||||
removeAllDelegatedListeners($element.target as HTMLElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($element.props.ref) {
|
|
||||||
$element.props.ref.current = undefined; // Help GC
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($element.target) {
|
|
||||||
$element.target = undefined; // Help GC
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isRealElement($element)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$element.children.forEach(unmountTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mountComponent(componentInstance: ComponentInstance) {
|
export function mountComponent(componentInstance: ComponentInstance) {
|
||||||
renderComponent(componentInstance);
|
renderComponent(componentInstance);
|
||||||
componentInstance.isMounted = true;
|
componentInstance.isMounted = true;
|
||||||
return componentInstance.$element;
|
return componentInstance.$element;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unmountComponent(componentInstance: ComponentInstance) {
|
export function unmountComponent(componentInstance: ComponentInstance) {
|
||||||
if (!componentInstance.isMounted) {
|
if (!componentInstance.isMounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -464,23 +442,6 @@ function forceUpdateComponent(componentInstance: ComponentInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTarget($element: VirtualElement): Node | undefined {
|
|
||||||
if (isComponentElement($element)) {
|
|
||||||
const componentElement = $element.children[0];
|
|
||||||
return componentElement ? getTarget(componentElement) : undefined;
|
|
||||||
} else {
|
|
||||||
return $element.target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setTarget($element: VirtualElement, target: Node) {
|
|
||||||
if (isComponentElement($element)) {
|
|
||||||
setTarget($element.children[0], target);
|
|
||||||
} else {
|
|
||||||
$element.target = target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useState<T>(initial?: T, debugKey?: string): [T, StateHookSetter<T>] {
|
export function useState<T>(initial?: T, debugKey?: string): [T, StateHookSetter<T>] {
|
||||||
const { cursor, byCursor } = renderingInstance.hooks.state;
|
const { cursor, byCursor } = renderingInstance.hooks.state;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user