javascript - מדוע לחנות נגזרת ב-Svelte יש התנהגות שונה כאשר משתמשים ב-`$` לעומת `subscribe`


יש לי מודל נתונים שאני לא יכול לשנות בפרויקט הזה. אני מנסה לפשט ולהפחית את הקוד לדוגמה שלמטה, אז אני מקווה שזה עדיין מובן עם מה שאני מנסה לשחזר.

נניח שיש לי שתי חנויות. חנות אחת מחזיקה "מיכלים" והחנות השנייה מחזיקה "פריטים" - כל חנות משמשת בעצמה לאורך כל האפליקציה למטרות שונות. המיכל מחזיק כברירת מחדל רק מזהי פריטים. לפעמים, אני רוצה להפוך את הנתונים לפחות מנורמליים לשימוש בכמה דפים. אני משתמש בחנות נגזרת ומפצל את כל אובייקטי המיכל, והופך אותם ל-DenormalizedContainers.

נניח שיש לי מקרה שימוש שבו אני רוצה ליצור אובייקט חדש מסוג Item, ואז להוסיף אותו למיכל נתון. למיטב הבנתי, זה יגרום לחנות הנגזרת לעדכן פעמיים (פעם אחת עבור השינוי ב-items ופעם נוספת עבור שינוי ב-containers), ולפיכך כל דבר שמשתמש באותה חנות נגזרת יעדכן פעמיים (תלוי איך היא נקראת).

מדוע ההתנהגות תהיה שונה מ-subscribe ל-$?

גם, למרות שזה לא בעיה גדולה בשבילי, אני סתם סקרן אם יש איזו דרך ילידית ב-Svelte לעקוף את זה בלי לשנות את מודל הנתונים (בעוד שעדיין אפשר להשתמש ב-subscribe API)?

<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>

תוצאות :

בלחיצה על כל כפתור פעם אחת, הנה היומנים:

טעינת דף:

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

הוסף מיכל:

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

הוסף פריט:

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

הוסף את שניהם:

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

העדכונים שמתבצעים דרך $ הם מה שאני רוצה כל הזמן, אבל מסיבה כלשהי ממשק ה-API הידני subscribe מראה שני עדכונים. יש לי כמה מקומות מחוץ לקבצי .svelte, שם אצטרך להשתמש בממשק ה-API subscribe.

הפניות :

דומה לזה, אך לא מסביר למה ה-$ מתנהג אחרת מה-subscribe.

עדכונים אטומיים/מושהים של חנות נגזרת ב-Svelte

אולי הבעיה הבסיסית?

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

ייתכן שהוסבר ב-3.1/3.2 כאן. משימות מיקרו אוספות הצהרות ריאקטיביות ומפעילות אותן בסוף ה-tick. בעוד ש-subscribe מטפל בעדכונים בזמן אמת. שים לב: עטיפת הקוד שלי של subscribe ב-tick רק גורמת לו לרוץ פעמיים עם אותם נתונים.

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

תשובה #1

אם תסתכל על הפלט המהודק, תוכל לראות שההצהרות הריאקטיביות מופעלות על update, שזה אמור לקרות רק פעם אחת לכל לולאת אירועים.

    $$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`);
        }
    };

אתה יכול ליצור את השיטה שלך לאריזת עדכונים בלולאת אירועים. לדוגמה, באמצעות מיקרו-משימות:

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`
    )
);

דוגמת REPL

אין דרך לבטל משימות מיקרו שוב, כך שהקודמות מבוטלות דרך מפתח. דרכים אחרות להבטיח שרק המשימה האחרונה תתבצע עשויות להיות אלגנטיות יותר.

עם setTimeout ניתן לעשות זאת גם כן, ובכך ניתן לבטל את הקודם, אך ההשהייה המינימלית תהיה ארוכה יותר:

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