This commit is contained in:
Géry Debongnie
2022-06-24 22:55:55 +02:00
parent 0f49048616
commit 4b1a94d990
4 changed files with 5 additions and 128 deletions
+4 -4
View File
@@ -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) : [];
} }
} }
-27
View File
@@ -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");
-59
View File
@@ -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
View File
@@ -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);
});
});