From e463938a9d3663fe48a210bfe8a5579755ed9019 Mon Sep 17 00:00:00 2001 From: Hoikan <408255371@qq.com> Date: Wed, 6 Mar 2024 08:51:37 +0000 Subject: [PATCH] !164 optimize(no-vdom): switch condition branches to plain array * optimize(no-vdom): switch condition branches to plain array * test(no-vdom): switch to jsx * feat(reactive): creatRoot * test(no-vdom): switch render test to jsx --- packages/inula-novdom/src/components/Cond.tsx | 9 +- packages/inula-novdom/src/core.ts | 10 +- .../inula-novdom/tests/conditions.test.tsx | 179 ++++++++---------- packages/inula-novdom/tests/event.test.tsx | 74 ++------ .../inula-novdom/tests/lifecycle.test.tsx | 70 +------ packages/inula-novdom/tests/render.test.tsx | 87 +++++---- packages/inula-reactive/src/RNode.ts | 64 ++++++- packages/inula-reactive/src/index.ts | 6 +- .../inula-reactive/tests/onCleanup.test.ts | 29 +++ 9 files changed, 244 insertions(+), 284 deletions(-) create mode 100644 packages/inula-reactive/tests/onCleanup.test.ts diff --git a/packages/inula-novdom/src/components/Cond.tsx b/packages/inula-novdom/src/components/Cond.tsx index b9f826df..5ff5e6b5 100644 --- a/packages/inula-novdom/src/components/Cond.tsx +++ b/packages/inula-novdom/src/components/Cond.tsx @@ -23,16 +23,17 @@ type CondExpression = boolean | (() => boolean); type Branch = JSXElement | (() => JSXElement); export interface CondProps { - // Array of tuples, first item is the condition, second is the branch to render - branches: [CondExpression, Branch][]; + // The odd number of branches is the condition expression and the even number of branches is the branch + branches: (CondExpression | Branch)[]; } export function Cond(props: CondProps) { // Find the first branch that matches the condition // Any signal that used in condition expression, will trigger the condition to recompute const currentBranch = computed(() => { - for (let i = 0; i < props.branches.length; i++) { - const [condition, branch] = props.branches[i]; + for (let i = 0; i < props.branches.length; i=i+2) { + const condition = props.branches[i]; + const branch = props.branches[i + 1]; if (typeof condition === 'function' ? condition() : condition) { return branch; } diff --git a/packages/inula-novdom/src/core.ts b/packages/inula-novdom/src/core.ts index 5bbaf3d5..07b0cf26 100644 --- a/packages/inula-novdom/src/core.ts +++ b/packages/inula-novdom/src/core.ts @@ -14,7 +14,7 @@ */ import { insert } from './dom'; -import { RNode, untrack, setScheduler } from 'inula-reactive'; +import { RNode, untrack, setScheduler, createRoot } from 'inula-reactive'; import { readContext } from './components/Env'; // enable the scheduler @@ -50,11 +50,9 @@ export function render(codeFn: CodeFunction, element: HTMLElement): () => void { throw new Error('Render target is not valid.'); } - const disposer = (): void => { - // TODO - }; - - insert(element, codeFn(), element.firstChild ? null : undefined); + const disposer = createRoot(() => { + insert(element, codeFn(), element.firstChild ? null : undefined); + }); return () => { disposer(); diff --git a/packages/inula-novdom/tests/conditions.test.tsx b/packages/inula-novdom/tests/conditions.test.tsx index 01f85abc..0e756669 100644 --- a/packages/inula-novdom/tests/conditions.test.tsx +++ b/packages/inula-novdom/tests/conditions.test.tsx @@ -67,33 +67,28 @@ describe('conditions', () => { $$runComponent(Cond, { get branches() { return [ - [ - () => x.get() > 10, - () => { - const _el$3 = _tmpl$(), - _el$4 = _el$3.firstChild; - $$insert(_el$3, x, _el$4); - return _el$3; - }, - ], - [ - () => 5 > x.get(), - () => { - const _el$5 = _tmpl$2(), - _el$6 = _el$5.firstChild; - $$insert(_el$5, x, _el$6); - return _el$5; - }, - ], - [ - true, - () => { - const _el$7 = _tmpl$4(), - _el$8 = _el$7.firstChild; - $$insert(_el$7, x, _el$8); - return _el$7; - }, - ], + () => x.get() > 10, + () => { + const _el$3 = _tmpl$(), + _el$4 = _el$3.firstChild; + $$insert(_el$3, x, _el$4); + return _el$3; + }, + () => 5 > x.get(), + () => { + const _el$5 = _tmpl$2(), + _el$6 = _el$5.firstChild; + $$insert(_el$5, x, _el$6); + return _el$5; + }, + + true, + () => { + const _el$7 = _tmpl$4(), + _el$8 = _el$7.firstChild; + $$insert(_el$7, x, _el$8); + return _el$7; + }, ]; }, }), @@ -150,15 +145,13 @@ describe('conditions', () => { $$runComponent(Cond, { get branches() { return [ - [ - () => x.get() > 10, - () => { - const _el$3 = _tmpl$(), - _el$4 = _el$3.firstChild; - $$insert(_el$3, x, _el$4); - return _el$3; - }, - ], + () => x.get() > 10, + () => { + const _el$3 = _tmpl$(), + _el$4 = _el$3.firstChild; + $$insert(_el$3, x, _el$4); + return _el$3; + }, ]; }, }), @@ -225,53 +218,43 @@ describe('conditions', () => { $$runComponent(Cond, { get branches() { return [ - [ - () => x.get() > 10, - () => { - const _el$3 = _tmpl$(), - _el$4 = _el$3.firstChild; - $$insert(_el$3, x, _el$4); - return _el$3; - }, - ], - [ - () => 5 > x.get(), - () => { - const _el$5 = _tmpl$2(), - _el$6 = _el$5.firstChild; - $$insert(_el$5, x, _el$6); - return _el$5; - }, - ], - [ - true, - () => { - return $$runComponent(Cond, { - get branches() { - return [ - [ - () => x.get() > 7, - () => { - const _el$8 = _tmpl$3(), - _el$9 = _el$8.firstChild; - $$insert(_el$8, x, _el$9); - return _el$8; - }, - ], - [ - true, - () => { - const _el$10 = _tmpl$4(), - _el$11 = _el$10.firstChild; - $$insert(_el$10, x, _el$11); - return _el$10; - }, - ], - ]; - }, - }); - }, - ], + () => x.get() > 10, + () => { + const _el$3 = _tmpl$(), + _el$4 = _el$3.firstChild; + $$insert(_el$3, x, _el$4); + return _el$3; + }, + () => 5 > x.get(), + () => { + const _el$5 = _tmpl$2(), + _el$6 = _el$5.firstChild; + $$insert(_el$5, x, _el$6); + return _el$5; + }, + true, + () => { + return $$runComponent(Cond, { + get branches() { + return [ + () => x.get() > 7, + () => { + const _el$8 = _tmpl$3(), + _el$9 = _el$8.firstChild; + $$insert(_el$8, x, _el$9); + return _el$8; + }, + true, + () => { + const _el$10 = _tmpl$4(), + _el$11 = _el$10.firstChild; + $$insert(_el$10, x, _el$11); + return _el$10; + }, + ]; + }, + }); + }, ]; }, }), @@ -339,12 +322,10 @@ describe('conditions', () => { $$runComponent(Cond, { get branches() { return [ - [ - () => showX.get(), - () => { - return _tmpl$(); - }, - ], + () => showX.get(), + () => { + return _tmpl$(); + }, ]; }, }), @@ -355,12 +336,10 @@ describe('conditions', () => { $$runComponent(Cond, { get branches() { return [ - [ - () => showY.get(), - () => { - return _tmpl2$(); - }, - ], + () => showY.get(), + () => { + return _tmpl2$(); + }, ]; }, }), @@ -371,12 +350,10 @@ describe('conditions', () => { $$runComponent(Cond, { get branches() { return [ - [ - () => showZ.get(), - () => { - return _tmpl3$(); - }, - ], + () => showZ.get(), + () => { + return _tmpl3$(); + }, ]; }, }), diff --git a/packages/inula-novdom/tests/event.test.tsx b/packages/inula-novdom/tests/event.test.tsx index 9b4900a1..8118368e 100644 --- a/packages/inula-novdom/tests/event.test.tsx +++ b/packages/inula-novdom/tests/event.test.tsx @@ -17,9 +17,7 @@ import { describe, expect, vi } from 'vitest'; import { domTest as it } from './utils'; -import { template as $$template } from '../src/dom'; -import { runComponent as $$runComponent, render} from '../src/core'; -import { delegateEvents as $$delegateEvents, addEventListener as $$on } from '../src/event'; +import { render } from '@inula/no-vdom'; function dispatchMouseEvent(element: HTMLElement, eventType = 'click') { element.dispatchEvent(new MouseEvent(eventType, { bubbles: true })); @@ -35,70 +33,20 @@ function dispatchChangeEvent(input: HTMLElement, value: string) { describe('event', () => { it('should trigger delegated and bound event', ({ container }) => { - /** - * 源码: - * const fn = vi.fn(); - * const Comp = () => { - * const handler = () => fn(); - * return <> - * fn("bound")}/> - * - * - * - * - * - * ; - * }; - * - * render(() => , container); - */ - - // 编译后: - const $tmpl = /*#__PURE__*/ $$template(``), - $tmpl_2 = /*#__PURE__*/ $$template(``), - $tmpl_3 = /*#__PURE__*/ $$template(``), - $tmpl_4 = /*#__PURE__*/ $$template(` + + + ; }; - render(() => $$runComponent(Comp), container); - $$delegateEvents(['click']); + + render(() => , container); dispatchChangeEvent(document.getElementById('inline-fn-change'), 'change'); expect(fn).toHaveBeenCalledTimes(1); diff --git a/packages/inula-novdom/tests/lifecycle.test.tsx b/packages/inula-novdom/tests/lifecycle.test.tsx index 29b942bf..bac5148e 100644 --- a/packages/inula-novdom/tests/lifecycle.test.tsx +++ b/packages/inula-novdom/tests/lifecycle.test.tsx @@ -17,47 +17,23 @@ import { describe, expect } from 'vitest'; import { domTest as it, nextTick } from './utils'; -import { template as $$template, insert as $$insert } from '../src/dom'; -import { runComponent as $$runComponent, render, onMount } from '../src/core'; import { reactive } from 'inula-reactive'; +import { render, onMount } from '@inula/no-vdom'; describe('onMount', () => { it('should work.', async ({ container, log }) => { - /** - * 源码: - * function App() { - * onMount(() => { - * log.add('App'); - * }); - * return ; - * } - * function Child() { - * onMount(() => { - * log.add('Child'); - * }); - * return ; - * } - * function GrandChild() { - * onMount(() => { - * log.add('GrandChild'); - * }); - * } - * render(() => , container); - */ - - // 编译后: function App() { onMount(() => { log.add('App'); }); - return $$runComponent(Child); + return ; } function Child() { onMount(() => { log.add('Child'); }); - return $$runComponent(GrandChild); + return ; } function GrandChild() { @@ -66,27 +42,14 @@ describe('onMount', () => { }); } - render(() => $$runComponent(App, {}), container); + render(() => , container); + await nextTick(); expect(log.get()).toEqual(['App', 'Child', 'GrandChild']); }); it('should support multiple onMounts.', async ({ container, log }) => { - /** - * 源码: - * function App() { - * onMount(() => { - * log.add('App1'); - * }); - * onMount(() => { - * log.add('App2'); - * }); - * return
; - * } - */ // 编译后: - const _tmpl$ = /*#__PURE__*/ $$template(`
`); - function App() { onMount(() => { log.add('App1'); @@ -94,9 +57,10 @@ describe('onMount', () => { onMount(() => { log.add('App2'); }); - return _tmpl$(); + return
; } - render(() => $$runComponent(App, {}), container); + + render(() => , container); await nextTick(); expect(log.get()).toEqual(['App1', 'App2']); @@ -117,26 +81,12 @@ describe('onMount', () => { return data; } - /** - * 源码: - * function App() { - * const data = useFetch(); - * return
{data.get()}
; - * } - */ - // 编译后: - const _tmpl$ = /*#__PURE__*/ $$template(`
`); - function App() { const data = useFetch(); - return (() => { - const _el$ = _tmpl$(); - $$insert(_el$, () => data.get()); - return _el$; - })(); + return
{data.get()}
; } - render(() => $$runComponent(App, {}), container); + render(() => , container); await nextTick(); expect(container.innerHTML).toBe('
fake data
'); }); diff --git a/packages/inula-novdom/tests/render.test.tsx b/packages/inula-novdom/tests/render.test.tsx index 96969045..862a0ab3 100644 --- a/packages/inula-novdom/tests/render.test.tsx +++ b/packages/inula-novdom/tests/render.test.tsx @@ -15,9 +15,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck For the compiled code. import { reactive } from 'inula-reactive'; -import { - render -} from '@inula/no-vdom'; +import { render } from '@inula/no-vdom'; import { describe, expect } from 'vitest'; import { domTest as it } from './utils'; @@ -26,7 +24,7 @@ describe('render', () => { const CountingComponent = () => { return
Count value is 0.
; }; - render(() => $$runComponent(CountingComponent, {}), container); + render(() => , container); expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0.'); }); @@ -35,7 +33,7 @@ describe('render', () => { const CountingComponent = () => { return
Count value is {0}.
; }; - render(() => $$runComponent(CountingComponent, {}), container); + render(() => , container); expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0.'); }); @@ -50,12 +48,14 @@ describe('render', () => { <>
Count value is {count.get()}.
- +
); }; - render(() => $$runComponent(CountingComponent, {}), container); + render(() => , container); container.querySelector('#btn').click(); @@ -75,12 +75,14 @@ describe('render', () => {
- +
); }; - render(() => $$runComponent(CountingComponent, {}), container); + render(() => , container); container.querySelector('#btn').click(); @@ -88,7 +90,7 @@ describe('render', () => { }); it('should render components with slot', ({ container }) => { - const CountValue = (props) => { + const CountValue = props => { return
Title: {props.children}
; }; @@ -98,10 +100,16 @@ describe('render', () => { count.set(c => c + 1); }; - return
-

Your count is {count.get()}.

-
-
; + return ( +
+ +

Your count is {count.get()}.

+
+
+ +
+
+ ); }; render(() => , container); @@ -118,7 +126,7 @@ describe('render', () => { }); it('should render sub components', ({ container }) => { - const CountValue = (props) => { + const CountValue = props => { return
Count value is {props.count} .
; }; @@ -140,7 +148,7 @@ describe('render', () => {
); }; - render(() => $$runComponent(CountingComponent, {}), container); + render(() => , container); expect(container.querySelector('h1').innerHTML).toMatchInlineSnapshot('"0"'); container.querySelector('button').click(); @@ -159,17 +167,16 @@ describe('render', () => { const CountingComponent = () => { return
Count value is 0.
; }; - render(() => $$runComponent(CountingComponent, {}), container); + render(() => , container); }); - it('should render string of style with expression', ({ container }) => { const Comp = () => { const color = 'red'; return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').style.color).toEqual('red'); }); @@ -177,18 +184,17 @@ describe('render', () => { const Comp = () => { return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').style.color).toEqual('red'); }); - it('should render object of style with expression', ({ container }) => { const Comp = () => { const color = 'red'; return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').style.color).toEqual('red'); }); @@ -198,7 +204,7 @@ describe('render', () => { return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').style.color).toEqual('red'); container.querySelector('div').style.color = 'green'; expect(container.querySelector('div').style.color).toEqual('green'); @@ -210,7 +216,7 @@ describe('render', () => { return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').style.color).toEqual('red'); container.querySelector('div').style.color = 'green'; expect(container.querySelector('div').style.color).toEqual('green'); @@ -221,7 +227,7 @@ describe('render', () => { return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').className).toEqual('red'); }); @@ -230,7 +236,7 @@ describe('render', () => { const color = 'red'; return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').className).toEqual('red'); }); @@ -238,7 +244,7 @@ describe('render', () => { const Comp = () => { return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').className).toEqual('red'); }); @@ -246,7 +252,7 @@ describe('render', () => { const Comp = () => { return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').className).toEqual('red green'); }); @@ -255,7 +261,7 @@ describe('render', () => { const color = reactive('red'); return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').className).toEqual('red'); container.querySelector('div').className = 'green'; expect(container.querySelector('div').className).toEqual('green'); @@ -267,39 +273,36 @@ describe('render', () => { return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').className).toEqual('red'); }); it('should update class with array', ({ container }) => { const Comp = () => { const color = reactive('red'); - return
Count value is 0.
; + return
Count value is 0.
; }; - render(() => $$runComponent(Comp, {}), container); + render(() => , container); expect(container.querySelector('div').className).toEqual('red green'); }); it('should render attribute', ({ container }) => { function App() { - return ( -
parallel
- ); + return
parallel
; } - render(() => $$runComponent(App, {}), container); + + render(() => , container); expect(container.querySelector('div').id).toEqual('test'); }); it('should update attribute', ({ container }) => { const id = reactive('el'); - function App() { - return ( -
parallel
- ); + function App() { + return
parallel
; } - render(() => $$runComponent(App, {}), container); + + render(() => , container); expect(container.querySelector('div').id).toEqual('el'); id.set('test'); expect(container.querySelector('div').id).toEqual('test'); diff --git a/packages/inula-reactive/src/RNode.ts b/packages/inula-reactive/src/RNode.ts index 04c3fbc8..dbc74eb3 100644 --- a/packages/inula-reactive/src/RNode.ts +++ b/packages/inula-reactive/src/RNode.ts @@ -18,7 +18,7 @@ import { Signal } from './Types'; import { isFunction } from './Utils'; import { schedule } from './SetScheduler'; -let runningRNode: RNode | undefined = undefined; // 当前正执行的RNode +let runningRNode: RNode | Root | undefined = undefined; // 当前正执行的RNode let calledGets: RNode[] | null = null; let sameGetsIndex = 0; // 记录前后两次运行RNode时,调用get顺序没有变化的节点 @@ -50,9 +50,9 @@ export class RNode implements Signal { _value: T; fn?: () => T; - private observers: RNode[] | null = null; // 被谁用 - private sources: RNode[] | null = null; // 使用谁 - + observers: RNode[] | null = null; // 被谁用 + sources: RNode[] | null = null; // 使用谁 + subNodes: RNode[] | null = null; // he RNode that are running within the current RNode. protected state: State; isEffect = false; @@ -93,7 +93,7 @@ export class RNode implements Signal { } track() { - if (runningRNode) { + if (runningRNode && !isRoot(runningRNode)) { // 前后两次运行RNode,从左到右对比,如果调用get的RNode相同就calledGetsIndex加1 if (!calledGets && runningRNode.sources && runningRNode.sources[sameGetsIndex] == this) { sameGetsIndex++; @@ -170,6 +170,14 @@ export class RNode implements Signal { const prevGets = calledGets; const prevGetsIndex = sameGetsIndex; + if (runningRNode) { + if (runningRNode.subNodes) { + runningRNode.subNodes.push(this); + } else { + runningRNode.subNodes = [this]; + } + } + runningRNode = this; calledGets = null as any; sameGetsIndex = 0; @@ -263,7 +271,7 @@ export class RNode implements Signal { this.state = Fresh; } - private removeParentObservers(index: number): void { + removeParentObservers(index: number): void { if (!this.sources) return; for (let i = index; i < this.sources.length; i++) { const source: RNode = this.sources[i]; @@ -297,6 +305,50 @@ export function onCleanup(fn: (oldValue: T) => void): void { } } +export function isRoot(node: RNode | Root): node is Root { + return (node as Root).isRoot; +} + +export function createRoot(fn: () => void): () => void { + const root: Root = { + cleanups: [], + subNodes: null, + _value: null, + isRoot: true, + }; + const prevNode = runningRNode; + runningRNode = root; + try { + fn(); + // TODO: handle error + } finally { + runningRNode = prevNode; + } + return () => cleanRNode(root); +} + +type Root = Cleanable & { isRoot: true }; +type Cleanable = { + cleanups: ((oldValue: any) => void)[]; + subNodes: RNode[] | null; + _value: any; +} + +export function cleanRNode(node: Cleanable) { + if ((node as RNode).sources) { + (node as RNode).removeParentObservers(0); + } + + // subNodes cleanup should be done first + if (node.subNodes) { + for (let i = 0; i < node.subNodes.length; i++) { + cleanRNode(node.subNodes[i]); + } + } + + node.cleanups.forEach(cleanup => cleanup(node._value)); +} + /** run all non-clean effect nodes */ export function runEffects(): void { for (let i = 0; i < Effects.length; i++) { diff --git a/packages/inula-reactive/src/index.ts b/packages/inula-reactive/src/index.ts index 66fd2a00..1b786721 100644 --- a/packages/inula-reactive/src/index.ts +++ b/packages/inula-reactive/src/index.ts @@ -15,7 +15,7 @@ import { createComputed as computed, createReactive as reactive, createWatch as watch } from './RNodeCreator'; import { isReactiveObj } from './Utils'; -import { RNode, untrack, runEffects } from './RNode'; +import { RNode, untrack, runEffects, onCleanup, createRoot } from './RNode'; import { setScheduler } from './SetScheduler'; export interface Index { @@ -38,5 +38,7 @@ export { untrack, unwrap, setScheduler, - runEffects + runEffects, + createRoot, + onCleanup }; diff --git a/packages/inula-reactive/tests/onCleanup.test.ts b/packages/inula-reactive/tests/onCleanup.test.ts new file mode 100644 index 00000000..ffc8494b --- /dev/null +++ b/packages/inula-reactive/tests/onCleanup.test.ts @@ -0,0 +1,29 @@ +import { computed, createRoot, onCleanup, watch } from '../src'; + +describe('onCleanup', () => { + it('should work', () => { + const cleanup = jest.fn(); + const unmount = createRoot(() => { + onCleanup(cleanup); + }); + unmount(); + expect(cleanup).toBeCalled(); + }); + + it('should work in the nested effect', () => { + const cleanup = jest.fn(); + const unmount = createRoot(() => { + onCleanup(() => cleanup(1)); + watch(() => { + onCleanup(() => cleanup(2)); + computed(() => { + onCleanup(() => cleanup(3)); + }).get(); + }); + }); + unmount(); + expect(cleanup).toHaveBeenNthCalledWith(1, 3); + expect(cleanup).toHaveBeenNthCalledWith(2, 2); + expect(cleanup).toHaveBeenNthCalledWith(3, 1); + }); +});