ค้นหาเว็บไซต์

javascript - ทำไม Svelte store ที่สืบทอดมาถึงมีพฤติกรรมต่างกันเมื่อใช้ `$` เทียบกับ `subscribe`


ฉันมีโมเดลข้อมูลที่ฉันไม่สามารถเปลี่ยนแปลงในโครงการนี้ได้ ฉันกำลังพยายามลดความซับซ้อนและทำให้โค้ดตัวอย่างด้านล่างง่ายขึ้น ดังนั้นหวังว่านี่จะยังคงเข้าใจได้กับสิ่งที่ฉันกำลังพยายามสร้างใหม่

สมมติว่าฉันมีร้านสองร้าน ร้านหนึ่งเก็บ "containers" และอีกร้านหนึ่งเก็บ "items" - แต่ละร้านถูกใช้งานอย่างอิสระทั่วทั้งแอปเพื่อวัตถุประสงค์ต่างๆ Container โดยค่าเริ่มต้นจะเก็บแค่ Item ID เป็นบางครั้งฉันต้องการทำให้ข้อมูลไม่เป็นปกติเพื่อใช้งานบนบางหน้า ฉันจึงใช้ร้านแบบ derived และทำให้แต่ละออบเจ็กต์ของ container ไม่เป็นปกติ เปลี่ยนพวกมันเป็น DenormalizedContainers

สมมติว่าฉันมีกรณีการใช้งานที่ฉันต้องการสร้างวัตถุ Item ใหม่ จากนั้นเพิ่มมันเข้าไปใน Container ที่กำหนด ตามที่ฉันเข้าใจ นั่นจะทำให้ store ที่สืบทอดอัพเดตสองครั้ง (ครั้งหนึ่งสำหรับการเปลี่ยนแปลงใน items และอีกครั้งสำหรับการเปลี่ยนแปลงใน containers) และดังนั้นสิ่งใดก็ตามที่ใช้ store ที่สืบทอดนั้นจะอัพเดตสองครั้ง (ขึ้นอยู่กับวิธีการเรียกใช้งาน)

ทำไมพฤติกรรมถึงจะแตกต่างจาก subscribe ไปยัง $?

นอกจากนี้ แม้ว่ามันจะไม่ใช่ปัญหาใหญ่สำหรับผม ผมก็แค่สงสัยว่ามีวิธีแบบ Svelte ดั้งเดิมในการแก้ปัญหานี้โดยไม่ต้องเปลี่ยนโมเดลข้อมูลหรือไม่ (ในขณะที่ยังสามารถใช้ 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>

ผลลัพธ์ :

เมื่อคลิกแต่ละปุ่มเพียงครั้งเดียว นี่คือล็อก:

การโหลดหน้า:

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 Derived Store การอัปเดตแบบอะตอมิก/แบบดีเลย์

อาจจะเป็นปัญหาพื้นฐานหรือไม่?

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

อาจอธิบายใน 3.1/3.2 ที่นี่ งานย่อยเล็ก ๆ จะรวมคำสั่งตอบสนองและนำไปใช้ในตอนท้ายของทิก ในขณะที่ subscribe จัดการอัปเดตแบบเรียลไทม์ หมายเหตุ: การห่อโค้ด subscribe ของฉันใน tick จะทำให้มันทำงานสองครั้งกับข้อมูลเดียวกัน

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

คำตอบ #1

ถ้าคุณดูที่ผลลัพธ์ที่คอมไพล์แล้ว คุณจะเห็นว่าคำสั่งแบบ reactive ถูกกระตุ้นใน 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));
    });
}

บทความที่เกี่ยวข้อง