!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
This commit is contained in:
Hoikan 2024-03-06 08:51:37 +00:00 committed by 陈超涛
parent 79a34e2849
commit e463938a9d
9 changed files with 244 additions and 284 deletions

View File

@ -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;
}

View File

@ -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
};
const disposer = createRoot(() => {
insert(element, codeFn(), element.firstChild ? null : undefined);
});
return () => {
disposer();

View File

@ -67,7 +67,6 @@ describe('conditions', () => {
$$runComponent(Cond, {
get branches() {
return [
[
() => x.get() > 10,
() => {
const _el$3 = _tmpl$(),
@ -75,8 +74,6 @@ describe('conditions', () => {
$$insert(_el$3, x, _el$4);
return _el$3;
},
],
[
() => 5 > x.get(),
() => {
const _el$5 = _tmpl$2(),
@ -84,8 +81,7 @@ describe('conditions', () => {
$$insert(_el$5, x, _el$6);
return _el$5;
},
],
[
true,
() => {
const _el$7 = _tmpl$4(),
@ -93,7 +89,6 @@ describe('conditions', () => {
$$insert(_el$7, x, _el$8);
return _el$7;
},
],
];
},
}),
@ -150,7 +145,6 @@ describe('conditions', () => {
$$runComponent(Cond, {
get branches() {
return [
[
() => x.get() > 10,
() => {
const _el$3 = _tmpl$(),
@ -158,7 +152,6 @@ describe('conditions', () => {
$$insert(_el$3, x, _el$4);
return _el$3;
},
],
];
},
}),
@ -225,7 +218,6 @@ describe('conditions', () => {
$$runComponent(Cond, {
get branches() {
return [
[
() => x.get() > 10,
() => {
const _el$3 = _tmpl$(),
@ -233,8 +225,6 @@ describe('conditions', () => {
$$insert(_el$3, x, _el$4);
return _el$3;
},
],
[
() => 5 > x.get(),
() => {
const _el$5 = _tmpl$2(),
@ -242,14 +232,11 @@ describe('conditions', () => {
$$insert(_el$5, x, _el$6);
return _el$5;
},
],
[
true,
() => {
return $$runComponent(Cond, {
get branches() {
return [
[
() => x.get() > 7,
() => {
const _el$8 = _tmpl$3(),
@ -257,8 +244,6 @@ describe('conditions', () => {
$$insert(_el$8, x, _el$9);
return _el$8;
},
],
[
true,
() => {
const _el$10 = _tmpl$4(),
@ -266,12 +251,10 @@ describe('conditions', () => {
$$insert(_el$10, x, _el$11);
return _el$10;
},
],
];
},
});
},
],
];
},
}),
@ -339,12 +322,10 @@ describe('conditions', () => {
$$runComponent(Cond, {
get branches() {
return [
[
() => showX.get(),
() => {
return _tmpl$();
},
],
];
},
}),
@ -355,12 +336,10 @@ describe('conditions', () => {
$$runComponent(Cond, {
get branches() {
return [
[
() => showY.get(),
() => {
return _tmpl2$();
},
],
];
},
}),
@ -371,12 +350,10 @@ describe('conditions', () => {
$$runComponent(Cond, {
get branches() {
return [
[
() => showZ.get(),
() => {
return _tmpl3$();
},
],
];
},
}),

View File

@ -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 <>
* <input id="inline-fn-change" onChange={() =>fn("bound")}/>
* <input id="var-change" onChange={handler}/>
* <input id="hoisted-var-change" onChange={fn}/>
* <button id="inline-fn-click" onClick={() =>fn("delegated")}>Click Delegated</button>
* <button id="var-click" onClick={handler}>Click Delegated</button>
* <button id="hoisted-var-click" onClick={fn}>Click Delegated</button>
* </>;
* };
*
* render(() => <CountingComponent />, container);
*/
// 编译后:
const $tmpl = /*#__PURE__*/ $$template(`<input id="inline-fn-change">`),
$tmpl_2 = /*#__PURE__*/ $$template(`<input id="var-change">`),
$tmpl_3 = /*#__PURE__*/ $$template(`<input id="hoisted-var-change">`),
$tmpl_4 = /*#__PURE__*/ $$template(`<button id="inline-fn-click">Click Delegated`),
$tmpl_5 = /*#__PURE__*/ $$template(`<button id="var-click">Click Delegated`),
$tmpl_6 = /*#__PURE__*/ $$template(`<button id="hoisted-var-click">Click Delegated`);
const fn = vi.fn();
const Comp = () => {
const handler = () => fn();
return [
(() => {
const _el$ = $tmpl();
$$on(_el$, 'change', () => fn('bound'));
return _el$;
})(),
(() => {
const _el$2 = $tmpl_2();
$$on(_el$2, 'change', handler);
return _el$2;
})(),
(() => {
const _el$3 = $tmpl_3();
$$on(_el$3, 'change', fn);
return _el$3;
})(),
(() => {
const _el$4 = $tmpl_4();
$$on(_el$4, 'click', () => fn('delegated'));
return _el$4;
})(),
(() => {
const _el$5 = $tmpl_5();
$$on(_el$5, 'click', handler);
return _el$5;
})(),
(() => {
const _el$6 = $tmpl_6();
$$on(_el$6, 'click', fn);
return _el$6;
})(),
];
return <>
<input id="inline-fn-change" onChange={() =>fn("bound")}/>
<input id="var-change" onChange={handler}/>
<input id="hoisted-var-change" onChange={fn}/>
<button id="inline-fn-click" onClick={() =>fn("delegated")}>Click Delegated</button>
<button id="var-click" onClick={handler}>Click Delegated</button>
<button id="hoisted-var-click" onClick={fn}>Click Delegated</button>
</>;
};
render(() => $$runComponent(Comp), container);
$$delegateEvents(['click']);
render(() => <Comp />, container);
dispatchChangeEvent(document.getElementById('inline-fn-change'), 'change');
expect(fn).toHaveBeenCalledTimes(1);

View File

@ -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 <Child />;
* }
* function Child() {
* onMount(() => {
* log.add('Child');
* });
* return <GrandChild />;
* }
* function GrandChild() {
* onMount(() => {
* log.add('GrandChild');
* });
* }
* render(() => <App />, container);
*/
// 编译后:
function App() {
onMount(() => {
log.add('App');
});
return $$runComponent(Child);
return <Child />;
}
function Child() {
onMount(() => {
log.add('Child');
});
return $$runComponent(GrandChild);
return <GrandChild />;
}
function GrandChild() {
@ -66,27 +42,14 @@ describe('onMount', () => {
});
}
render(() => $$runComponent(App, {}), container);
render(() => <App />, 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 <div />;
* }
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ $$template(`<div>`);
function App() {
onMount(() => {
log.add('App1');
@ -94,9 +57,10 @@ describe('onMount', () => {
onMount(() => {
log.add('App2');
});
return _tmpl$();
return <div />;
}
render(() => $$runComponent(App, {}), container);
render(() => <App />, container);
await nextTick();
expect(log.get()).toEqual(['App1', 'App2']);
@ -117,26 +81,12 @@ describe('onMount', () => {
return data;
}
/**
*
* function App() {
* const data = useFetch();
* return <div>{data.get()}</div>;
* }
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ $$template(`<div>`);
function App() {
const data = useFetch();
return (() => {
const _el$ = _tmpl$();
$$insert(_el$, () => data.get());
return _el$;
})();
return <div>{data.get()}</div>;
}
render(() => $$runComponent(App, {}), container);
render(() => <App />, container);
await nextTick();
expect(container.innerHTML).toBe('<div>fake data</div>');
});

View File

@ -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 <div id="count">Count value is 0.</div>;
};
render(() => $$runComponent(CountingComponent, {}), container);
render(() => <CountingComponent />, container);
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0.');
});
@ -35,7 +33,7 @@ describe('render', () => {
const CountingComponent = () => {
return <div id="count">Count value is {0}.</div>;
};
render(() => $$runComponent(CountingComponent, {}), container);
render(() => <CountingComponent />, container);
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0.');
});
@ -50,12 +48,14 @@ describe('render', () => {
<>
<div id="count">Count value is {count.get()}.</div>
<div>
<button id="btn" onClick={add}>add</button>
<button id="btn" onClick={add}>
add
</button>
</div>
</>
);
};
render(() => $$runComponent(CountingComponent, {}), container);
render(() => <CountingComponent />, container);
container.querySelector('#btn').click();
@ -75,12 +75,14 @@ describe('render', () => {
<div>
<CountValue count={count} />
<div>
<button id="btn" onClick={add}>add</button>
<button id="btn" onClick={add}>
add
</button>
</div>
</div>
);
};
render(() => $$runComponent(CountingComponent, {}), container);
render(() => <CountingComponent />, 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 <div>Title: {props.children}</div>;
};
@ -98,10 +100,16 @@ describe('render', () => {
count.set(c => c + 1);
};
return <div>
<CountValue><h1>Your count is {count.get()}.</h1></CountValue>
<div><button onClick={add}>add</button></div>
</div>;
return (
<div>
<CountValue>
<h1>Your count is {count.get()}.</h1>
</CountValue>
<div>
<button onClick={add}>add</button>
</div>
</div>
);
};
render(() => <CountingComponent />, container);
@ -118,7 +126,7 @@ describe('render', () => {
});
it('should render sub components', ({ container }) => {
const CountValue = (props) => {
const CountValue = props => {
return <div>Count value is {props.count} .</div>;
};
@ -140,7 +148,7 @@ describe('render', () => {
</div>
);
};
render(() => $$runComponent(CountingComponent, {}), container);
render(() => <CountingComponent />, container);
expect(container.querySelector('h1').innerHTML).toMatchInlineSnapshot('"0"');
container.querySelector('button').click();
@ -159,17 +167,16 @@ describe('render', () => {
const CountingComponent = () => {
return <div style="color: red;">Count value is 0.</div>;
};
render(() => $$runComponent(CountingComponent, {}), container);
render(() => <CountingComponent />, container);
});
it('should render string of style with expression', ({ container }) => {
const Comp = () => {
const color = 'red';
return <div style={`color: ${color};`}>Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, container);
expect(container.querySelector('div').style.color).toEqual('red');
});
@ -177,18 +184,17 @@ describe('render', () => {
const Comp = () => {
return <div style={{ color: 'red', display: 'flex' }}>Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, container);
expect(container.querySelector('div').style.color).toEqual('red');
});
it('should render object of style with expression', ({ container }) => {
const Comp = () => {
const color = 'red';
return <div style={{ color }}>Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, container);
expect(container.querySelector('div').style.color).toEqual('red');
});
@ -198,7 +204,7 @@ describe('render', () => {
return <div style={{ color: color.get() }}>Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, 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 <div style={style.get()}>Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, 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 <div className="red">Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, container);
expect(container.querySelector('div').className).toEqual('red');
});
@ -230,7 +236,7 @@ describe('render', () => {
const color = 'red';
return <div className={color}>Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, container);
expect(container.querySelector('div').className).toEqual('red');
});
@ -238,7 +244,7 @@ describe('render', () => {
const Comp = () => {
return <div className={{ red: true }}>Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, container);
expect(container.querySelector('div').className).toEqual('red');
});
@ -246,7 +252,7 @@ describe('render', () => {
const Comp = () => {
return <div className={['red', 'green']}>Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, container);
expect(container.querySelector('div').className).toEqual('red green');
});
@ -255,7 +261,7 @@ describe('render', () => {
const color = reactive('red');
return <div className={color.get()}>Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, 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 <div className={{ [color.get()]: true }}>Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, container);
expect(container.querySelector('div').className).toEqual('red');
});
it('should update class with array', ({ container }) => {
const Comp = () => {
const color = reactive('red');
return <div className={[color.get(), 'green']}
>Count value is 0.</div>;
return <div className={[color.get(), 'green']}>Count value is 0.</div>;
};
render(() => $$runComponent(Comp, {}), container);
render(() => <Comp />, container);
expect(container.querySelector('div').className).toEqual('red green');
});
it('should render attribute', ({ container }) => {
function App() {
return (
<div id="test">parallel</div>
);
return <div id="test">parallel</div>;
}
render(() => $$runComponent(App, {}), container);
render(() => <App />, container);
expect(container.querySelector('div').id).toEqual('test');
});
it('should update attribute', ({ container }) => {
const id = reactive('el');
function App() {
return (
<div id={id.get()}>parallel</div>
);
function App() {
return <div id={id.get()}>parallel</div>;
}
render(() => $$runComponent(App, {}), container);
render(() => <App />, container);
expect(container.querySelector('div').id).toEqual('el');
id.set('test');
expect(container.querySelector('div').id).toEqual('test');

View File

@ -18,7 +18,7 @@ import { Signal } from './Types';
import { isFunction } from './Utils';
import { schedule } from './SetScheduler';
let runningRNode: RNode<any> | undefined = undefined; // 当前正执行的RNode
let runningRNode: RNode<any> | Root | undefined = undefined; // 当前正执行的RNode
let calledGets: RNode<any>[] | null = null;
let sameGetsIndex = 0; // 记录前后两次运行RNode时调用get顺序没有变化的节点
@ -50,9 +50,9 @@ export class RNode<T = any> implements Signal<T> {
_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<T = any> implements Signal<T> {
}
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<T = any> implements Signal<T> {
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<T = any> implements Signal<T> {
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<any> = this.sources[i];
@ -297,6 +305,50 @@ export function onCleanup<T = any>(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++) {

View File

@ -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
};

View File

@ -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);
});
});