diff --git a/src/reactivity.ts b/src/reactivity.ts index e3b00e44..737c2219 100644 --- a/src/reactivity.ts +++ b/src/reactivity.ts @@ -1,5 +1,6 @@ import { onWillUnmount } from "./component/lifecycle_hooks"; import { ComponentNode, getCurrent } from "./component/component_node"; +import { batched, Callback } from "./utils"; // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object) const TARGET = Symbol("Target"); @@ -8,7 +9,6 @@ const KEYCHANGES = Symbol("Key changes"); type ObjectKey = string | number | symbol; type Target = object; -type Callback = () => void; export type Reactive = T & { [TARGET]: any; }; @@ -191,30 +191,6 @@ export function reactive(target: T, callback: Callback = () => return reactivesForTarget.get(callback) as Reactive; } -/** - * 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 () => { - // 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; - callback(); - // 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 - await Promise.resolve(); - called = false; - } - }; -} - const batchedRenderFunctions = new WeakMap(); /** * Creates a reactive object that will be observed by the current component. diff --git a/src/utils.ts b/src/utils.ts index 4ccdd2b2..58f8b027 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,29 @@ +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 () => { + // 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; + callback(); + // 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 + await Promise.resolve(); + called = false; + } + }; +} + export class EventBus extends EventTarget { trigger(name: string, payload?: any) { this.dispatchEvent(new CustomEvent(name, { detail: payload })); diff --git a/tests/reactivity.test.ts b/tests/reactivity.test.ts index 235bd96f..4be85ed2 100644 --- a/tests/reactivity.test.ts +++ b/tests/reactivity.test.ts @@ -7,7 +7,8 @@ import { useState, xml, } from "../src"; -import { batched, reactive } from "../src/reactivity"; +import { reactive } from "../src/reactivity"; +import { batched } from "../src/utils"; import { makeDeferred, makeTestFixture, diff --git a/tests/utils.test.ts b/tests/utils.test.ts index b3892fe2..19081358 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -1,4 +1,5 @@ -import { EventBus } from "../src/utils"; +import { batched, EventBus } from "../src/utils"; +import { nextMicroTick } from "./helpers"; describe("event bus behaviour", () => { test("can subscribe and be notified", () => { @@ -33,3 +34,20 @@ describe("event bus behaviour", () => { 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); + }); +});