ivi
Version:
Lightweight Embeddable Web UI Library.
1,753 lines (1,648 loc) • 55 kB
text/typescript
import {
type TemplateData,
TemplateFlags, ChildOpCode, PropOpCode, StateOpCode, CommonPropType,
} from "./template.js";
export const EMPTY_ARRAY: any[] = [];
/**
* Globally shared strings are automatically generated by template compiler.
*/
const __IVI_STRINGS__: string[] = ["IVI:fa7327d9-0034-492d-bfdf-576548b2d9cc"];
// Store global variables in a local scope as const variables so that JIT
// compiler could easily inline functions and eliminate checks in case global
// variables are overriden.
const _Object = Object;
const _Array = Array;
const _isArray: <T = any>(a: any) => a is T[] = _Array.isArray;
const _Map = Map;
const _Int32Array = Int32Array;
const _queueMicrotask = queueMicrotask;
const _requestAnimationFrame = requestAnimationFrame;
const _requestIdleCallback = requestIdleCallback;
const nodeProto = Node.prototype;
const elementProto = Element.prototype;
const doc = document;
// Template containers are used to create static templates from HTML strings
// via `innerHTML`.
const HTM_TEMPLATE = /**@__PURE__*/doc.createElement("template");
const HTM_TEMPLATE_CONTENT = HTM_TEMPLATE.content;
const _SVG_TEMPLATE = /**@__PURE__*/doc.createElement("template");
const SVG_TEMPLATE = /**@__PURE__*/doc.createElementNS("http://www.w3.org/2000/svg", "svg");
_SVG_TEMPLATE.content.appendChild(SVG_TEMPLATE);
const SVG_TEMPLATE_CONTENT = _SVG_TEMPLATE.content.firstChild as Element;
// Store Node/Element methods to avoid going through a long prototype chain and
// avoid megamorphic call-sites when accessing DOM nodes.
/** `Node.prototype.insertBefore` */
const nodeInsertBefore: <T extends Node>(this: Node, node: T, child: Node | null) => T = nodeProto.insertBefore;
/** `Node.prototype.removeChild`. */
const nodeRemoveChild: <T extends Node>(this: Node, node: T) => T = nodeProto.removeChild;
/** `Node.prototype.cloneNode`. */
const nodeCloneNode: (this: Node, deep?: boolean | undefined) => Node = nodeProto.cloneNode;
declare global {
interface Element {
moveBefore<T extends Element | CharacterData>(node: T, child: Node | null): void;
}
}
/** `Element.prototype.moveBefore` */
const elementMoveBefore: <T extends Element | CharacterData>(this: Element, node: T, child: Node | null) => void = elementProto.moveBefore ?? nodeInsertBefore;
/** `Element.prototype.setAttribute` */
const elementSetAttribute: (this: Element, qualifiedName: string, value: string) => void = elementProto.setAttribute;
/** `Element.prototype.removeAttribute` */
const elementRemoveAttribute: (this: Element, qualifiedName: string) => void = elementProto.removeAttribute;
/** `EventTarget.prototype.addEventListener` */
const elementAddEventListener = elementProto.addEventListener;
/** `EventTarget.prototype.removeEventListener` */
const elementRemoveEventListener = elementProto.removeEventListener;
/** `Object.getOwnPropertyDescriptor(o, p)` */
const getDescriptor = (o: any, p: string | number | symbol) => _Object.getOwnPropertyDescriptor(o, p);
/** `get Node.prototype.firstChild` */
const nodeGetFirstChild: (this: Node) => ChildNode | null = /*@__PURE__*/getDescriptor(nodeProto, "firstChild")!.get!;
/** `get Node.prototype.nextSibling` */
const nodeGetNextSibling: (this: Node) => ChildNode | null = /*@__PURE__*/getDescriptor(nodeProto, "nextSibling")!.get!;
/** `set Node.prototype.textContent` */
const nodeSetTextContent: (this: Node, value: string | number) => void = /*@__PURE__*/getDescriptor(nodeProto, "textContent")!.set!;
/** `set Element.prototype.innerHTML` */
const elementSetInnerHTML: (this: Element, value: string) => void = /*@__PURE__*/getDescriptor(elementProto, "innerHTML")!.set!;
/** `set Element.prototype.className` */
const elementSetClassName: (this: Element, value: string) => void = /*@__PURE__*/getDescriptor(elementProto, "className")!.set!;
/** `get HTMLElement.prototype.style`. */
const htmlElementGetStyle: (this: HTMLElement) => CSSStyleDeclaration = /*@__PURE__*/getDescriptor(HTMLElement.prototype, "style")!.get!;
/** `get SVGElement.prototype.style` */
const svgElementGetStyle: (this: SVGElement) => CSSStyleDeclaration = /*@__PURE__*/getDescriptor(SVGElement.prototype, "style")!.get!;
/**
* Render Context.
*/
export interface RenderContext {
/** Parent DOM Element */
p: Element;
/** Next DOM Node. */
n: Node | null;
/** Template state index. */
si: number;
/** DOM Side Effects */
e: Array<() => void>;
}
// When object is sealed and stored in a const variable, JIT compiler can
// eliminate object map(shape) checks when accessing its properties.
/**
* Global Render Context.
*/
export const RENDER_CONTEXT: RenderContext = _Object.seal({
p: null!,
n: null,
si: 0,
e: [],
});
// Types are stored as bit flags so that we could perform multiple tests with a
// single bitwise operation. E.g. `flags & (List | Array)`.
/**
* Flags.
*/
export const enum Flags {
// VNode and SNode flags
Template = 1,
Component = 1 << 1,
List = 1 << 2,
Array = 1 << 3,
Text = 1 << 4,
Root = 1 << 5,
Context = 1 << 6,
TypeMask = (1 << 7) - 1,
// Component Dirty Flags
Dirty = 1 << 7,
DirtySubtree = 1 << 8,
// Update Flags
ForceUpdate = 1 << 9,
DisplaceNode = 1 << 10,
}
/**
* Stateful Node.
*/
export type SAny =
| null // Hole
| SRoot // Root
| SText // Text
| STemplate // Template
| SList // Dynamic List
| SComponent // Component
| SContext // Context
;
/**
* Stateful Node.
*/
export type SNode<V = VAny> = SNode1<V> | SNode2<V>;
/**
* Stateful Node with 1 state slot.
*
* @typeparam S1 State slot #1.
*/
export interface SNode1<V = VAny, S1 = any> {
/** Stateless Node. */
v: V;
/** See {@link Flags} for details. */
f: Flags;
/** Children Stateful Nodes. */
c: SNode | (SNode | null)[] | null;
/** Parent Stateful Node. */
p: SNode | null,
/** State slot #1. */
s1: S1;
}
/**
* Stateful Node with 2 state slots.
*
* @typeparam S1 State slot #1.
* @typeparam S2 State slot #2.
*/
export interface SNode2<V = VAny, S1 = any, S2 = any> extends SNode1<V, S1> {
/** State slot #2. */
s2: S2;
}
/** Stateful Root Node. */
export type SRoot<S = any> = SNode1<VRoot, S>;
/** Stateful Root Node. */
export type Root<S = any> = SRoot<S>;
/** Stateful Text Node. */
export type SText = SNode1<string | number, Text>;
/** Stateful Template Node. */
export type STemplate = SNode1<VTemplate, Node[]>;
/** Stateful List Node. */
export type SList = SNode1<VList, null>;
/** Stateful Component Node. */
export type SComponent<P = any> = SNode2<
VComponent,
/** Render function. */
null | ComponentRenderFn<P>,
/** Unmount hooks. */
null | (() => void) | (() => void)[]
>;
/** Stateful Component Node. */
export type Component<P = any> = SComponent<P>;
export type ComponentRenderFn<P = any> = (props: P) => VAny;
export type SContext = SNode1<VContext, null>;
/**
* Creates a Stateful Node instance.
*
* @param v VNode.
* @returns {@link SNode} instance.
*/
export const createSNode = <V extends VAny, S>(
f: Flags,
v: V,
c: SNode | Array<SNode1 | null> | null,
p: SNode | null,
s1: S,
): SNode1<V, S> => ({ f, v, c, p, s1 });
/**
* Stateless Tree Node.
*/
export type VAny =
| null // Hole
| undefined // Hole
| false // Hole
| string // Text
| number // Text
| VRoot // Root
| VTemplate // Template
| VComponent // Component
| VContext // Context
| VList // Dynamic List with track by key algo
| VAny[] // Dynamic List with track by index algo
;
/**
* Stateless Node Descriptor.
*/
export interface VDescriptor<P1 = any, P2 = any> {
/** See {@link Flags} for details. */
readonly f: Flags;
/** First property. */
readonly p1: P1;
/** Second property. */
readonly p2: P2;
}
/** Root Invalidate Hook. */
export type OnRootInvalidated<S> = (root: SRoot<S>, state: S) => void;
/** Root Descriptor. */
export type RootDescriptor<S = any> = VDescriptor<OnRootInvalidated<S>, null>;
/** Template Descriptor */
export type TemplateDescriptor = VDescriptor<TemplateData, () => Element>;
/** Component Descriptor */
export type ComponentDescriptor<P = any> = VDescriptor<
// Component factory function.
ComponentFactoryFn<P>,
// `areEqual()` function.
undefined | ((prev: P, next: P) => boolean)
>;
export type ComponentFactoryFn<P = any> = (component: Component) => (props: P) => VAny;
/** Context Descriptor */
export type ContextDescriptor = VDescriptor<null, null>;
/** List Descriptor */
export type ListDescriptor = VDescriptor<null, null>;
/**
* Stateless Node.
*
* @typeparam D Descriptor.
* @typeparam P Property.
*/
export interface VNode<D extends VDescriptor<any, any> = VDescriptor<any, any>, P = any> {
/** Descriptor. */
readonly d: D;
/** Property. */
readonly p: P;
}
/** Stateless Root Node. */
export type VRoot = VNode<RootDescriptor, RootProps>;
/** Stateless Template Node. */
export type VTemplate<P = any> = VNode<TemplateDescriptor, P>;
/** Stateless Component Node. */
export type VComponent<P = any> = VNode<ComponentDescriptor, P>;
/** Stateless Context Node. */
export type VContext<T = any> = VNode<ContextDescriptor, ContextProps<T>>;
/** Stateless List Node. */
export type VList<K = any> = VNode<ListDescriptor, ListProps<K>>;
/**
* Stateless Root Node Props.
*
* Contains a DOM position where root children should mounted.
*/
export interface RootProps {
/** Parent Element */
p: Element,
/** Next Node */
n: Node | null,
}
/**
* Stateless Context Node Props.
*/
export interface ContextProps<T = any> {
/** Context Value. */
v: T;
/** Stateless Child Node. */
c: VAny;
}
/**
* Stateless List Node Props.
*
* Contains unique keys for stateless nodes and stateless nodes.
*/
export interface ListProps<K = any> {
/** Unique Keys. */
k: K[],
/** Stateless Nodes. */
v: VAny[],
}
/**
* Element Directive.
*/
export type ElementDirective = <E extends Element>(element: E) => void;
export const _flushDOMEffects = () => {
const e = RENDER_CONTEXT.e;
if (e.length > 0) {
RENDER_CONTEXT.e = [];
for (let i = 0; i < e.length; i++) {
e[i]();
}
}
};
const _updateTemplateProperties = (
currentElement: Element,
opCodes: PropOpCode[],
data: string[],
state: Node[],
prevProps: any[] | null,
nextProps: any[],
svg: boolean,
) => {
let style: CSSStyleDeclaration | undefined;
for (let i = 0; i < opCodes.length; i++) {
const op = opCodes[i];
const type = op & PropOpCode.TypeMask;
const dataIndex = op >> PropOpCode.DataShift;
if (type === PropOpCode.SetNode) {
currentElement = state[dataIndex] as Element;
style = void 0;
} else {
const propsIndex = (op >> PropOpCode.InputShift) & PropOpCode.Mask6;
const next = nextProps[propsIndex];
if (type === PropOpCode.DiffDOMProperty) {
const key = data[dataIndex];
if (prevProps === null) {
if (next !== void 0) {
(currentElement as Record<string, any>)[key] = next;
}
} else if ((currentElement as Record<string, any>)[key] !== next) {
(currentElement as Record<string, any>)[key] = next;
}
} else {
let prev;
if (prevProps !== null) {
prev = prevProps[propsIndex];
}
if (prev !== next) {
if (type === PropOpCode.Common) {
if (dataIndex === CommonPropType.ClassName) {
if (next !== "" && next != null && next !== false) {
elementSetClassName.call(currentElement, next);
} else if (prev !== "" && prev != null && prev !== false) {
elementSetClassName.call(currentElement, "");
}
} else if (dataIndex === CommonPropType.TextContent) {
if (next !== "" && next != null && next !== false) {
if (prev == null || prev === "" || prev === false) {
nodeSetTextContent.call(currentElement, next);
} else {
nodeGetFirstChild.call(currentElement)!.nodeValue = next;
}
} else if (prev != null && prev !== "" && prev !== false) {
nodeSetTextContent.call(currentElement, "");
}
} else { // CommonPropType.InnerHTML
if (next !== "" && next != null && next !== false) {
elementSetInnerHTML.call(currentElement, next);
} else if (prev !== "" && prev != null && prev !== false) {
nodeSetTextContent.call(currentElement, "");
}
}
} else if (type === PropOpCode.Directive) {
(next as ElementDirective)(currentElement);
} else {
const key = data[dataIndex];
if (type === PropOpCode.Attribute) {
if (next !== false && next != null) {
elementSetAttribute.call(currentElement, key, next as string);
} else if (prev !== false && prev != null) {
elementRemoveAttribute.call(currentElement, key);
}
} else if (type === PropOpCode.Property) {
(currentElement as Record<string, any>)[key] = next;
} else if (type === PropOpCode.Style) {
if (next !== false && next != null) {
if (style === void 0) {
style = (svg === false)
? htmlElementGetStyle.call(currentElement as HTMLElement)
: svgElementGetStyle.call(currentElement as SVGElement);
}
style!.setProperty(key, next as string);
} else if (prev !== false && prev != null) {
if (style === void 0) {
style = (svg === false)
? htmlElementGetStyle.call(currentElement as HTMLElement)
: svgElementGetStyle.call(currentElement as SVGElement);
}
style!.removeProperty(key);
}
} else { // PropOpCode.Event
if (prev != null && prev !== false) {
elementRemoveEventListener.call(currentElement, key, prev);
}
if (next != null && next !== false) {
elementAddEventListener.call(currentElement, key, next);
}
}
}
}
}
}
}
};
const _assignTemplateSlots = (
currentNode: Node,
opCodes: StateOpCode[],
offset: number,
endOffset: number,
state: Node[],
) => {
const ctx = RENDER_CONTEXT;
while (true) {
const op = opCodes[offset++];
if (op & StateOpCode.Save) {
state[++ctx.si] = currentNode;
}
if (op & StateOpCode.EnterOrRemove) {
const enterOffset = op >> StateOpCode.OffsetShift;
// Enter offset is used to disambiguate between enter and remove
// operations. Remove operations will always have a 0 enterOffset.
if (enterOffset) { // Enter
_assignTemplateSlots(
nodeGetFirstChild.call(currentNode)!,
opCodes,
offset,
offset += enterOffset,
state,
);
} else { // Remove
// Remove operation implies that current node is always a comment node
// followed by a text node.
const commentNode = currentNode as Comment;
state[++ctx.si] = currentNode = nodeGetNextSibling.call(currentNode)!;
commentNode.remove();
}
}
if (offset === endOffset) {
return;
}
currentNode = nodeGetNextSibling.call(currentNode as ChildNode)!;
}
};
const _mountList = (
parentState: SNode1,
flags: Flags,
children: VAny[],
vNode: VAny,
): SNode1 => {
let i = children.length;
const sChildren = _Array(i);
const sNode = createSNode(flags, vNode, sChildren, parentState, null);
while (i > 0) {
sChildren[--i] = _mount(sNode, children[i]);
}
return sNode;
};
const _updateArray = (
parentSNode: SNode1,
sNode: SNode1,
next: VAny,
updateFlags: Flags,
): SNode1 | null => {
if (!_isArray(next)) {
_unmount(sNode, true);
return _mount(parentSNode, next);
}
const prevSChildren = sNode.c as (SNode1 | null)[];
let nextSChildren = prevSChildren;
let prevLength = prevSChildren.length;
let nextLength = next.length;
if (nextLength !== prevLength) {
sNode.c = nextSChildren = _Array(nextLength);
while (prevLength > nextLength) {
const sChild = prevSChildren[--prevLength];
if (sChild !== null) {
_unmount(sChild, true);
}
}
while (nextLength > prevLength) {
nextSChildren[--nextLength] = _mount(sNode, next[nextLength]);
}
}
while (nextLength > 0) {
nextSChildren[--nextLength] = _update(
sNode,
prevSChildren[nextLength],
next[nextLength],
updateFlags,
);
}
return sNode;
};
/**
* Updates a Stateful Node with a new Stateless Node.
*
* @param parentSNode Parent Stateul Node.
* @param sNode Stateful Node to update.
* @param next New Stateless Node.
* @param updateFlags Update flags (ForceUpdate and DisplaceNode).
* @returns Stateful Node.
*/
const _update = (
parentSNode: SNode,
sNode: SNode | null,
next: VAny,
updateFlags: number,
): SNode | null => {
if (sNode === null) {
return _mount(parentSNode, next);
}
if (next === false || next == null || next === "") {
_unmount(sNode, true);
return null;
}
// polymorphic call-site
const children = sNode.c;
const prev = sNode.v;
const state = sNode.s1;
const flags = sNode.f;
const type = flags & Flags.TypeMask;
sNode.f = type;
// Reassign to reduce memory consumption even if next value is strictly
// equal to the prev value.
sNode.v = next;
// Text and Array should be checked before Component, Template and List
// because their stateless nodes are represented with basic string and array
// types.
if (type === Flags.Text) {
const ctx = RENDER_CONTEXT;
if (typeof next !== "object") {
if (prev !== next) {
(state as Text).nodeValue = next as string;
}
if (updateFlags & Flags.DisplaceNode) {
elementMoveBefore!.call(
ctx.p,
(state as Text),
ctx.n,
);
}
ctx.n = state;
return sNode;
}
nodeRemoveChild!.call(ctx.p, (state as Text));
return _mount(parentSNode, next)!;
}
if (prev === next) {
_dirtyCheck(sNode, updateFlags);
return sNode;
}
// Dirty flags should be cleared after dirty checking.
sNode.f = type;
if (type === Flags.Array) {
return _updateArray(parentSNode, sNode, next, updateFlags);
}
const descriptor = (next as VNode).d;
const nextProps = (next as VNode).p;
const prevProps = (prev as VNode).p;
if ((prev as VNode).d !== descriptor) {
_unmount(sNode, true);
return _mount(parentSNode, next);
}
if (type === Flags.Component) {
if (
((flags | updateFlags) & (Flags.Dirty | Flags.ForceUpdate)) ||
(descriptor.p2 === void 0) ||
(descriptor.p2(prevProps, nextProps) !== true)
) {
sNode.c = _update(
sNode,
children as SNode,
(state as ComponentRenderFn)(nextProps),
updateFlags,
);
} else if (children !== null) {
_dirtyCheck(children as SNode, updateFlags);
}
} else if (type === Flags.Template) {
const ctx = RENDER_CONTEXT;
const parentElement = ctx.p;
const tplData = (descriptor as TemplateDescriptor).p1;
const flags = tplData.f;
const data = tplData.d;
const propsOpCodes = tplData.p;
const childOpCodes = tplData.c;
const rootDOMNode = state[0] as Element;
if (updateFlags & Flags.DisplaceNode) {
updateFlags ^= Flags.DisplaceNode;
elementMoveBefore!.call(parentElement, rootDOMNode, ctx.n);
}
_updateTemplateProperties(
rootDOMNode,
propsOpCodes,
data,
state as Node[],
prevProps,
nextProps,
!!(flags & TemplateFlags.Svg),
);
if (children !== null) {
ctx.p = rootDOMNode;
ctx.n = null;
let childrenIndex = 0;
for (let i = 0; i < childOpCodes.length; i++) {
const childOpCode = childOpCodes[i];
const type = childOpCode & ChildOpCode.Type;
const value = childOpCode >> ChildOpCode.ValueShift;
if (type === ChildOpCode.Child) {
(children as (SNode | null)[])[childrenIndex] =
_update(
sNode,
(children as (SNode | null)[])[childrenIndex++],
nextProps[value],
updateFlags,
);
} else if (type === ChildOpCode.SetNext) {
ctx.n = state[value];
} else { // ChildOpCode.SetParent
ctx.p = state[value] as Element;
ctx.n = null;
}
}
ctx.p = parentElement;
}
ctx.n = rootDOMNode;
} else if (type === Flags.List) {
_updateList(
sNode as SList,
prevProps,
nextProps,
updateFlags,
);
} else { // Context
if (prevProps.v !== nextProps.v) {
updateFlags |= Flags.ForceUpdate;
}
sNode.c = _update(
sNode,
children as SNode | null,
nextProps.c,
updateFlags,
);
}
return sNode;
};
/**
* Mounts Stateless Node.
*
* @param parentSNode Parent Stateful Node.
* @param v Stateless Node.
* @returns Mounted Stateful Node.
*/
const _mount = (parentSNode: SNode, v: VAny): SNode | null => {
if (v !== false && v != null) {
if (typeof v === "object") {
if (_isArray(v)) {
return _mountList(parentSNode, Flags.Array, v, v);
} else {
const descriptor = v.d;
const props = v.p;
const descriptorP1 = descriptor.p1;
const type = descriptor.f & (Flags.Template | Flags.Component | Flags.List);
if (type === Flags.Template) {
const ctx = RENDER_CONTEXT;
const parentDOMElement = ctx.p;
const nextDOMNode = ctx.n;
const tplData = descriptorP1 as TemplateData;
const data = tplData.d;
const propsOpCodes = tplData.p;
const stateOpCodes = tplData.s;
const childOpCodes = tplData.c;
const flags = tplData.f;
const rootDOMNode = (descriptor as TemplateDescriptor).p2();
const state = _Array<Node>(flags & TemplateFlags.Mask6);
state[0] = rootDOMNode;
if (stateOpCodes.length > 0) {
ctx.si = 0;
_assignTemplateSlots(
nodeGetFirstChild.call(rootDOMNode)!,
stateOpCodes,
0,
stateOpCodes.length,
state,
);
}
_updateTemplateProperties(
rootDOMNode,
propsOpCodes,
data,
state,
null,
props,
!!(flags & TemplateFlags.Svg),
);
const sNode = createSNode(
Flags.Template,
v,
null,
parentSNode,
state,
);
if (childOpCodes.length > 0) {
const children = _Array<SNode | null>(
(flags >> TemplateFlags.ChildrenSizeShift) & TemplateFlags.Mask6
);
sNode.c = children;
ctx.p = rootDOMNode;
ctx.n = null;
let childrenIndex = 0;
for (let i = 0; i < childOpCodes.length; i++) {
const childOpCode = childOpCodes[i];
const type = childOpCode & ChildOpCode.Type;
const value = childOpCode >> ChildOpCode.ValueShift;
if (type === ChildOpCode.Child) {
children[childrenIndex++] = _mount(sNode, props[value]);
} else if (type === ChildOpCode.SetNext) {
ctx.n = state[value];
} else { // ChildOpCode.SetParent
ctx.p = state[value] as Element;
ctx.n = null;
}
}
ctx.p = parentDOMElement;
}
ctx.n = rootDOMNode;
nodeInsertBefore!.call(parentDOMElement, rootDOMNode, nextDOMNode);
return sNode;
} else if (type === Flags.Component) {
const sNode: Component = {
f: Flags.Component,
v: v as VComponent,
c: null,
p: parentSNode,
s1: null!,
s2: null,
};
const renderFn = (descriptorP1 as ComponentFactoryFn)(sNode);
sNode.c = _mount(sNode, renderFn(props));
sNode.s1 = renderFn;
return sNode;
} else if (type === Flags.List) {
return _mountList(parentSNode, Flags.List, (props as ListProps).v, v);
}
// Context
const sNode = createSNode(Flags.Context, v, null, parentSNode, null);
sNode.c = _mount(sNode, (props as ContextProps).c);
return sNode;
}
} else if (v !== "") { // text
const ctx = RENDER_CONTEXT;
const next = ctx.n;
const e = doc.createTextNode(v as string);
ctx.n = e;
nodeInsertBefore.call(ctx.p, e, next);
return createSNode(Flags.Text, v, null, parentSNode, e);
}
}
return null;
};
/**
* Performs a Dirty Checking in a Stateful Node Subtree.
*
* @param sNode Stateful Node.
* @param updateFlags Update flags (ForceUpdate and DisplaceNode).
*/
const _dirtyCheck = (sNode: SNode, updateFlags: number): void => {
const ctx = RENDER_CONTEXT;
// polymorphic call-site
const state = sNode.s1;
const v = sNode.v;
const children = sNode.c;
const flags = sNode.f;
const type = flags & Flags.TypeMask;
sNode.f = type;
if (type === Flags.Template) {
const rootDOMNode = (state as Node[])[0] as Element;
if (updateFlags & Flags.DisplaceNode) {
updateFlags ^= Flags.DisplaceNode;
elementMoveBefore.call(ctx.p, rootDOMNode, ctx.n);
}
if (flags & Flags.DirtySubtree) {
ctx.p = rootDOMNode;
ctx.n = null;
const parentDOMElement = ctx.p;
const childOpCodes = (v as VTemplate).d.p1.c;
let childrenIndex = 0;
for (let i = 0; i < childOpCodes.length; i++) {
const op = childOpCodes[i];
const type = op & ChildOpCode.Type;
const value = op >> ChildOpCode.ValueShift;
if (type === ChildOpCode.Child) {
const sChild = (children as (SNode1 | null)[])[childrenIndex++];
if (sChild !== null) {
_dirtyCheck(sChild, updateFlags);
}
} else if (type === ChildOpCode.SetNext) {
ctx.n = (state as Node[])[value];
} else { // ChildOpCode.SetParent
ctx.p = state[value] as Element;
ctx.n = null;
}
}
ctx.p = parentDOMElement;
}
ctx.n = rootDOMNode;
} else if (type === Flags.Text) {
if (updateFlags & Flags.DisplaceNode) {
elementMoveBefore.call(ctx.p, state as Text, ctx.n);
}
ctx.n = state as Text;
} else if (type === Flags.Component) {
if ((flags | updateFlags) & (Flags.Dirty | Flags.ForceUpdate)) {
sNode.c = _update(
sNode,
children as SNode,
(state as ComponentRenderFn)!((v as VComponent).p),
updateFlags,
);
} else if (children !== null) {
_dirtyCheck(children as SNode, updateFlags);
}
} else if (type === Flags.Context) {
if (children !== null) {
_dirtyCheck(children as SNode, updateFlags);
}
} else { // Array || List
let i = (children as Array<SNode | null>).length;
while (--i >= 0) {
const sChild = (children as Array<SNode | null>)[i];
if (sChild !== null) {
_dirtyCheck(sChild, updateFlags);
}
}
}
};
/**
* Unmounts Stateful Node.
*
* @param sNode Stateful Node.
* @param detach Detach root DOM nodes from the DOM.
*/
const _unmount = (sNode: SNode, detach: boolean): void => {
const flags = sNode.f; // polymorphic call-site
const sChildren = sNode.c;
if (detach === true && (flags & (Flags.Template | Flags.Text))) {
detach = false;
nodeRemoveChild.call(
RENDER_CONTEXT.p,
(flags & Flags.Template)
? (sNode as STemplate).s1[0]
: (sNode as SText).s1
);
}
if (flags & Flags.Component) {
const unmountHooks = (sNode as SComponent).s2;
if (unmountHooks !== null) {
if (typeof unmountHooks === "function") {
unmountHooks();
} else {
for (let i = 0; i < unmountHooks.length; i++) {
unmountHooks[i]();
}
}
}
}
if (sChildren !== null) {
if (_isArray(sChildren)) {
for (let i = 0; i < sChildren.length; i++) {
const sChild = sChildren[i];
if (sChild !== null) {
_unmount(sChild, detach);
}
}
} else {
_unmount(sChildren as SNode, detach);
}
}
};
const enum MagicValues {
/**
* One of the children nodes were moved.
*/
RearrangeNodes = 1073741823, // Max SMI Value
/**
* New node marker.
*/
NewNodeMark = -1,
/**
* LIS marker.
*/
LISMark = -2,
}
/**
* Update children list with track by key algorithm.
*
* High-level overview of the algorithm that is implemented in this function:
*
* This algorithm finds a minimum number of DOM operations. It works in
* several steps:
*
* 1. Common prefix and suffix optimization.
*
* Look for nodes with identical keys by simultaneously iterating through nodes
* in the old children list `A` and new children list `B` from both sides.
*
* A: -> [a b c d] <-
* B: -> [a b d] <-
*
* Skip nodes "a" and "b" at the start, and node "d" at the end.
*
* A: -> [c] <-
* B: -> [] <-
*
* 2. Zero length optimizations.
*
* Check if the size of one of the list is equal to zero. When length of the
* old children list is zero, insert remaining nodes from the new list. When
* length of the new children list is zero, remove remaining nodes from the old
* list.
*
* A: -> [a b c g] <-
* B: -> [a g] <-
*
* Skip nodes "a" and "g" (prefix and suffix optimization).
*
* A: [b c]
* B: []
*
* Remove nodes "b" and "c".
*
* 3. Index and unmount removed nodes.
*
* A: [b c d e f]
* B: [c b h f e]
* P: [. . . . .] // . == -1
*
* Create array `P` (`sources`) with the length of the new children list and
* fills it with `NewNodeMark` values. This mark indicates that node at this
* position should be mounted. Later we will assign node positions in the old
* children list to this array.
*
* A: [b c d e f]
* B: [c b h f e]
* P: [. . . . .] // . == -1
* I: {
* c: 0, // B[0] == c
* b: 1, // B[1] == b
* h: 2,
* f: 3,
* e: 4,
* }
* last = 0
*
* Create reverse index `I` that maps keys to node positions in the new
* children list.
*
* A: [b c d e f]
* ^
* B: [c b h f e]
* P: [. 0 . . .] // . == -1
* I: {
* c: 0,
* b: 1, <-
* h: 2,
* f: 3,
* e: 4,
* }
* last = 1
*
* Assign original positions of the nodes from the old children list to the
* array `P`.
*
* Iterate through nodes in the old children list and gets their new positions
* from the index `I`. Assign old node position to the array `P`. When index
* `I` doesn't have a key for the old node, it means that it should be
* unmounted.
*
* When we assigning positions to the array `P`, we also store position of the
* last seen node in the new children list `pos`, if the last seen position is
* greater than the current position of the node at the new list, then we are
* switching `rearrangeNodes` flag to `true` (`pos === RearrangeNodes`).
*
* A: [b c d e f]
* ^
* B: [c b h f e]
* P: [1 0 . . .] // . == -1
* I: {
* c: 0, <-
* b: 1,
* h: 2,
* f: 3,
* e: 4,
* }
* last = 1 // last > 0; rearrangeNodes = true
*
* The last position `1` is greater than the current position of the node at the
* new list `0`, switch `rearrangeNodes` flag to `true`.
*
* A: [b c d e f]
* ^
* B: [c b h f e]
* P: [1 0 . . .] // . == -1
* I: {
* c: 0,
* b: 1,
* h: 2,
* f: 3,
* e: 4,
* }
* rearrangeNodes = true
*
* Node with key "d" doesn't exist in the index `I`, unmounts node `d`.
*
* A: [b c d e f]
* ^
* B: [c b h f e]
* P: [1 0 . . 3] // . == -1
* I: {
* c: 0,
* b: 1,
* h: 2,
* f: 3,
* e: 4, <-
* }
* rearrangeNodes = true
*
* Assign position `3` for `e` node.
*
* A: [b c d e f]
* ^
* B: [c b h f e]
* P: [1 0 . 4 3] // . == -1
* I: {
* c: 0,
* b: 1,
* h: 2,
* f: 3, <-
* e: 4,
* }
* rearrangeNodes = true
*
* Assign position `4` for 'f' node.
*
* 4. Find minimum number of moves when `rearrangeNodes` flag is on and mount
* new nodes.
*
* A: [b c d e f]
* B: [c b h f e]
* P: [1 * . 4 *] // . == -1 * == -2
*
* When `rearrangeNodes` is on, mark all nodes in the array `P` that belong to
* the [longest increasing subsequence](http://en.wikipedia.org/wiki/Longest_increasing_subsequence)
* and move all nodes that doesn't belong to this subsequence.
*
* Iterate over the new children list and the `P` array simultaneously. When
* value from `P` array is equal to `NewNodeMark`, mount a new node. When it
* isn't equal to `LisMark`, move it to a new position.
*
* A: [b c d e f]
* B: [c b h f e]
* ^ // new_pos == 4
* P: [1 * . 4 *] // . == NewNodeMark * == LisMark
* ^
*
* Node "e" has `LisMark` value in the array `P`, nothing changes.
*
* A: [b c d e f]
* B: [c b h f e]
* ^ // new_pos == 3
* P: [1 * . 4 *] // . == NewNodeMark * == LisMark
* ^
*
* Node "f" has `4` value in the array `P`, move it before the next node "e".
*
* A: [b c d e f]
* B: [c b h f e]
* ^ // new_pos == 2
* P: [1 * . 4 *] // . == NewNodeMark * == LisMark
* ^
*
* Node "h" has `NewNodeMark` value in the array `P`, mount new node "h".
*
* A: [b c d e f]
* B: [c b h f e]
* ^ // new_pos == 1
* P: [1 * . 4 *] // . == NewNodeMark * == LisMark
* ^
*
* Node "b" has `LisMark` value in the array `P`, nothing changes.
*
* A: [b c d e f]
* B: [c b h f e]
* ^ // new_pos == 0
* P: [1 * . 4 *] // . == NewNodeMark * == LisMark
*
* Node "c" has `1` value in the array `P`, move it before the next node "b".
*
* When `rearrangeNodes` flag is off, skip LIS algorithm and mount nodes that
* have `NewNodeMark` value in the array `P`.
*
* NOTE: There are many variations of this algorithm that are used by many UI
* libraries and many implementations are still using an old optimization
* technique that were removed several years ago from this implementation. This
* optimization were used to improve performance of simple moves/swaps. E.g.
*
* A: -> [a b c] <-
* B: -> [c b a] <-
*
* Move "a" and "c" nodes to the other edge.
*
* A: -> [b] <-
* B: -> [b] <-
*
* Skip node "b".
*
* This optimization were removed because it breaks invariant that insert and
* remove operations shouldn't trigger a move operation. E.g.
*
* A: -> [a b]
* B: [c a] <-
*
* Move node "a" to the end.
*
* A: [b]
* B: [c a]
*
* Remove node "b" and insert node "c".
*
* In this use case, this optimization performs one unnecessary operation.
* Instead of removing node "b" and inserting node "c", it also moves node "a".
*
* @param sNode {@link SList} node.
* @param a Previous {@link ListProps}.
* @param b Next {@link ListProps}.
* @param updateFlags Update flags.
* @noinline
* @__NOINLINE__
*/
const _updateList = (
sNode: SList,
a: ListProps<any>,
b: ListProps<any>,
updateFlags: Flags,
): void => {
const aKeys = a.k;
const bKeys = b.k;
const bVNodes = b.v;
let bLength = bKeys.length;
let aLength = aKeys.length;
const result = _Array(bLength);
if (bLength === 0) { // New children list is empty.
if (aLength > 0) { // Unmount nodes from the old children list.
_unmount(sNode, true);
}
} else if (aLength === 0) { // Old children list is empty.
while (bLength > 0) { // Mount nodes from the new children list.
result[--bLength] = _mount(sNode, bVNodes[bLength]);
}
} else {
const sChildren = sNode.c as Array<SNode | null>;
let aEnd = aLength - 1;
let bEnd = bLength - 1;
let start = 0;
// Step 1
outer: while (true) {
// Update nodes with the same key at the end.
while (aKeys[aEnd] === bKeys[bEnd]) {
result[bEnd] = _update(
sNode,
sChildren[aEnd--],
bVNodes[bEnd],
updateFlags,
);
if (start > --bEnd || start > aEnd) {
break outer;
}
}
// Update nodes with the same key at the beginning.
while (aKeys[start] === bKeys[start] && ++start <= aEnd && start <= bEnd) {
// delayed update (all updates should be performed from right-to-left).
}
break;
}
// Step 2
if (start > aEnd) {
// All nodes from `a` are updated, insert the rest from `b`.
while (bEnd >= start) {
result[bEnd] = _mount(sNode, bVNodes[bEnd--]);
}
} else if (start > bEnd) {
// All nodes from `b` are updated, remove the rest from `a`.
bLength = start;
do {
const sChild = sChildren[bLength++];
if (sChild !== null) {
_unmount(sChild, true);
}
} while (bLength <= aEnd);
} else { // Step 3
let bLength = bEnd - start + 1;
const sources = new _Int32Array(bLength); // Maps positions in the new children list to positions in the old list.
const keyIndex = new _Map<any, number>(); // Maps keys to their positions in the new children list.
for (let i = 0; i < bLength; i++) {
// `NewNodeMark` value indicates that node doesn't exist in the old children list.
sources[i] = MagicValues.NewNodeMark;
const j = start + i;
keyIndex.set(bKeys[j], j);
}
// When `nodePosition === RearrangeNodes`, it means that one of the nodes is in the wrong position and we should
// rearrange nodes with LIS-based algorithm `markLIS()`.
let nodePosition = 0;
for (let i = start; i <= aEnd; i++) {
const sChild = sChildren[i];
const nextPosition = keyIndex.get(aKeys[i]);
if (nextPosition !== void 0) {
nodePosition = (nodePosition < nextPosition)
? nextPosition
: MagicValues.RearrangeNodes;
sources[nextPosition - start] = i;
result[nextPosition] = sChild;
} else if (sChild !== null) {
_unmount(sChild, true);
}
}
// Step 4
// Mark LIS nodes only when this node weren't moved `moveNode === false` and we've detected that one of the
// children nodes were moved `pos === MagicValues.MovedChildren`.
if (!(updateFlags & Flags.DisplaceNode) && nodePosition === MagicValues.RearrangeNodes) {
markLIS(sources);
}
while (bLength-- > 0) {
bEnd = bLength + start;
const node = bVNodes[bEnd];
const lisValue = sources[bLength];
result[bEnd] = (lisValue === -1)
? _mount(sNode, node)
: _update(
sNode,
result[bEnd],
node,
updateFlags |
((nodePosition === MagicValues.RearrangeNodes && lisValue !== MagicValues.LISMark)
? Flags.DisplaceNode
: 0),
);
}
}
// Delayed update for nodes from Step 1 (prefix only). Reconciliation algorithm always updates nodes from right to
// left.
while (start > 0) {
result[--start] = _update(
sNode,
sChildren[start],
bVNodes[start],
updateFlags,
);
}
}
sNode.c = result;
};
/**
* Modified Longest Increased Subsequence algorithm.
*
* Mutates input array `a` and replaces all values that are part of LIS with -2 value.
*
* Constraints:
* - Doesn't work with negative numbers. -1 values are ignored.
* - Input array `a` should contain at least one value that is greater than -1.
*
* {@link http://en.wikipedia.org/wiki/Longest_increasing_subsequence}
*
* @example
*
* const A = Int32Array.from([-1, 0, 2, 1]);
* markLIS(A);
* // A => [-1, -2, 2, -2]
*
* @param a Array of numbers.
* @noinline
* @__NOINLINE__
*/
const markLIS = (a: Int32Array): void => {
const length = a.length;
const parent = new _Int32Array(length);
const index = new _Int32Array(length);
let indexLength = 0;
let i = 0;
let j: number;
let k: number;
let lo: number;
let hi: number;
// Skip -1 values at the start of the input array `a`.
for (; a[i] === MagicValues.NewNodeMark; i++) { /**/ }
index[0] = i++;
for (; i < length; i++) {
k = a[i];
if (k !== MagicValues.NewNodeMark) { // Ignore -1 values.
j = index[indexLength];
if (a[j] < k) {
parent[i] = j;
index[++indexLength] = i;
} else {
lo = 0;
hi = indexLength;
while (lo < hi) {
j = (lo + hi) >> 1;
if (a[index[j]] < k) {
lo = j + 1;
} else {
hi = j;
}
}
if (k < a[index[lo]]) {
if (lo > 0) {
parent[i] = index[lo - 1];
}
index[lo] = i;
}
}
}
};
// Mutate input array `a` and assign -2 value to all nodes that are part of LIS.
j = index[indexLength];
while (indexLength-- >= 0) {
a[j] = MagicValues.LISMark;
j = parent[j];
}
};
/**
* Creates a HTML Template cloning factory.
*
* @__NO_SIDE_EFFECTS__
*/
export const _hN = (t: string | Node): () => Element => (
() => {
if (typeof t === "string") {
HTM_TEMPLATE.innerHTML = t;
t = HTM_TEMPLATE_CONTENT.firstChild!;
}
return nodeCloneNode.call(t, true) as Element;
}
);
/**
* Creates a HTML Element factory.
*
* @__NO_SIDE_EFFECTS__
*/
export const _hE = (t: string): () => Element => (
() => doc.createElement(t)
);
/**
* Creates a SVG Template cloning factory.
*/
export const _sN = (t: string | Node): () => Element => (
() => {
if (typeof t === "string") {
SVG_TEMPLATE.innerHTML = t;
t = SVG_TEMPLATE_CONTENT.firstChild!;
}
return nodeCloneNode.call(t, true) as Element;
}
);
/**
* Creates a SVG Element factory.
*
* @__NO_SIDE_EFFECTS__
*/
export const _sE = (t: string): () => Element => (
() => doc.createElementNS("http://www.w3.org/2000/svg", t)
);
/**
* Creates a template descriptor with globally shared data.
*
* @__NO_SIDE_EFFECTS__
*/
export const _T = (
p2: () => Element,
f: number,
p: PropOpCode[],
c: ChildOpCode[],
s: StateOpCode[],
d = __IVI_STRINGS__,
): TemplateDescriptor => ({
f: Flags.Template,
p1: { f, p, c, s, d },
p2,
});
/**
* @__NO_SIDE_EFFECTS__
*/
export const _t = (d: TemplateDescriptor, p: any[]): VTemplate => ({ d, p });
export type ComponentFactory = {
(
factory: (c: Component) => () => VAny,
areEqual?: (a?: any, b?: any) => boolean
): () => VComponent<undefined>;
<P>(
factory: (c: Component<P>) => (props: P) => VAny,
areEqual?: (prev: P, next: P) => boolean
): (props: P) => VComponent<P>;
};
/**
* Creates a factory that produces component nodes.
*
* @typeparam P Property type.
* @param factory Function that produces stateful render functions.
* @param areEqyal Function that checks `props` for equality.
* @returns Factory that produces component nodes.
* @__NO_SIDE_EFFECTS__
*/
export const component: ComponentFactory = <P>(
p1: (c: Component) => (props?: P) => VAny,
p2?: (prev: P, next: P) => boolean,
): (p?: any) => VComponent => {
const d: ComponentDescriptor = { f: Flags.Component, p1, p2 };
return (p: P) => ({ d, p });
};
/**
* Gets current component props.
*
* @typeparam P Property type.
* @param component Component node.
* @returns Current component props.
*/
export const getProps = <P>(component: Component<P>): P => (
component.v.p
);
/**
* Adds an unmount hook.
*
* @example
*
* const Example = component((c) => {
* useUnmount(c, () => { console.log("unmounted"); });
*
* return () => null;
* });
*
* @param component Component instance.
* @param hook Unmount hook.
*/
export const useUnmount = (component: Component, hook: () => void): void => {
const hooks = component.s2;
component.s2 = (hooks === null)
? hook
: (typeof hooks === "function")
? [hooks, hook]
: (hooks.push(hook), hooks);
};
export type Effect = {
(
component: Component,
effect: () => (() => void) | void,
areEqual?: (prev?: any, next?: any) => boolean
): () => void;
<P>(
component: Component,
effect: (props: P) => (() => void) | void,
areEqual?: (prev: P, next: P) => boolean
): (props: P) => void;
};
/**
* Creates a side effect hook.
*
* @example
*
* const Example = component((c) => {
* const [count, setCount] = useState(c, 0);
* const timer = useEffect(c, ({ interval }) => {
* const tid = setInterval(() => { setCount(count() + 1); }, interval);
* return () => { clearInterval(tid); };
* }, shallowEq);
*
* return (interval) => (
* timer({ interval }),
*
* html`<span>${count()}</span>`
* );
* });
*
* @typeparam T Hook props type.
* @param component Component instance.
* @param hook Side effect function.
* @param areEqual Function that checks if input value hasn't changed.
* @returns Side effect hook.
*/
export const useEffect: Effect = <P>(
component: Component,
hook: (props?: P) => (() => void) | void,
areEqual?: (prev: P, next: P) => boolean,
): (props?: P) => void => {
// var usage is intentional, see `docs/internals/perf.md` for an explanation.
var reset: (() => void) | void;
var prev: P | undefined;
var pending: boolean | undefined;
return (next?: P) => {
if (
pending !== true && (
areEqual === void 0 ||
prev === void 0 ||
areEqual(prev as P, next as P) === false
)
) {
if (pending === void 0) {
useUnmount(component, () => {
pending = false;
if (reset !== void 0) {
reset();
}
});
}
pending = true;
RENDER_CONTEXT.e.push(() => {
if (pending === true) {
pending = false;
if (reset !== void 0) {
reset();
}
reset = hook(next!);
}
});
}
prev = next;
};
};
let _animationFrameEffects: (() => void)[] = [];
let _idleEffects: (() => void)[] = [];
const _flushAnimationFrameEffects = () => {
while (_animationFrameEffects.length > 0) {
const e = _animationFrameEffects;
_animationFrameEffects = [];
for (let i = 0; i < e.length; i++) {
e[i]();
}
}
};
const _flushIdleEffects = () => {
while (_idleEffects.length > 0) {
const e = _idleEffects;
_idleEffects = [];
for (let i = 0; i < e.length; i++) {
e[i]();
}
}
};
/* @__NO_SIDE_EFFECTS__ */
export const createEffectHandler = (scheduleFlushTask: () => Array<() => void>) => <P>(
component: Component,
hook: (props?: P) => (() => void) | void,
areEqual?: (prev: P, next: P) => boolean,
): (props?: P) => void => {
// var usage is intentional, see `docs/internals/perf.md` for an explanation.
var reset: (() => void) | void;
var prev: P | undefined;
var pending: boolean | undefined;
return (next?: P) => {
if (
pending !== true && (
areEqual === void 0 ||
prev === void 0 ||
areEqual(prev as P, next as P) === false
)
) {
if (pending === void 0) {
useUnmount(component, () => {
pending = false;
if (reset !== void 0) {
reset();
}
});
}
pending = true;
scheduleFlushTask().push(() => {
if (pending === true) {
pending = false;
if (reset !== void 0) {
reset();
}
reset = hook(next!);
}
});
}
prev = next;
};
};
const _scheduleAnimationFrameEffects = () => {
const queue = _animationFrameEffects;
if (queue.length === 0) {
_requestAnimationFrame(_flushAnimationFrameEffects);
}
return queue;
};
export const useAnimationFrameEffect = createEffectHandler(_scheduleAnimationFrameEffects);
const _scheduleIdleEffects = () => {
const queue = _idleEffects;
if (queue.length === 0) {
_requestIdleCallback(_flushIdleEffects);
}
return queue;
};
export const useIdleEffect = createEffectHandler(_scheduleIdleEffects);
/**
* Invalidates a component.
*
* @param c Component instance.
*/
export const invalidate = (c: Component): void => {
if (!(c.f & Flags.Dirty)) {
c.f |= Flags.Dirty;
let prev: SNode = c;
let parent = c.p;
while (parent !== null) {
// Polymorphic call-sites
if (parent.f & Flags.DirtySubtree) {
return;
}
prev = parent;
parent.f |= Flags.DirtySubtree;
parent = parent.p;
}
(prev as SRoot).v.d.p1(prev as SRoot, prev.s1);
}
};
/**
* VDescriptor for List nodes.
*/
export const LIST_DESCRIPTOR: ListDescriptor = {
f: Flags.List,
p1: null,
p2: null,
};
/**
* Creates a dynamic list.
*
* @typeparam E Entry type.
* @typeparam K Key type.
* @param entries Entries.
* @param getKey Get key from entry function.
* @param render Render entry function.
* @returns Dynamic list.
* @__NO_SIDE_EFFECTS__
*/
export const List = <E, K>(
entries: E[],
getKey: (e