`);
-
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);
+ });
+});