diff --git a/examples/array-test/src/main.tsx b/examples/array-test/src/main.tsx index aa3b9a2..748c6d5 100644 --- a/examples/array-test/src/main.tsx +++ b/examples/array-test/src/main.tsx @@ -10,7 +10,7 @@ const Component = () => {
{() => }
{() => [, ]}
- [ ,,] + {() => [, ]} ); }; diff --git a/packages/core/src/createElement.spec.ts b/packages/core/src/createElement.spec.ts index 790b077..70cab90 100644 --- a/packages/core/src/createElement.spec.ts +++ b/packages/core/src/createElement.spec.ts @@ -84,14 +84,56 @@ describe("render test", () => { it("component return array", () => { const Component = () => { - return [createElement("div"), createElement("div"), createElement("div")]; + return [ + createElement("div", { children: "A" }), + createElement("div", { children: "B" }), + createElement("div", { children: "C" }), + ]; }; - const node = createElement("div", { children: createElement(Component) }); + const node = createElement("div", { + children: [ + createElement("div", { + children: () => createElement(Component), + }), + createElement("div", { + children: () => [createElement(Component)], + }), + createElement(Component), + ], + }); expect(node.el).toMatchInlineSnapshot(`
-
-
-
+
+
+ A +
+
+ B +
+
+ C +
+
+
+
+ A +
+
+ B +
+
+ C +
+
+
+ A +
+
+ B +
+
+ C +
`); }); @@ -392,7 +434,7 @@ describe("reactive test", () => { vi.useRealTimers(); }); - it("Three Todo List with key", async () => { + it.only("Three Todo List with key", async () => { vi.useFakeTimers(); const items = createState<{ id: number; hidden: boolean }[]>([ @@ -403,14 +445,6 @@ describe("reactive test", () => { ]); const updateList = () => { - // const values = new Array(10).fill("").map((v, index) => { - // return { - // id: index, - // hidden: (Math.random() * 10) % 2 > 1, - // }; - // }); - // console.log("test test", (Math.random() * 10) % 2, values); - // items.val = values; items.val = [ { id: 1, hidden: false }, { id: 2, hidden: false }, @@ -432,7 +466,7 @@ describe("reactive test", () => { }); return createElement("p", { children: () => { - return `${name} counter ${state.val}`; + return `TodoItem1 ${name} counter ${state.val}`; }, }); }; @@ -455,24 +489,6 @@ describe("reactive test", () => { }); }; - const TodoItem3 = (props: { name: number }) => { - const { name } = props; - const state = createState(0); - useEffect(() => { - const handler = setInterval(() => { - state.val++; - }, 1000); - return () => { - clearInterval(handler); - }; - }); - return createElement("p", { - children: () => { - return `TodoItem3 ${name} counter ${state.val}`; - }, - }); - }; - const TodoList = () => { return createElement("div", { children: [ @@ -494,12 +510,6 @@ describe("reactive test", () => { ? null : createElement(TodoItem2, { name: item.id }, item.id); }), - () => - items.val.map((item) => { - return item.hidden - ? null - : createElement(TodoItem3, { name: item.id }, item.id); - }), ], }), ], @@ -514,13 +524,13 @@ describe("reactive test", () => {

- 1 counter 0 + TodoItem1 1 counter 0

- 2 counter 0 + TodoItem1 2 counter 0

- 4 counter 0 + TodoItem1 4 counter 0

TodoItem2 1 counter 0 @@ -531,15 +541,6 @@ describe("reactive test", () => {

TodoItem2 4 counter 0

-

- TodoItem3 1 counter 0 -

-

- TodoItem3 2 counter 0 -

-

- TodoItem3 4 counter 0 -

`); @@ -555,16 +556,16 @@ describe("reactive test", () => {

- 1 counter 10 + TodoItem1 1 counter 10

- 2 counter 10 + TodoItem1 2 counter 10

- 3 counter 5 + TodoItem1 3 counter 5

- 4 counter 10 + TodoItem1 4 counter 10

TodoItem2 1 counter 10 @@ -578,18 +579,6 @@ describe("reactive test", () => {

TodoItem2 4 counter 10

-

- TodoItem3 1 counter 10 -

-

- TodoItem3 2 counter 10 -

-

- TodoItem3 3 counter 5 -

-

- TodoItem3 4 counter 10 -

`); diff --git a/packages/core/src/createElement.ts b/packages/core/src/createElement.ts index e3f91d8..dbb07d3 100644 --- a/packages/core/src/createElement.ts +++ b/packages/core/src/createElement.ts @@ -8,21 +8,19 @@ import { TagNameMap, TagOption, } from "./type"; -import arrify from "./utils/arrify"; import isPrimitive from "./utils/isPrimitive"; +import numberConcat from "./utils/numberConcat"; type Dispose = () => void; -type JSXTag

= - | keyof TagNameMap - | ((props: P) => SaplingElement | SaplingElement[] | PrimitiveChild | null); +type JSXTag

= keyof TagNameMap | ((props: P) => SaplingNode); export type JSXElementType

= (props: P) => SaplingNode | SaplingElement; export type SaplingNode = | SaplingElement | PrimitiveChild - | Iterable + | SaplingNode[] | (() => SaplingNode) | null; @@ -89,6 +87,9 @@ export class SaplingElement { } if (params?.children != null) { this.children = params.children; + Array.from(params.children).forEach( + (child) => child.el != null && this._el?.appendChild(child.el), + ); } } @@ -101,30 +102,60 @@ export class SaplingElement { this.parent?.children.delete(this); }; - public appendChildJSXNode = (childrenNode: SaplingElement[]) => { - [...childrenNode].reduceRight( - (p: SaplingElement | null, c): SaplingElement => { - if (c.el?.parentElement != null) { + public append = (child: SaplingElement) => { + if (child.el != null) { + if (child.el.parentElement != null) { + // skip append for optimization + return child; + } + this.el?.appendChild(child.el); + this.children.add(child); + return child; + } else { + return Array.from(child.children).reduce( + (p: SaplingElement | null, c): SaplingElement | null => { + if (c.el == null) { + return this.append(c) ?? p; + } + if (c.el != null) { + if (c.el?.parentElement != null) { + return c; + } + if (p == null) { + // start from here + this.el?.appendChild(c.el); + } + if (p?.el != null) { + // continue insert + if (p.el.nextSibling != null) { + this.el?.insertBefore(c.el, p.el.nextSibling); + } else { + this.el?.appendChild(c.el); + } + } + this.children.add(c); + c.parent = this; + } + return c; - } - if (p == null && c.el != null) { - this.el?.appendChild(c.el); - } - if (p?.el != null && c.el != null) { - this.el?.insertBefore(c.el, p.el); - } - this.children.add(c); - c.parent = this; - return c; - }, - null, + }, + null, + ); + } + }; + + public hasChild = (childElement: SaplingElement): boolean => { + return ( + this.children.has(childElement) || + Array.from(this.children).reduce((p, c) => { + return p || c.hasChild(childElement); + }, false) ); - return this; }; - public removeExtraNodes = (childrenNode: Set) => { + public disposeElementNotIn = (childElement: SaplingElement) => { this.children.forEach((child) => { - if (!childrenNode.has(child)) { + if (!childElement.hasChild(child)) { child.dispose(); } }); @@ -136,19 +167,6 @@ export class SaplingElement { }; } -function prepareChildrenNodes( - children: SaplingNode | SaplingNode[], -): SaplingElement[] { - return arrify(children) - .flatMap((child) => { - if (isPrimitive(child)) { - return primitiveToJSXNode(child); - } - return child; - }) - .filter((v): v is SaplingElement => v != null); -} - const primitiveToJSXNode = (primitive: Primitive) => new SaplingElement({ node: new Text(primitive.toString()), @@ -158,6 +176,49 @@ const primitiveToJSXNode = (primitive: Primitive) => const JSXFactory = () => { const jsxScope = new JSXScope(); + // parse SaplingNode to SaplingElement + const prepareSaplingElement = ( + saplingNode: SaplingNode, + nodeCaches?: Map[], + cacheKey: number = 0, + ): SaplingElement => { + if (saplingNode instanceof SaplingElement) { + return saplingNode; + } + if (Array.isArray(saplingNode)) { + return new SaplingElement({ + children: new Set( + saplingNode.map((child, index) => + prepareSaplingElement( + child, + nodeCaches, + numberConcat(index, cacheKey), + ), + ), + ), + }); + } + if (isPrimitive(saplingNode)) { + return primitiveToJSXNode(saplingNode); + } + if (typeof saplingNode === "function") { + let resume; + + if (nodeCaches != null) { + const nodeCache = (nodeCaches[cacheKey] ||= new Map< + Key, + SaplingElement + >()); + resume = jsxScope.collectNodeCache(nodeCache); + } + + const element = prepareSaplingElement(saplingNode(), nodeCaches); + resume?.(); + return element; + } + return new SaplingElement(); + }; + const upsert = (element: Node, child: SaplingElement) => { child.el && element.appendChild(child.el); return () => { @@ -179,9 +240,7 @@ const JSXFactory = () => { ): SaplingElement; function createElement

( - jsxTag: ( - props: P, - ) => SaplingElement | SaplingElement[] | PrimitiveChild | null, + jsxTag: (props: P) => SaplingNode, options?: P, key?: Key, _isStaticChildren?: boolean, @@ -216,27 +275,12 @@ const JSXFactory = () => { const resume = jsxScope.collectDispose(disposeStack); const node = jsxTag(options as P); resume(); - - let jsxNode: SaplingElement; - if (Array.isArray(node)) { - jsxNode = new SaplingElement({ - node: document.createDocumentFragment(), - }); - jsxNode.appendChildJSXNode(node); - } else if (isPrimitive(node) || node == null) { - jsxNode = new SaplingElement({ - node: node == null ? null : new Text(node.toString()), - disposeStack, - children: null, - }); - } else { - jsxNode = node; - } - jsxNode.mergeDisposeStack(disposeStack); + const element = prepareSaplingElement(node); + element.mergeDisposeStack(disposeStack); if (key != null) { - jsxScope.setCache(key, jsxNode); + jsxScope.setCache(key, element); } - return jsxNode; + return element; } const { children, ref, ...props } = (options ?? {}) as TagOption< @@ -248,38 +292,24 @@ const JSXFactory = () => { ref.val = el; } - const jsxNode = new SaplingElement({ + const currentElement = new SaplingElement({ node: el, }); if (children != null) { - let nodeCaches: (Map | undefined)[] = []; + let nodeCaches: Map[] = []; effect(() => { - const childrenNode = arrify(children).map((child, index) => { - const nodeCache = (nodeCaches[index] ||= new Map< - Key, - SaplingElement - >()); - const resume = jsxScope.collectNodeCache(nodeCache); - const children = prepareChildrenNodes( - typeof child === "function" ? child() : child, - ); - resume(); - return children; - }); - - jsxNode.removeExtraNodes( - new Set(childrenNode.flatMap((child) => child)), - ); - jsxNode.appendChildJSXNode(childrenNode.flatMap((child) => child)); + const element = prepareSaplingElement(children, nodeCaches); + currentElement.disposeElementNotIn(element); + currentElement.append(element); }); } if (key != null) { - jsxScope.setCache(key, jsxNode); + jsxScope.setCache(key, currentElement); } - return jsxNode; + return currentElement; } const useEffect = (callback: () => Dispose | void) => { diff --git a/packages/core/src/utils/numberConcat.ts b/packages/core/src/utils/numberConcat.ts new file mode 100644 index 0000000..3eae5b9 --- /dev/null +++ b/packages/core/src/utils/numberConcat.ts @@ -0,0 +1,4 @@ +const numberConcat = (a: number, b: number) => + (a << (Math.ceil(Math.log2(b)) + 1)) + b; + +export default numberConcat;