!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); type Branch = JSXElement | (() => JSXElement);
export interface CondProps { export interface CondProps {
// Array of tuples, first item is the condition, second is the branch to render // The odd number of branches is the condition expression and the even number of branches is the branch
branches: [CondExpression, Branch][]; branches: (CondExpression | Branch)[];
} }
export function Cond(props: CondProps) { export function Cond(props: CondProps) {
// Find the first branch that matches the condition // Find the first branch that matches the condition
// Any signal that used in condition expression, will trigger the condition to recompute // Any signal that used in condition expression, will trigger the condition to recompute
const currentBranch = computed(() => { const currentBranch = computed(() => {
for (let i = 0; i < props.branches.length; i++) { for (let i = 0; i < props.branches.length; i=i+2) {
const [condition, branch] = props.branches[i]; const condition = props.branches[i];
const branch = props.branches[i + 1];
if (typeof condition === 'function' ? condition() : condition) { if (typeof condition === 'function' ? condition() : condition) {
return branch; return branch;
} }

View File

@ -14,7 +14,7 @@
*/ */
import { insert } from './dom'; import { insert } from './dom';
import { RNode, untrack, setScheduler } from 'inula-reactive'; import { RNode, untrack, setScheduler, createRoot } from 'inula-reactive';
import { readContext } from './components/Env'; import { readContext } from './components/Env';
// enable the scheduler // enable the scheduler
@ -50,11 +50,9 @@ export function render(codeFn: CodeFunction, element: HTMLElement): () => void {
throw new Error('Render target is not valid.'); throw new Error('Render target is not valid.');
} }
const disposer = (): void => { const disposer = createRoot(() => {
// TODO insert(element, codeFn(), element.firstChild ? null : undefined);
}; });
insert(element, codeFn(), element.firstChild ? null : undefined);
return () => { return () => {
disposer(); disposer();

View File

@ -67,33 +67,28 @@ describe('conditions', () => {
$$runComponent(Cond, { $$runComponent(Cond, {
get branches() { get branches() {
return [ return [
[ () => x.get() > 10,
() => x.get() > 10, () => {
() => { const _el$3 = _tmpl$(),
const _el$3 = _tmpl$(), _el$4 = _el$3.firstChild;
_el$4 = _el$3.firstChild; $$insert(_el$3, x, _el$4);
$$insert(_el$3, x, _el$4); return _el$3;
return _el$3; },
}, () => 5 > x.get(),
], () => {
[ const _el$5 = _tmpl$2(),
() => 5 > x.get(), _el$6 = _el$5.firstChild;
() => { $$insert(_el$5, x, _el$6);
const _el$5 = _tmpl$2(), return _el$5;
_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;
true, $$insert(_el$7, x, _el$8);
() => { return _el$7;
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, { $$runComponent(Cond, {
get branches() { get branches() {
return [ return [
[ () => x.get() > 10,
() => x.get() > 10, () => {
() => { const _el$3 = _tmpl$(),
const _el$3 = _tmpl$(), _el$4 = _el$3.firstChild;
_el$4 = _el$3.firstChild; $$insert(_el$3, x, _el$4);
$$insert(_el$3, x, _el$4); return _el$3;
return _el$3; },
},
],
]; ];
}, },
}), }),
@ -225,53 +218,43 @@ describe('conditions', () => {
$$runComponent(Cond, { $$runComponent(Cond, {
get branches() { get branches() {
return [ return [
[ () => x.get() > 10,
() => x.get() > 10, () => {
() => { const _el$3 = _tmpl$(),
const _el$3 = _tmpl$(), _el$4 = _el$3.firstChild;
_el$4 = _el$3.firstChild; $$insert(_el$3, x, _el$4);
$$insert(_el$3, x, _el$4); return _el$3;
return _el$3; },
}, () => 5 > x.get(),
], () => {
[ const _el$5 = _tmpl$2(),
() => 5 > x.get(), _el$6 = _el$5.firstChild;
() => { $$insert(_el$5, x, _el$6);
const _el$5 = _tmpl$2(), return _el$5;
_el$6 = _el$5.firstChild; },
$$insert(_el$5, x, _el$6); true,
return _el$5; () => {
}, return $$runComponent(Cond, {
], get branches() {
[ return [
true, () => x.get() > 7,
() => { () => {
return $$runComponent(Cond, { const _el$8 = _tmpl$3(),
get branches() { _el$9 = _el$8.firstChild;
return [ $$insert(_el$8, x, _el$9);
[ return _el$8;
() => x.get() > 7, },
() => { true,
const _el$8 = _tmpl$3(), () => {
_el$9 = _el$8.firstChild; const _el$10 = _tmpl$4(),
$$insert(_el$8, x, _el$9); _el$11 = _el$10.firstChild;
return _el$8; $$insert(_el$10, x, _el$11);
}, return _el$10;
], },
[ ];
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, { $$runComponent(Cond, {
get branches() { get branches() {
return [ return [
[ () => showX.get(),
() => showX.get(), () => {
() => { return _tmpl$();
return _tmpl$(); },
},
],
]; ];
}, },
}), }),
@ -355,12 +336,10 @@ describe('conditions', () => {
$$runComponent(Cond, { $$runComponent(Cond, {
get branches() { get branches() {
return [ return [
[ () => showY.get(),
() => showY.get(), () => {
() => { return _tmpl2$();
return _tmpl2$(); },
},
],
]; ];
}, },
}), }),
@ -371,12 +350,10 @@ describe('conditions', () => {
$$runComponent(Cond, { $$runComponent(Cond, {
get branches() { get branches() {
return [ return [
[ () => showZ.get(),
() => showZ.get(), () => {
() => { return _tmpl3$();
return _tmpl3$(); },
},
],
]; ];
}, },
}), }),

View File

@ -17,9 +17,7 @@
import { describe, expect, vi } from 'vitest'; import { describe, expect, vi } from 'vitest';
import { domTest as it } from './utils'; import { domTest as it } from './utils';
import { template as $$template } from '../src/dom'; import { render } from '@inula/no-vdom';
import { runComponent as $$runComponent, render} from '../src/core';
import { delegateEvents as $$delegateEvents, addEventListener as $$on } from '../src/event';
function dispatchMouseEvent(element: HTMLElement, eventType = 'click') { function dispatchMouseEvent(element: HTMLElement, eventType = 'click') {
element.dispatchEvent(new MouseEvent(eventType, { bubbles: true })); element.dispatchEvent(new MouseEvent(eventType, { bubbles: true }));
@ -35,70 +33,20 @@ function dispatchChangeEvent(input: HTMLElement, value: string) {
describe('event', () => { describe('event', () => {
it('should trigger delegated and bound event', ({ container }) => { 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 fn = vi.fn();
const Comp = () => { const Comp = () => {
const handler = () => fn(); const handler = () => fn();
return [ return <>
(() => { <input id="inline-fn-change" onChange={() =>fn("bound")}/>
const _el$ = $tmpl(); <input id="var-change" onChange={handler}/>
$$on(_el$, 'change', () => fn('bound')); <input id="hoisted-var-change" onChange={fn}/>
return _el$; <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>
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;
})(),
];
}; };
render(() => $$runComponent(Comp), container);
$$delegateEvents(['click']); render(() => <Comp />, container);
dispatchChangeEvent(document.getElementById('inline-fn-change'), 'change'); dispatchChangeEvent(document.getElementById('inline-fn-change'), 'change');
expect(fn).toHaveBeenCalledTimes(1); expect(fn).toHaveBeenCalledTimes(1);

View File

@ -17,47 +17,23 @@
import { describe, expect } from 'vitest'; import { describe, expect } from 'vitest';
import { domTest as it, nextTick } from './utils'; 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 { reactive } from 'inula-reactive';
import { render, onMount } from '@inula/no-vdom';
describe('onMount', () => { describe('onMount', () => {
it('should work.', async ({ container, log }) => { 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() { function App() {
onMount(() => { onMount(() => {
log.add('App'); log.add('App');
}); });
return $$runComponent(Child); return <Child />;
} }
function Child() { function Child() {
onMount(() => { onMount(() => {
log.add('Child'); log.add('Child');
}); });
return $$runComponent(GrandChild); return <GrandChild />;
} }
function GrandChild() { function GrandChild() {
@ -66,27 +42,14 @@ describe('onMount', () => {
}); });
} }
render(() => $$runComponent(App, {}), container); render(() => <App />, container);
await nextTick(); await nextTick();
expect(log.get()).toEqual(['App', 'Child', 'GrandChild']); expect(log.get()).toEqual(['App', 'Child', 'GrandChild']);
}); });
it('should support multiple onMounts.', async ({ container, log }) => { 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() { function App() {
onMount(() => { onMount(() => {
log.add('App1'); log.add('App1');
@ -94,9 +57,10 @@ describe('onMount', () => {
onMount(() => { onMount(() => {
log.add('App2'); log.add('App2');
}); });
return _tmpl$(); return <div />;
} }
render(() => $$runComponent(App, {}), container);
render(() => <App />, container);
await nextTick(); await nextTick();
expect(log.get()).toEqual(['App1', 'App2']); expect(log.get()).toEqual(['App1', 'App2']);
@ -117,26 +81,12 @@ describe('onMount', () => {
return data; return data;
} }
/**
*
* function App() {
* const data = useFetch();
* return <div>{data.get()}</div>;
* }
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ $$template(`<div>`);
function App() { function App() {
const data = useFetch(); const data = useFetch();
return (() => { return <div>{data.get()}</div>;
const _el$ = _tmpl$();
$$insert(_el$, () => data.get());
return _el$;
})();
} }
render(() => $$runComponent(App, {}), container); render(() => <App />, container);
await nextTick(); await nextTick();
expect(container.innerHTML).toBe('<div>fake data</div>'); expect(container.innerHTML).toBe('<div>fake data</div>');
}); });

View File

@ -15,9 +15,7 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck For the compiled code. // @ts-nocheck For the compiled code.
import { reactive } from 'inula-reactive'; import { reactive } from 'inula-reactive';
import { import { render } from '@inula/no-vdom';
render
} from '@inula/no-vdom';
import { describe, expect } from 'vitest'; import { describe, expect } from 'vitest';
import { domTest as it } from './utils'; import { domTest as it } from './utils';
@ -26,7 +24,7 @@ describe('render', () => {
const CountingComponent = () => { const CountingComponent = () => {
return <div id="count">Count value is 0.</div>; 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.'); expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0.');
}); });
@ -35,7 +33,7 @@ describe('render', () => {
const CountingComponent = () => { const CountingComponent = () => {
return <div id="count">Count value is {0}.</div>; 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.'); 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 id="count">Count value is {count.get()}.</div>
<div> <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(); container.querySelector('#btn').click();
@ -75,12 +75,14 @@ describe('render', () => {
<div> <div>
<CountValue count={count} /> <CountValue count={count} />
<div> <div>
<button id="btn" onClick={add}>add</button> <button id="btn" onClick={add}>
add
</button>
</div> </div>
</div> </div>
); );
}; };
render(() => $$runComponent(CountingComponent, {}), container); render(() => <CountingComponent />, container);
container.querySelector('#btn').click(); container.querySelector('#btn').click();
@ -88,7 +90,7 @@ describe('render', () => {
}); });
it('should render components with slot', ({ container }) => { it('should render components with slot', ({ container }) => {
const CountValue = (props) => { const CountValue = props => {
return <div>Title: {props.children}</div>; return <div>Title: {props.children}</div>;
}; };
@ -98,10 +100,16 @@ describe('render', () => {
count.set(c => c + 1); count.set(c => c + 1);
}; };
return <div> return (
<CountValue><h1>Your count is {count.get()}.</h1></CountValue> <div>
<div><button onClick={add}>add</button></div> <CountValue>
</div>; <h1>Your count is {count.get()}.</h1>
</CountValue>
<div>
<button onClick={add}>add</button>
</div>
</div>
);
}; };
render(() => <CountingComponent />, container); render(() => <CountingComponent />, container);
@ -118,7 +126,7 @@ describe('render', () => {
}); });
it('should render sub components', ({ container }) => { it('should render sub components', ({ container }) => {
const CountValue = (props) => { const CountValue = props => {
return <div>Count value is {props.count} .</div>; return <div>Count value is {props.count} .</div>;
}; };
@ -140,7 +148,7 @@ describe('render', () => {
</div> </div>
); );
}; };
render(() => $$runComponent(CountingComponent, {}), container); render(() => <CountingComponent />, container);
expect(container.querySelector('h1').innerHTML).toMatchInlineSnapshot('"0"'); expect(container.querySelector('h1').innerHTML).toMatchInlineSnapshot('"0"');
container.querySelector('button').click(); container.querySelector('button').click();
@ -159,17 +167,16 @@ describe('render', () => {
const CountingComponent = () => { const CountingComponent = () => {
return <div style="color: red;">Count value is 0.</div>; 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 }) => { it('should render string of style with expression', ({ container }) => {
const Comp = () => { const Comp = () => {
const color = 'red'; const color = 'red';
return <div style={`color: ${color};`}>Count value is 0.</div>; 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'); expect(container.querySelector('div').style.color).toEqual('red');
}); });
@ -177,18 +184,17 @@ describe('render', () => {
const Comp = () => { const Comp = () => {
return <div style={{ color: 'red', display: 'flex' }}>Count value is 0.</div>; 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'); expect(container.querySelector('div').style.color).toEqual('red');
}); });
it('should render object of style with expression', ({ container }) => { it('should render object of style with expression', ({ container }) => {
const Comp = () => { const Comp = () => {
const color = 'red'; const color = 'red';
return <div style={{ color }}>Count value is 0.</div>; return <div style={{ color }}>Count value is 0.</div>;
}; };
render(() => $$runComponent(Comp, {}), container); render(() => <Comp />, container);
expect(container.querySelector('div').style.color).toEqual('red'); 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>; 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'); expect(container.querySelector('div').style.color).toEqual('red');
container.querySelector('div').style.color = 'green'; container.querySelector('div').style.color = 'green';
expect(container.querySelector('div').style.color).toEqual('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>; 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'); expect(container.querySelector('div').style.color).toEqual('red');
container.querySelector('div').style.color = 'green'; container.querySelector('div').style.color = 'green';
expect(container.querySelector('div').style.color).toEqual('green'); expect(container.querySelector('div').style.color).toEqual('green');
@ -221,7 +227,7 @@ describe('render', () => {
return <div className="red">Count value is 0.</div>; return <div className="red">Count value is 0.</div>;
}; };
render(() => $$runComponent(Comp, {}), container); render(() => <Comp />, container);
expect(container.querySelector('div').className).toEqual('red'); expect(container.querySelector('div').className).toEqual('red');
}); });
@ -230,7 +236,7 @@ describe('render', () => {
const color = 'red'; const color = 'red';
return <div className={color}>Count value is 0.</div>; return <div className={color}>Count value is 0.</div>;
}; };
render(() => $$runComponent(Comp, {}), container); render(() => <Comp />, container);
expect(container.querySelector('div').className).toEqual('red'); expect(container.querySelector('div').className).toEqual('red');
}); });
@ -238,7 +244,7 @@ describe('render', () => {
const Comp = () => { const Comp = () => {
return <div className={{ red: true }}>Count value is 0.</div>; return <div className={{ red: true }}>Count value is 0.</div>;
}; };
render(() => $$runComponent(Comp, {}), container); render(() => <Comp />, container);
expect(container.querySelector('div').className).toEqual('red'); expect(container.querySelector('div').className).toEqual('red');
}); });
@ -246,7 +252,7 @@ describe('render', () => {
const Comp = () => { const Comp = () => {
return <div className={['red', 'green']}>Count value is 0.</div>; 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'); expect(container.querySelector('div').className).toEqual('red green');
}); });
@ -255,7 +261,7 @@ describe('render', () => {
const color = reactive('red'); const color = reactive('red');
return <div className={color.get()}>Count value is 0.</div>; return <div className={color.get()}>Count value is 0.</div>;
}; };
render(() => $$runComponent(Comp, {}), container); render(() => <Comp />, container);
expect(container.querySelector('div').className).toEqual('red'); expect(container.querySelector('div').className).toEqual('red');
container.querySelector('div').className = 'green'; container.querySelector('div').className = 'green';
expect(container.querySelector('div').className).toEqual('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>; 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'); expect(container.querySelector('div').className).toEqual('red');
}); });
it('should update class with array', ({ container }) => { it('should update class with array', ({ container }) => {
const Comp = () => { const Comp = () => {
const color = reactive('red'); const color = reactive('red');
return <div className={[color.get(), 'green']} return <div className={[color.get(), 'green']}>Count value is 0.</div>;
>Count value is 0.</div>;
}; };
render(() => $$runComponent(Comp, {}), container); render(() => <Comp />, container);
expect(container.querySelector('div').className).toEqual('red green'); expect(container.querySelector('div').className).toEqual('red green');
}); });
it('should render attribute', ({ container }) => { it('should render attribute', ({ container }) => {
function App() { function App() {
return ( return <div id="test">parallel</div>;
<div id="test">parallel</div>
);
} }
render(() => $$runComponent(App, {}), container);
render(() => <App />, container);
expect(container.querySelector('div').id).toEqual('test'); expect(container.querySelector('div').id).toEqual('test');
}); });
it('should update attribute', ({ container }) => { it('should update attribute', ({ container }) => {
const id = reactive('el'); const id = reactive('el');
function App() {
return ( function App() {
<div id={id.get()}>parallel</div> return <div id={id.get()}>parallel</div>;
);
} }
render(() => $$runComponent(App, {}), container);
render(() => <App />, container);
expect(container.querySelector('div').id).toEqual('el'); expect(container.querySelector('div').id).toEqual('el');
id.set('test'); id.set('test');
expect(container.querySelector('div').id).toEqual('test'); expect(container.querySelector('div').id).toEqual('test');

View File

@ -18,7 +18,7 @@ import { Signal } from './Types';
import { isFunction } from './Utils'; import { isFunction } from './Utils';
import { schedule } from './SetScheduler'; 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 calledGets: RNode<any>[] | null = null;
let sameGetsIndex = 0; // 记录前后两次运行RNode时调用get顺序没有变化的节点 let sameGetsIndex = 0; // 记录前后两次运行RNode时调用get顺序没有变化的节点
@ -50,9 +50,9 @@ export class RNode<T = any> implements Signal<T> {
_value: T; _value: T;
fn?: () => T; fn?: () => T;
private observers: RNode[] | null = null; // 被谁用 observers: RNode[] | null = null; // 被谁用
private sources: RNode[] | null = null; // 使用谁 sources: RNode[] | null = null; // 使用谁
subNodes: RNode[] | null = null; // he RNode that are running within the current RNode.
protected state: State; protected state: State;
isEffect = false; isEffect = false;
@ -93,7 +93,7 @@ export class RNode<T = any> implements Signal<T> {
} }
track() { track() {
if (runningRNode) { if (runningRNode && !isRoot(runningRNode)) {
// 前后两次运行RNode从左到右对比如果调用get的RNode相同就calledGetsIndex加1 // 前后两次运行RNode从左到右对比如果调用get的RNode相同就calledGetsIndex加1
if (!calledGets && runningRNode.sources && runningRNode.sources[sameGetsIndex] == this) { if (!calledGets && runningRNode.sources && runningRNode.sources[sameGetsIndex] == this) {
sameGetsIndex++; sameGetsIndex++;
@ -170,6 +170,14 @@ export class RNode<T = any> implements Signal<T> {
const prevGets = calledGets; const prevGets = calledGets;
const prevGetsIndex = sameGetsIndex; const prevGetsIndex = sameGetsIndex;
if (runningRNode) {
if (runningRNode.subNodes) {
runningRNode.subNodes.push(this);
} else {
runningRNode.subNodes = [this];
}
}
runningRNode = this; runningRNode = this;
calledGets = null as any; calledGets = null as any;
sameGetsIndex = 0; sameGetsIndex = 0;
@ -263,7 +271,7 @@ export class RNode<T = any> implements Signal<T> {
this.state = Fresh; this.state = Fresh;
} }
private removeParentObservers(index: number): void { removeParentObservers(index: number): void {
if (!this.sources) return; if (!this.sources) return;
for (let i = index; i < this.sources.length; i++) { for (let i = index; i < this.sources.length; i++) {
const source: RNode<any> = this.sources[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 */ /** run all non-clean effect nodes */
export function runEffects(): void { export function runEffects(): void {
for (let i = 0; i < Effects.length; i++) { 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 { createComputed as computed, createReactive as reactive, createWatch as watch } from './RNodeCreator';
import { isReactiveObj } from './Utils'; import { isReactiveObj } from './Utils';
import { RNode, untrack, runEffects } from './RNode'; import { RNode, untrack, runEffects, onCleanup, createRoot } from './RNode';
import { setScheduler } from './SetScheduler'; import { setScheduler } from './SetScheduler';
export interface Index { export interface Index {
@ -38,5 +38,7 @@ export {
untrack, untrack,
unwrap, unwrap,
setScheduler, 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);
});
});