Skip to content

Commit d3fd269

Browse files
committed
[IMP] introduce signals and derived values
1 parent 5187f01 commit d3fd269

22 files changed

+1970
-1433
lines changed

src/common/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,37 @@ export type customDirectives = Record<
22
string,
33
(node: Element, value: string, modifier: string[]) => void
44
>;
5+
6+
// Reactivity system
7+
8+
export enum ComputationState {
9+
EXECUTED = 0,
10+
STALE = 1,
11+
PENDING = 2,
12+
}
13+
export type Computation<T = any> = {
14+
compute?: () => T;
15+
state: ComputationState;
16+
sources: Set<Atom | Derived<any, any>>;
17+
isEager?: boolean;
18+
isDerived?: boolean;
19+
value: T; // for effects, this is the cleanup function
20+
childrenEffect?: Computation[]; // only for effects
21+
} & Opts;
22+
23+
export type Opts = {
24+
name?: string;
25+
};
26+
export type Atom<T = any> = {
27+
value: T;
28+
observers: Set<Computation>;
29+
} & Opts;
30+
31+
export interface Derived<Prev, Next = Prev> extends Atom<Next>, Computation<Next> {}
32+
33+
export type OldValue = any;
34+
35+
export type Getter<V> = () => V | null;
36+
export type Setter<T, V> = (this: T, value: V) => void;
37+
export type MakeGetSetReturn<T, V> = readonly [Getter<V>] | readonly [Getter<V>, Setter<T, V>];
38+
export type MakeGetSet<T, V> = (obj: T) => MakeGetSetReturn<T, V>;

src/runtime/component_node.ts

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import { OwlError } from "../common/owl_error";
2+
import { Atom, Computation, ComputationState } from "../common/types";
13
import type { App, Env } from "./app";
24
import { BDom, VNode } from "./blockdom";
35
import { Component, ComponentConstructor, Props } from "./component";
46
import { fibersInError } from "./error_handling";
5-
import { OwlError } from "../common/owl_error";
67
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
7-
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity";
8+
import { reactive } from "./reactivity";
9+
import { getCurrentComputation, setComputation, withoutReactivity } from "./signals";
810
import { STATUS } from "./status";
9-
import { batched, Callback } from "./utils";
1011

1112
let currentNode: ComponentNode | null = null;
1213

@@ -42,7 +43,6 @@ function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>)
4243
// Integration with reactivity system (useState)
4344
// -----------------------------------------------------------------------------
4445

45-
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
4646
/**
4747
* Creates a reactive object that will be observed by the current component.
4848
* Reading data from the returned object (eg during rendering) will cause the
@@ -54,15 +54,7 @@ const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
5454
* @see reactive
5555
*/
5656
export function useState<T extends object>(state: T): T {
57-
const node = getCurrent();
58-
let render = batchedRenderFunctions.get(node)!;
59-
if (!render) {
60-
render = batched(node.render.bind(node, false));
61-
batchedRenderFunctions.set(node, render);
62-
// manual implementation of onWillDestroy to break cyclic dependency
63-
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
64-
}
65-
return reactive(state, render);
57+
return reactive(state);
6658
}
6759

6860
// -----------------------------------------------------------------------------
@@ -96,6 +88,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
9688
willPatch: LifecycleHook[] = [];
9789
patched: LifecycleHook[] = [];
9890
willDestroy: LifecycleHook[] = [];
91+
signalComputation: Computation;
9992

10093
constructor(
10194
C: ComponentConstructor<P, E>,
@@ -109,23 +102,26 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
109102
this.parent = parent;
110103
this.props = props;
111104
this.parentKey = parentKey;
105+
this.signalComputation = {
106+
value: undefined,
107+
compute: () => this.render(false),
108+
sources: new Set<Atom>(),
109+
state: ComputationState.EXECUTED,
110+
};
112111
const defaultProps = C.defaultProps;
113112
props = Object.assign({}, props);
114113
if (defaultProps) {
115114
applyDefaultProps(props, defaultProps);
116115
}
117116
const env = (parent && parent.childEnv) || app.env;
118117
this.childEnv = env;
119-
for (const key in props) {
120-
const prop = props[key];
121-
if (prop && typeof prop === "object" && targets.has(prop)) {
122-
props[key] = useState(prop);
123-
}
124-
}
118+
const previousComputation = getCurrentComputation();
119+
setComputation(this.signalComputation);
125120
this.component = new C(props, env, this);
126121
const ctx = Object.assign(Object.create(this.component), { this: this.component });
127122
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
128123
this.component.setup();
124+
setComputation(previousComputation);
129125
currentNode = null;
130126
}
131127

@@ -142,7 +138,11 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
142138
}
143139
const component = this.component;
144140
try {
145-
await Promise.all(this.willStart.map((f) => f.call(component)));
141+
let prom: Promise<any[]>;
142+
withoutReactivity(() => {
143+
prom = Promise.all(this.willStart.map((f) => f.call(component)));
144+
});
145+
await prom!;
146146
} catch (e) {
147147
this.app.handleError({ node: this, error: e });
148148
return;
@@ -257,16 +257,11 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
257257
applyDefaultProps(props, defaultProps);
258258
}
259259

260-
currentNode = this;
261-
for (const key in props) {
262-
const prop = props[key];
263-
if (prop && typeof prop === "object" && targets.has(prop)) {
264-
props[key] = useState(prop);
265-
}
266-
}
267-
currentNode = null;
268-
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
269-
await prom;
260+
let prom: Promise<any[]>;
261+
withoutReactivity(() => {
262+
prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
263+
});
264+
await prom!;
270265
if (fiber !== this.fiber) {
271266
return;
272267
}
@@ -383,9 +378,4 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
383378
get name(): string {
384379
return this.component.constructor.name;
385380
}
386-
387-
get subscriptions(): ReturnType<typeof getSubscriptions> {
388-
const render = batchedRenderFunctions.get(this);
389-
return render ? getSubscriptions(render) : [];
390-
}
391381
}

src/runtime/fibers.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ComponentNode } from "./component_node";
33
import { fibersInError } from "./error_handling";
44
import { OwlError } from "../common/owl_error";
55
import { STATUS } from "./status";
6+
import { runWithComputation } from "./signals";
67

78
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
89
let current = node.fiber;
@@ -133,12 +134,15 @@ export class Fiber {
133134
const node = this.node;
134135
const root = this.root;
135136
if (root) {
136-
try {
137-
(this.bdom as any) = true;
138-
this.bdom = node.renderFn();
139-
} catch (e) {
140-
node.app.handleError({ node, error: e });
141-
}
137+
// todo: should use updateComputation somewhere else.
138+
runWithComputation(node.signalComputation, () => {
139+
try {
140+
(this.bdom as any) = true;
141+
this.bdom = node.renderFn();
142+
} catch (e) {
143+
node.app.handleError({ node, error: e });
144+
}
145+
});
142146
root.setCounter(root.counter - 1);
143147
}
144148
}

src/runtime/hooks.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Env } from "./app";
22
import { getCurrent } from "./component_node";
33
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
4+
import { runWithComputation } from "./signals";
45
import { inOwnerDocument } from "./utils";
56

67
// -----------------------------------------------------------------------------
@@ -86,22 +87,31 @@ export function useEffect<T extends unknown[]>(
8687
effect: Effect<T>,
8788
computeDependencies: () => [...T] = () => [NaN] as never
8889
) {
90+
const context = getCurrent().component.__owl__.signalComputation;
91+
8992
let cleanup: (() => void) | void;
90-
let dependencies: T;
93+
94+
let dependencies: any;
95+
const runEffect = () =>
96+
runWithComputation(context, () => {
97+
cleanup = effect(...dependencies);
98+
});
99+
const computeDependenciesWithContext = () => runWithComputation(context, computeDependencies);
100+
91101
onMounted(() => {
92-
dependencies = computeDependencies();
93-
cleanup = effect(...dependencies);
102+
dependencies = computeDependenciesWithContext();
103+
runEffect();
94104
});
95105

96106
onPatched(() => {
97-
const newDeps = computeDependencies();
98-
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
107+
const newDeps = computeDependenciesWithContext();
108+
const shouldReapply = newDeps.some((val: any, i: number) => val !== dependencies[i]);
99109
if (shouldReapply) {
100110
dependencies = newDeps;
101111
if (cleanup) {
102112
cleanup();
103113
}
104-
cleanup = effect(...dependencies);
114+
runEffect();
105115
}
106116
});
107117

src/runtime/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ export const blockDom = {
3232
html,
3333
comment,
3434
};
35-
3635
export { App, mount } from "./app";
3736
export { xml } from "./template_set";
3837
export { Component } from "./component";
3938
export type { ComponentConstructor } from "./component";
4039
export { useComponent, useState } from "./component_node";
4140
export { status } from "./status";
4241
export { reactive, markRaw, toRaw } from "./reactivity";
42+
export { effect, withoutReactivity, derived } from "./signals";
4343
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
4444
export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils";
4545
export {

0 commit comments

Comments
 (0)