Vyhledávání na webu

javascript - Proč má odvozený Svelte store odlišné chování při použití `$` vs `subscribe`


Mám datový model, který v tomto projektu nemohu změnit. Snažím se zjednodušit a zmenšit níže uvedený ukázkový kód, takže doufám, že to bude stále dávat smysl s tím, co se snažím znovu vytvořit.

Řekněme, že mám dva obchody. Jeden obchod obsahuje „kontejnery“ a druhý obchod obsahuje „položky“ - každý obchod je v aplikaci používán nezávisle pro různé účely. Kontejner ve výchozím nastavení obsahuje pouze ID položek. Občas chci data denormalizovat pro použití na některých stránkách. Používám odvozený obchod a denormalizuji každý z objektů kontejneru, čímž je proměňuji na DenormalizedContainers.

Řekněme, že mám případ použití, kde chci vytvořit nový objekt Item a pak ho přidat do daného Containeru. Pokud tomu dobře rozumím, způsobí to, že odvozený store se aktualizuje dvakrát (jednou pro změnu v items a podruhé pro změnu v containers), a tím pádem se cokoli, co používá tento odvozený store, aktualizuje dvakrát (v závislosti na tom, jak je voláno).

Proč by se chování lišilo od subscribe k $?

Také, i když to pro mě není velký problém, jen mě zajímá, jestli existuje nějaký nativní způsob ve Svelte, jak to obejít, aniž bych měnil datový model (a přitom stále mohl používat API subscribe?

<script lang="ts">
    import { derived, writable } from "svelte/store";

    type Container = {
        id: string;
        itemIds: string[];
    };

    type Item = {
        id: string;
        name: string;
    };

    type DenormalizedContainer = {
        id: string;
        items: Item[];
    };

    const containers = writable<Container[]>([]);
    const items = writable<Item[]>([]);

    const denormalizedContainers = derived(
        [containers, items],
        ([$containers, $items]): DenormalizedContainer[] => {
            return $containers.map(({ id, itemIds }) => {
                const denormalizedContainer = {
                    id,
                    items: itemIds.map((id) =>
                        $items.find((item) => {
                            item.id === id;
                        })
                    ),
                };
                return denormalizedContainer;
            });
        }
    );

    function handleAddContainer() {
        const value = Math.random() * 1000;
        const newContainer: Container = { id: `new-container-${value}`, itemIds: [] };
        containers.set([...$containers, newContainer]);
    }
    function handleAddItem() {
        const value = Math.random() * 1000;
        const newItem: Item = { id: `new-id-${value}`, name: `new-name-${value}` };
        items.set([...$items, newItem]);
    }
    function handleAddBoth() {
        const value = Math.random() * 1000;
        const newItem: Item = { id: `new-id-${value}`, name: `new-name-${value}` };
        const newContainer: Container = { id: `new-container-${value}`, itemIds: [newItem.id] };
        items.set([...$items, newItem]);
        containers.set([...$containers, newContainer]);
    }

    $: console.log(`$: There are ${$containers.length} containers`);
    $: console.log(`$: There are ${$items.length} items`);
    $: console.log(`$: There are ${$denormalizedContainers.length} denormalized containers`);

    denormalizedContainers.subscribe((newValue) =>
        console.log(
            `Subscribe: There are ${$denormalizedContainers.length} denormalized containers`
        )
    );
</script>

<button class="block" on:click={() => handleAddContainer()}>Add container</button>
<button class="block" on:click={() => handleAddItem()}>Add item</button>
<button class="block" on:click={() => handleAddBoth()}>Add container and item</button>

Výsledky :

Po jednom kliknutí na každé tlačítko jsou zde záznamy:

Načtení stránky:

Subscribe: There are 0 denormalized containers
$: There are 0 containers
$: There are 0 items
$: There are 0 denormalized containers

Přidat kontejner:

Subscribe: There are 1 denormalized containers
$: There are 1 containers
$: There are 1 denormalized containers

Přidat položku:

Subscribe: There are 1 denormalized containers
$: There are 1 items
$: There are 1 denormalized containers

Přidejte oba:

Subscribe: There are 1 denormalized containers
Subscribe: There are 2 denormalized containers
$: There are 2 containers
$: There are 2 items
$: There are 2 denormalized containers

Aktualizace zpracovávané přes $ jsou to, co bych chtěl mít neustále, ale z nějakého důvodu ukazuje manuální API subscribe dvě aktualizace. Mám několik míst mimo soubory .svelte, kde bych potřeboval použít API subscribe.

Reference:

Podobné tomu, ale nevysvětluje, proč se $ chová jinak než subscribe.

Atomické/zpožděné aktualizace odvozeného úložiště Svelte

Možná základní problém?

https://github.com/sveltejs/svelte/issues/6730

Možná vysvětleno v 3.1/3.2 zde. Mikro-úkoly seskupují reaktivní příkazy a aplikují je na konci „ticku“. Zatímco subscribe zpracovává aktualizace v reálném čase. Poznámka: Zabalit můj kód subscribe do tick způsobí, že poběží dvakrát se stejnými daty.

https://dev.to/isaachagoel/svelte-reactivity-gotchas-solutions-if-you-re-using-svelte-in-production-you-should-read-this-3oj3

Odpověď č. 1

Pokud se podíváte na zkompilovaný výstup, můžete vidět, že reaktivní příkazy jsou spuštěny při update, což by mělo nastat pouze jednou za smyčku událostí.

    $$self.$$.update = () => {
        if ($$self.$$.dirty & /*$containers*/ 256) {
            $: console.log(`$: There are ${$containers.length} containers`);
        }

        if ($$self.$$.dirty & /*$items*/ 128) {
            $: console.log(`$: There are ${$items.length} items`);
        }

        if ($$self.$$.dirty & /*$denormalizedContainers*/ 64) {
            $: console.log(`$: There are ${$denormalizedContainers.length} denormalized containers`);
        }
    };

Můžete si vytvořit vlastní metodu seskupování aktualizací v událostní smyčce. Např. pomocí mikrotasků:

function debouncedSubscribe(store, callback) {
    let key;
    return store.subscribe(value => {
        key = {};
        const currentKey = key;
        queueMicrotask(() => {
            if (key == currentKey)
                callback(value);
        });
    });
}

debouncedSubscribe(denormalizedContainers, value =>
    console.log(
        `Subscribe: There are ${value.length} denormalized containers`
    )
);

Příklad REPL

Není žádný způsob, jak znovu zrušit mikro úkoly, takže ty staré jsou neplatné prostřednictvím klíče. Jiné způsoby, jak zajistit, že bude proveden pouze poslední úkol, mohou být elegantnější.

S setTimeout to lze také provést, a tím lze předchozí zrušit, ale minimální zpoždění bude větší:

function debouncedSubscribe(store, callback) {
    let timeout;
    return store.subscribe(value => {
        clearTimeout(timeout);
        timeout = setTimeout(() => callback(value));
    });
}

Související články