mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] reactivity: Context replacement
Aim to replace the abstraction "Context" from OWL 1 with the new primitives "atom" and "useState": - notification is done only after a batch of modifications. - observers are notified at most once for a batch. - an observer of type component is notified (and rerendered) only if it does not have an ancestor that has to be notified for the same batch of operations (anywhere in the web of references!). - notification of components is done on all levels "simultaneously". Co-authored-by: Aaron Bohy <aab@odoo.com> Co-authored-by: Géry Debongnie <ged@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
This commit is contained in:
committed by
Géry Debongnie
parent
d47dcf6be2
commit
d60a5a414e
+149
-70
@@ -1,52 +1,67 @@
|
||||
import { getCurrent } from "./component/component_node";
|
||||
import { ComponentNode, getCurrent } from "./component/component_node";
|
||||
import { onWillUnmount } from "./component/lifecycle_hooks";
|
||||
|
||||
type Observer = ComponentNode | Function;
|
||||
type Atom = any; // proxy linked to a unique observer and source
|
||||
type Source = any; // trackable that is not an atom
|
||||
type Observer = () => void;
|
||||
type Keys = Set<any>;
|
||||
|
||||
const sourceAtoms: WeakMap<Source, Set<Atom>> = new WeakMap();
|
||||
const observerAtoms: WeakMap<Observer, Set<Atom>> = new WeakMap();
|
||||
const sourceAtoms: WeakMap<Source, Map<any, ObserverSet>> = new WeakMap();
|
||||
const observerSourceAtom: WeakMap<Observer, Map<any, Atom>> = new WeakMap();
|
||||
|
||||
const SOURCE = Symbol("source");
|
||||
const OBSERVER = Symbol("observer");
|
||||
const KEYS = Symbol("keys");
|
||||
const ROOT = Symbol("root");
|
||||
|
||||
export function atom(source: any, observer: Observer) {
|
||||
if (isTrackable(source) && observerAtoms.has(observer)) {
|
||||
if (isTrackable(source) && observerSourceAtom.has(observer)) {
|
||||
source = source[SOURCE] || source;
|
||||
const oldAtom = getObserverSourceAtom(observer, source);
|
||||
const oldAtom = observerSourceAtom.get(observer)!.get(source);
|
||||
if (oldAtom) {
|
||||
return oldAtom;
|
||||
}
|
||||
registerSource(source);
|
||||
return createAtom(source, observer);
|
||||
if (!sourceAtoms.get(source)) {
|
||||
sourceAtoms.set(source, new Map([[ROOT, new ObserverSet()]]));
|
||||
}
|
||||
const newAtom = createAtom(source, observer);
|
||||
observerSourceAtom.get(observer)!.set(source, newAtom);
|
||||
sourceAtoms.get(source)!.get(ROOT)!.add(newAtom);
|
||||
return newAtom;
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
function createAtom(source: Source, observer: Observer): Atom {
|
||||
const keys: Keys = new Set();
|
||||
const keys: Set<any> = new Set();
|
||||
let self: Atom;
|
||||
const newAtom: Atom = new Proxy(source as any, {
|
||||
set(target: any, key: string, value: any): boolean {
|
||||
if (!(key in target)) {
|
||||
target[key] = value;
|
||||
notifySourceObservers(source);
|
||||
notify(sourceAtoms.get(source)!.get(ROOT)!);
|
||||
return true;
|
||||
}
|
||||
const current = target[key];
|
||||
if (current !== value) {
|
||||
target[key] = value;
|
||||
notifySourceKeyOBservers(source, key);
|
||||
const observerSet = sourceAtoms.get(source)!.get(key);
|
||||
if (observerSet) {
|
||||
notify(observerSet);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
deleteProperty(target: any, key: string): boolean {
|
||||
if (key in target) {
|
||||
delete target[key];
|
||||
notifySourceObservers(source);
|
||||
deleteKeyFromKeys(source, key);
|
||||
// notify source observers
|
||||
notify(sourceAtoms.get(source)!.get(ROOT)!);
|
||||
const atoms = sourceAtoms.get(source)!;
|
||||
if (atoms.has(key)) {
|
||||
// clear source-key observers
|
||||
atoms.get(key)!.deleteKey(key);
|
||||
atoms.delete(key);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -60,39 +75,24 @@ function createAtom(source: Source, observer: Observer): Atom {
|
||||
return keys;
|
||||
default:
|
||||
const value = target[key];
|
||||
keys.add(key);
|
||||
// register observer to source-key
|
||||
if (!keys.has(key) && observerSourceAtom.has(observer)) {
|
||||
const atoms = sourceAtoms.get(source)!;
|
||||
if (!atoms.has(key)) {
|
||||
atoms.set(key, new ObserverSet());
|
||||
}
|
||||
atoms.get(key)!.add(self);
|
||||
keys.add(key);
|
||||
}
|
||||
//
|
||||
return atom(value, observer);
|
||||
}
|
||||
},
|
||||
});
|
||||
getObserverAtoms(observer).add(newAtom);
|
||||
getSourceAtoms(source).add(newAtom);
|
||||
self = newAtom;
|
||||
return newAtom;
|
||||
}
|
||||
|
||||
function deleteKeyFromKeys(source: Source, key: any) {
|
||||
for (const atom of getSourceAtoms(source)) {
|
||||
atom[KEYS].delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function getObserverAtoms(observer: Observer): Set<Atom> {
|
||||
return observerAtoms.get(observer)!;
|
||||
}
|
||||
|
||||
function getObserverSourceAtom(observer: Observer, source: Source): Atom | null {
|
||||
for (const atom of getObserverAtoms(observer)) {
|
||||
if (atom[SOURCE] === source) {
|
||||
return atom;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSourceAtoms(source: Source): Set<Atom> {
|
||||
return sourceAtoms.get(source)!;
|
||||
}
|
||||
|
||||
function isTrackable(value: any): boolean {
|
||||
return (
|
||||
value !== null &&
|
||||
@@ -102,40 +102,22 @@ function isTrackable(value: any): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function notifySourceKeyOBservers(source: Source, key: any) {
|
||||
for (const atom of getSourceAtoms(source)) {
|
||||
if (atom[KEYS].has(key)) {
|
||||
atom[OBSERVER]();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function notifySourceObservers(source: Source) {
|
||||
for (const atom of getSourceAtoms(source)) {
|
||||
atom[OBSERVER]();
|
||||
}
|
||||
}
|
||||
|
||||
export function registerObserver(observer: Observer) {
|
||||
if (!observerAtoms.get(observer)) {
|
||||
observerAtoms.set(observer, new Set());
|
||||
if (!observerSourceAtom.get(observer)) {
|
||||
observerSourceAtom.set(observer, new Map());
|
||||
}
|
||||
return unregisterObserver.bind(null, observer);
|
||||
}
|
||||
|
||||
function registerSource(source: Source) {
|
||||
if (!sourceAtoms.get(source)) {
|
||||
sourceAtoms.set(source, new Set());
|
||||
}
|
||||
}
|
||||
|
||||
function unregisterObserver(observer: Observer) {
|
||||
for (const atom of getObserverAtoms(observer)) {
|
||||
const source = atom[SOURCE];
|
||||
const sourceAtoms = getSourceAtoms(source);
|
||||
sourceAtoms.delete(atom);
|
||||
for (const [source, atom] of observerSourceAtom.get(observer)!) {
|
||||
const atoms = sourceAtoms.get(source)!;
|
||||
atoms.get(ROOT)!.delete(atom);
|
||||
for (const key of atom[KEYS]) {
|
||||
atoms.get(key)!.delete(atom);
|
||||
}
|
||||
}
|
||||
observerAtoms.delete(observer);
|
||||
observerSourceAtom.delete(observer);
|
||||
}
|
||||
|
||||
export function useState(state: any): Atom {
|
||||
@@ -143,8 +125,105 @@ export function useState(state: any): Atom {
|
||||
throw new Error("Argument is not trackable");
|
||||
}
|
||||
const node = getCurrent()!;
|
||||
const observer = () => node.render();
|
||||
const unregisterObserver = registerObserver(observer);
|
||||
const unregisterObserver = registerObserver(node);
|
||||
onWillUnmount(() => unregisterObserver());
|
||||
return atom(state, observer);
|
||||
return atom(state, node);
|
||||
}
|
||||
|
||||
class ObserverSet {
|
||||
nodeAntichain: Antichain = new Antichain();
|
||||
callbackSet: Set<Atom> = new Set();
|
||||
add(atom: Atom) {
|
||||
if (atom[OBSERVER] instanceof ComponentNode) {
|
||||
this.nodeAntichain.add(atom);
|
||||
} else {
|
||||
this.callbackSet.add(atom);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
delete(atom: Atom) {
|
||||
if (atom[OBSERVER] instanceof ComponentNode) {
|
||||
return this.nodeAntichain.delete(atom);
|
||||
} else {
|
||||
return this.callbackSet.delete(atom);
|
||||
}
|
||||
}
|
||||
deleteKey(key: any) {
|
||||
for (const atom of this.nodeAntichain) {
|
||||
atom[KEYS].delete(key);
|
||||
}
|
||||
for (const atom of this.callbackSet) {
|
||||
atom[KEYS].delete(key);
|
||||
}
|
||||
}
|
||||
union(other: ObserverSet) {
|
||||
for (const atom of other.nodeAntichain) {
|
||||
this.nodeAntichain.add(atom);
|
||||
}
|
||||
for (const callback of other.callbackSet) {
|
||||
this.callbackSet.add(callback);
|
||||
}
|
||||
}
|
||||
notify() {
|
||||
for (const atom of this.nodeAntichain) {
|
||||
atom[OBSERVER].render();
|
||||
}
|
||||
for (const atom of this.callbackSet) {
|
||||
atom[OBSERVER]();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need to optimize this!
|
||||
function isLessOrEqual(node1: ComponentNode, node2: ComponentNode) {
|
||||
let current: any = node1;
|
||||
if (current.level <= node2.level) {
|
||||
return false;
|
||||
}
|
||||
do {
|
||||
if (current === node2) {
|
||||
return true;
|
||||
}
|
||||
current = current.parent;
|
||||
} while (current);
|
||||
return false;
|
||||
}
|
||||
|
||||
// set of atoms linked to observers of type ComponentNode
|
||||
class Antichain extends Set<Atom> {
|
||||
level?: number;
|
||||
add(atom: Atom) {
|
||||
const node = atom[OBSERVER];
|
||||
if (this.level === node.level || this.level === undefined) {
|
||||
super.add(atom);
|
||||
this.level = node.level;
|
||||
return this;
|
||||
}
|
||||
let willAdd = false;
|
||||
for (const atom2 of this) {
|
||||
const node2 = atom2[OBSERVER];
|
||||
if (!willAdd && isLessOrEqual(node, node2)) {
|
||||
return this;
|
||||
} else if (isLessOrEqual(node2, node)) {
|
||||
super.delete(atom2);
|
||||
willAdd = true;
|
||||
}
|
||||
}
|
||||
super.add(atom);
|
||||
this.level = NaN;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
let toNotify: ObserverSet | null = null;
|
||||
async function notify(observers: ObserverSet) {
|
||||
if (toNotify) {
|
||||
toNotify.union(observers);
|
||||
return;
|
||||
}
|
||||
toNotify = new ObserverSet();
|
||||
toNotify.union(observers);
|
||||
await Promise.resolve();
|
||||
toNotify.notify();
|
||||
toNotify = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user