mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
wip
This commit is contained in:
@@ -41,7 +41,7 @@ function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>)
|
|||||||
// Integration with reactivity system (useState)
|
// Integration with reactivity system (useState)
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
const renderFunctions = new WeakMap<ComponentNode, Callback>();
|
||||||
/**
|
/**
|
||||||
* Creates a reactive object that will be observed by the current component.
|
* Creates a reactive object that will be observed by the current component.
|
||||||
* Reading data from the returned object (eg during rendering) will cause the
|
* Reading data from the returned object (eg during rendering) will cause the
|
||||||
@@ -54,10 +54,10 @@ const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
|||||||
*/
|
*/
|
||||||
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
|
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
|
||||||
const node = getCurrent();
|
const node = getCurrent();
|
||||||
let render = batchedRenderFunctions.get(node)!;
|
let render = renderFunctions.get(node)!;
|
||||||
if (!render) {
|
if (!render) {
|
||||||
render = node.render.bind(node, false);
|
render = node.render.bind(node, false);
|
||||||
batchedRenderFunctions.set(node, render);
|
renderFunctions.set(node, render);
|
||||||
// manual implementation of onWillDestroy to break cyclic dependency
|
// manual implementation of onWillDestroy to break cyclic dependency
|
||||||
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
|
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
|
||||||
}
|
}
|
||||||
@@ -366,7 +366,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
|||||||
}
|
}
|
||||||
|
|
||||||
get subscriptions(): ReturnType<typeof getSubscriptions> {
|
get subscriptions(): ReturnType<typeof getSubscriptions> {
|
||||||
const render = batchedRenderFunctions.get(this);
|
const render = renderFunctions.get(this);
|
||||||
return render ? getSubscriptions(render) : [];
|
return render ? getSubscriptions(render) : [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,5 @@
|
|||||||
export type Callback = () => void;
|
export type Callback = () => void;
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a batched version of a callback so that all calls to it in the same
|
|
||||||
* microtick will only call the original callback once.
|
|
||||||
*
|
|
||||||
* @param callback the callback to batch
|
|
||||||
* @returns a batched version of the original callback
|
|
||||||
*/
|
|
||||||
export function batched(callback: Callback): Callback {
|
|
||||||
let called = false;
|
|
||||||
return async () => {
|
|
||||||
debugger;
|
|
||||||
// This await blocks all calls to the callback here, then releases them sequentially
|
|
||||||
// in the next microtick. This line decides the granularity of the batch.
|
|
||||||
await Promise.resolve();
|
|
||||||
if (!called) {
|
|
||||||
called = true;
|
|
||||||
// wait for all calls in this microtick to fall through before resetting "called"
|
|
||||||
// so that only the first call to the batched function calls the original callback.
|
|
||||||
// Schedule this before calling the callback so that calls to the batched function
|
|
||||||
// within the callback will proceed only after resetting called to false, and have
|
|
||||||
// a chance to execute the callback again
|
|
||||||
Promise.resolve().then(() => (called = false));
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateTarget(target: HTMLElement) {
|
export function validateTarget(target: HTMLElement) {
|
||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
throw new Error("Cannot mount component: the target is not a valid DOM element");
|
throw new Error("Cannot mount component: the target is not a valid DOM element");
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
toRaw,
|
toRaw,
|
||||||
} from "../src";
|
} from "../src";
|
||||||
import { reactive, Reactive } from "../src/runtime/reactivity";
|
import { reactive, Reactive } from "../src/runtime/reactivity";
|
||||||
import { batched } from "../src/runtime/utils";
|
|
||||||
import {
|
import {
|
||||||
makeDeferred,
|
makeDeferred,
|
||||||
makeTestFixture,
|
makeTestFixture,
|
||||||
@@ -190,64 +189,6 @@ describe("Reactivity", () => {
|
|||||||
expect(n).toBe(2);
|
expect(n).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("batched: callback is called after batch of operation", async () => {
|
|
||||||
let n = 0;
|
|
||||||
const state = createReactive(
|
|
||||||
{ a: 1, b: 2 },
|
|
||||||
batched(() => n++)
|
|
||||||
);
|
|
||||||
state.a = 2;
|
|
||||||
expect(n).toBe(0);
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(0); // key has not be read yet
|
|
||||||
state.a = state.a + 5; // key is read and then modified
|
|
||||||
expect(n).toBe(0);
|
|
||||||
state.b = state.b + 5; // key is read and then modified
|
|
||||||
expect(n).toBe(0);
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(1); // two operations but only one notification
|
|
||||||
});
|
|
||||||
|
|
||||||
test("batched: modifying the reactive in the callback doesn't break reactivity", async () => {
|
|
||||||
let n = 0;
|
|
||||||
let obj = { a: 1 };
|
|
||||||
const state = createReactive(
|
|
||||||
obj,
|
|
||||||
batched(() => {
|
|
||||||
state.a; // subscribe to a
|
|
||||||
state.a = 2;
|
|
||||||
n++;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(n).toBe(0);
|
|
||||||
state.a = 2;
|
|
||||||
expect(n).toBe(0);
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(0); // key has not be read yet
|
|
||||||
state.a = state.a + 5; // key is read and then modified
|
|
||||||
expect(n).toBe(0);
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(1);
|
|
||||||
// the write a = 2 inside the batched callback triggered another notification, wait for it
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(2);
|
|
||||||
// Should now be stable as we're writing the same value again
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(2);
|
|
||||||
|
|
||||||
// Do it again to check it's not broken
|
|
||||||
state.a = state.a + 5; // key is read and then modified
|
|
||||||
expect(n).toBe(2);
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(3);
|
|
||||||
// the write a = 2 inside the batched callback triggered another notification, wait for it
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(4);
|
|
||||||
// Should now be stable as we're writing the same value again
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("setting property to same value does not trigger callback", async () => {
|
test("setting property to same value does not trigger callback", async () => {
|
||||||
let n = 0;
|
let n = 0;
|
||||||
const state = createReactive({ a: 1 }, () => n++);
|
const state = createReactive({ a: 1 }, () => n++);
|
||||||
|
|||||||
+1
-38
@@ -1,4 +1,4 @@
|
|||||||
import { batched, EventBus } from "../src/runtime/utils";
|
import { EventBus } from "../src/runtime/utils";
|
||||||
import { nextMicroTick } from "./helpers";
|
import { nextMicroTick } from "./helpers";
|
||||||
|
|
||||||
describe("event bus behaviour", () => {
|
describe("event bus behaviour", () => {
|
||||||
@@ -34,40 +34,3 @@ describe("event bus behaviour", () => {
|
|||||||
bus.trigger("event", "hello world");
|
bus.trigger("event", "hello world");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("batched", () => {
|
|
||||||
test("callback is called only once after operations", async () => {
|
|
||||||
let n = 0;
|
|
||||||
let fn = batched(() => n++);
|
|
||||||
|
|
||||||
expect(n).toBe(0);
|
|
||||||
fn();
|
|
||||||
fn();
|
|
||||||
expect(n).toBe(0);
|
|
||||||
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(1);
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("calling batched function from within the callback is not treated as part of the original batch", async () => {
|
|
||||||
let n = 0;
|
|
||||||
let fn = batched(() => {
|
|
||||||
n++;
|
|
||||||
if (n === 1) {
|
|
||||||
fn();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(n).toBe(0);
|
|
||||||
fn();
|
|
||||||
expect(n).toBe(0);
|
|
||||||
await nextMicroTick(); // First batch
|
|
||||||
expect(n).toBe(1);
|
|
||||||
await nextMicroTick(); // Second batch initiated from within the callback
|
|
||||||
expect(n).toBe(2);
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(n).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user