MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Svelte状态管理最佳实践:readable与writable的协作模式

2022-08-301.9k 阅读

Svelte状态管理的基础理解

在深入探讨readable与writable的协作模式之前,我们先来巩固一下Svelte状态管理的一些基础知识。

Svelte的响应式系统

Svelte的核心优势之一在于其强大的响应式系统。当一个变量发生变化时,Svelte会自动更新与之相关的DOM元素。例如,考虑以下简单的Svelte组件:

<script>
    let count = 0;
    const increment = () => {
        count++;
    };
</script>

<button on:click={increment}>
    Click me! {count}
</button>

在这个例子中,count变量是一个普通的JavaScript变量。当我们点击按钮调用increment函数时,count的值增加,Svelte会自动检测到这个变化并更新按钮中的文本内容。这种自动响应式更新是Svelte状态管理的基石。

writable状态

Svelte提供了writable函数来创建可写的状态。writable返回一个包含subscribesetupdate方法的对象。

<script>
    import { writable } from'svelte/store';
    const count = writable(0);
    const increment = () => {
        count.update(n => n + 1);
    };
</script>

<button on:click={increment}>
    Click me! {$count}
</button>

这里通过writable(0)创建了一个初始值为0的可写状态count。在模板中,我们使用$count语法来订阅这个状态。increment函数使用update方法来修改状态,update接受一个函数,该函数以当前状态值作为参数并返回新的状态值。set方法则可以直接设置状态值,例如count.set(10)会将count的值直接设置为10。

readable状态

readable用于创建可读状态。可读状态一旦创建,外部代码不能直接修改其值,只能订阅它。这在某些场景下非常有用,比如创建基于其他状态派生出来的状态,或者表示一些只读的应用状态。

<script>
    import { readable } from'svelte/store';
    const message = readable('Hello, Svelte!');
</script>

<p>{$message}</p>

在这个例子中,message是一个只读状态,我们只能在组件中订阅并显示它的值,无法直接在组件内修改message的值。

readable与writable的协作场景

派生状态的创建

一个常见的场景是从writable状态派生出readable状态。例如,假设我们有一个表示购物车中商品数量的writable状态,同时我们希望有一个表示购物车总价的readable状态,总价是根据商品数量和商品单价计算得出的。

<script>
    import { writable, readable } from'svelte/store';

    const itemCount = writable(0);
    const itemPrice = 10;

    const totalPrice = readable(0, set => {
        let unsubscribe;
        itemCount.subscribe(count => {
            const price = count * itemPrice;
            set(price);
        }).then(un => {
            unsubscribe = un;
        });

        return () => {
            if (unsubscribe) {
                unsubscribe();
            }
        };
    });
</script>

<div>
    <p>Item Count: {$itemCount}</p>
    <p>Total Price: {$totalPrice}</p>
    <button on:click={() => itemCount.update(c => c + 1)}>Add Item</button>
</div>

在上述代码中,totalPrice是通过readable创建的派生状态。readable的第二个参数是一个回调函数,这个回调函数在状态被订阅时执行。在回调函数中,我们订阅了itemCount状态,当itemCount发生变化时,重新计算总价并通过set方法更新totalPrice的值。返回的清理函数会在状态不再被订阅时执行,用于取消对itemCount的订阅,以避免内存泄漏。

缓存只读数据

有时候,我们需要从后端API获取一些数据,并且这些数据在应用的某个阶段不会改变,我们可以将其存储为readable状态。同时,为了在需要时能够更新这个数据,我们可以结合writable状态来实现。

<script>
    import { writable, readable } from'svelte/store';

    const apiUrl = 'https://example.com/api/data';
    const isFetching = writable(false);
    const cachedData = readable(null, set => {
        const fetchData = async () => {
            isFetching.set(true);
            try {
                const response = await fetch(apiUrl);
                const data = await response.json();
                set(data);
            } catch (error) {
                console.error('Error fetching data:', error);
            } finally {
                isFetching.set(false);
            }
        };

        fetchData();

        return () => {
            // 清理逻辑,这里可以添加取消请求的逻辑,如果使用了fetch-abort等
        };
    });
</script>

{#if $isFetching}
    <p>Loading...</p>
{:else}
    {#if $cachedData}
        <pre>{JSON.stringify($cachedData, null, 2)}</pre>
    {:else}
        <p>No data yet.</p>
    {/if}
{/if}

<button on:click={() => {
    // 模拟需要重新获取数据的操作,比如用户点击刷新按钮
    // 这里可以通过一些逻辑重新触发fetchData
}}>Refresh Data</button>

在这个例子中,cachedDatareadable状态,用于缓存从API获取的数据。isFetchingwritable状态,用于表示数据是否正在获取中。当组件初始化时,cachedData的回调函数会触发数据的获取。如果需要重新获取数据,比如用户点击了刷新按钮,我们可以通过一些逻辑再次触发数据获取操作,同时更新isFetchingcachedData状态。

封装复杂业务逻辑

通过readablewritable的协作,我们可以将复杂的业务逻辑封装在一个模块中,对外提供简洁的状态接口。例如,假设我们正在开发一个简单的任务管理应用,有一个任务列表,并且可以对任务进行筛选。

<script>
    import { writable, readable } from'svelte/store';

    const tasks = writable([
        { id: 1, text: 'Task 1', completed: false },
        { id: 2, text: 'Task 2', completed: true },
        { id: 3, text: 'Task 3', completed: false }
    ]);

    const filter = writable('all');

    const filteredTasks = readable([], set => {
        let unsubscribeTasks, unsubscribeFilter;
        const updateFilteredTasks = () => {
            let filtered;
            const currentTasks = $tasks;
            const currentFilter = $filter;
            if (currentFilter === 'all') {
                filtered = currentTasks;
            } else if (currentFilter === 'completed') {
                filtered = currentTasks.filter(task => task.completed);
            } else if (currentFilter === 'active') {
                filtered = currentTasks.filter(task =>!task.completed);
            }
            set(filtered);
        };

        unsubscribeTasks = tasks.subscribe(updateFilteredTasks);
        unsubscribeFilter = filter.subscribe(updateFilteredTasks);

        updateFilteredTasks();

        return () => {
            if (unsubscribeTasks) {
                unsubscribeTasks();
            }
            if (unsubscribeFilter) {
                unsubscribeFilter();
            }
        };
    });
</script>

<div>
    <input type="radio" bind:group={$filter} value="all"> All
    <input type="radio" bind:group={$filter} value="completed"> Completed
    <input type="radio" bind:group={$filter} value="active"> Active

    <ul>
        {#each $filteredTasks as task}
            <li>{task.text} - {task.completed? 'Completed' : 'Active'}</li>
        {/each}
    </ul>
</div>

在这个例子中,taskswritable状态,表示所有任务。filterwritable状态,用于存储当前的筛选条件。filteredTasksreadable状态,它根据tasksfilter的变化动态计算出筛选后的任务列表。这样,我们将任务筛选的复杂逻辑封装起来,在组件模板中只需要使用简洁的$filteredTasks来展示数据,提高了代码的可维护性和可读性。

协作模式中的注意事项

避免无限循环

在使用readablewritable协作时,要特别注意避免无限循环。例如,如果在readable的回调函数中订阅了writable状态,并且在writable状态的更新逻辑中又触发了readable状态的重新计算,就可能导致无限循环。

<script>
    import { writable, readable } from'svelte/store';

    const a = writable(0);
    const b = readable(0, set => {
        a.subscribe(value => {
            set(value + 1);
            a.set(value + 1); // 这会导致无限循环
        });

        return () => {
            // 清理逻辑
        };
    });
</script>

<p>{$a}</p>
<p>{$b}</p>

在上述代码中,breadable回调函数订阅了a,并且在a变化时更新b的值,同时又更新了a的值,这会导致无限循环。要解决这个问题,需要仔细设计状态更新逻辑,确保状态的变化不会形成循环依赖。

内存管理

readable的回调函数中订阅writable状态时,一定要记得在返回的清理函数中取消订阅,以避免内存泄漏。如前面购物车总价和缓存数据的例子中,我们都在清理函数中取消了对相关writable状态的订阅。如果不这样做,当readable状态不再被订阅时,对writable状态的订阅仍然存在,可能会导致内存泄漏,尤其是在组件频繁创建和销毁的场景下。

数据一致性

确保readablewritable状态之间的数据一致性非常重要。特别是在派生状态的场景下,如果writable状态的更新逻辑发生变化,可能需要相应地调整readable状态的计算逻辑。例如,在购物车总价的例子中,如果商品单价的获取方式发生变化,就需要更新totalPrice的计算逻辑,以保证总价的正确性。

性能优化方面的考虑

批量更新

在Svelte中,频繁地更新状态可能会导致性能问题。当writable状态发生变化时,与之相关的readable状态也会相应更新。如果有多个状态变化,并且这些变化是相互关联的,最好进行批量更新。例如,假设我们有一个包含多个商品信息的购物车,同时有一个totalPricereadable状态和一个totalItemsreadable状态,它们都依赖于购物车商品列表的writable状态。

<script>
    import { writable, readable } from'svelte/store';

    const cartItems = writable([
        { id: 1, name: 'Item 1', price: 10, quantity: 1 },
        { id: 2, name: 'Item 2', price: 20, quantity: 2 }
    ]);

    const totalPrice = readable(0, set => {
        let unsubscribe;
        cartItems.subscribe(items => {
            let price = 0;
            items.forEach(item => {
                price += item.price * item.quantity;
            });
            set(price);
        }).then(un => {
            unsubscribe = un;
        });

        return () => {
            if (unsubscribe) {
                unsubscribe();
            }
        };
    });

    const totalItems = readable(0, set => {
        let unsubscribe;
        cartItems.subscribe(items => {
            let count = 0;
            items.forEach(item => {
                count += item.quantity;
            });
            set(count);
        }).then(un => {
            unsubscribe = un;
        });

        return () => {
            if (unsubscribe) {
                unsubscribe();
            }
        };
    });

    const addItemToCart = () => {
        cartItems.update(items => {
            const newItem = { id: 3, name: 'New Item', price: 15, quantity: 1 };
            return [...items, newItem];
        });
    };
</script>

<div>
    <p>Total Price: {$totalPrice}</p>
    <p>Total Items: {$totalItems}</p>
    <button on:click={addItemToCart}>Add Item to Cart</button>
</div>

在这个例子中,当调用addItemToCart时,cartItems状态更新,会同时触发totalPricetotalItems的更新。如果在更复杂的场景下,有更多依赖于cartItemsreadable状态,频繁的单个更新可能会导致性能问题。我们可以使用Svelte的batch函数来进行批量更新,减少不必要的重新渲染。

<script>
    import { writable, readable, batch } from'svelte/store';

    // 其他代码不变

    const addItemToCart = () => {
        batch(() => {
            cartItems.update(items => {
                const newItem = { id: 3, name: 'New Item', price: 15, quantity: 1 };
                return [...items, newItem];
            });
        });
    };
</script>

通过batch函数,我们将cartItems的更新操作包裹起来,Svelte会在batch结束时一次性处理所有相关状态的更新,从而提高性能。

防抖和节流

在一些场景下,writable状态的频繁变化可能会导致readable状态不必要的频繁更新。例如,假设我们有一个搜索框,输入内容会实时更新一个writable状态,同时有一个readable状态用于根据输入内容从后端搜索数据。如果用户输入速度很快,可能会导致大量不必要的后端请求。这时可以使用防抖或节流技术。

<script>
    import { writable, readable } from'svelte/store';
    import { throttle } from 'lodash';

    const searchInput = writable('');
    const searchResults = readable([], set => {
        let unsubscribe;
        const fetchResults = throttle(async () => {
            const response = await fetch(`https://example.com/api/search?q={$searchInput}`);
            const data = await response.json();
            set(data);
        }, 300);

        unsubscribe = searchInput.subscribe(fetchResults);

        return () => {
            if (unsubscribe) {
                unsubscribe();
            }
            fetchResults.cancel();
        };
    });
</script>

<input type="text" bind:value={$searchInput}>

{#if $searchResults.length > 0}
    <ul>
        {#each $searchResults as result}
            <li>{result}</li>
        {/each}
    </ul>
{/if}

在这个例子中,我们使用了lodashthrottle函数,将fetchResults函数节流,每300毫秒最多执行一次。这样,即使用户快速输入,也不会频繁触发后端请求,从而提高性能和用户体验。同时,在清理函数中,我们取消了throttle函数,以避免潜在的内存泄漏。

与其他状态管理库的比较

与Redux的比较

Redux是一个流行的状态管理库,它采用单向数据流的架构。在Redux中,状态集中存储在一个store中,通过dispatch action来更新状态。与Svelte的readablewritable协作模式相比,Redux的学习曲线相对较陡,因为它涉及到action、reducer等概念。 在Svelte中,writable状态类似于Redux中的可更新状态,但是Svelte的状态更新更加简洁直接,不需要像Redux那样通过dispatch action和编写reducer函数。readable状态在Redux中没有直接对应的概念,但是可以通过selector函数来实现类似的派生状态功能。然而,Svelte的readable状态是基于响应式系统自动更新的,而Redux的selector函数需要手动调用或借助中间件来实现类似的响应式更新。

与MobX的比较

MobX也是一个基于响应式编程的状态管理库。它使用observable和reaction等概念来管理状态和响应状态变化。Svelte的writable状态类似于MobX的observable状态,但是Svelte的语法更加简洁,并且与组件的集成更加紧密。 在MobX中,创建派生状态通常使用computed函数,这与Svelte的readable状态有相似之处。但是,Svelte的readable状态在取消订阅时的清理逻辑更加明确和直观,而MobX在处理复杂的清理逻辑时可能需要更多的代码。

优势总结

Svelte的readablewritable协作模式的优势在于其简洁性和与Svelte组件的紧密集成。它不需要引入过多的额外概念,开发人员可以基于对JavaScript和Svelte响应式系统的基本理解快速上手。同时,这种协作模式能够有效地管理不同类型的状态,无论是可写状态还是派生的只读状态,并且在性能优化方面也提供了一些便捷的方式,使得前端应用的状态管理更加高效和可维护。

实际项目中的应用案例

电商应用中的购物车模块

在一个电商应用的购物车模块中,我们可以充分利用readablewritable的协作模式。假设我们有一个writable状态用于存储购物车中的商品列表,同时有多个readable状态用于计算总价、商品数量等。

<script>
    import { writable, readable } from'svelte/store';

    const cartItems = writable([
        { id: 1, name: 'Product 1', price: 25, quantity: 1 },
        { id: 2, name: 'Product 2', price: 30, quantity: 2 }
    ]);

    const totalPrice = readable(0, set => {
        let unsubscribe;
        cartItems.subscribe(items => {
            let total = 0;
            items.forEach(item => {
                total += item.price * item.quantity;
            });
            set(total);
        }).then(un => {
            unsubscribe = un;
        });

        return () => {
            if (unsubscribe) {
                unsubscribe();
            }
        };
    });

    const totalItems = readable(0, set => {
        let unsubscribe;
        cartItems.subscribe(items => {
            let count = 0;
            items.forEach(item => {
                count += item.quantity;
            });
            set(count);
        }).then(un => {
            unsubscribe = un;
        });

        return () => {
            if (unsubscribe) {
                unsubscribe();
            }
        };
    });

    const addItemToCart = (product) => {
        cartItems.update(items => {
            const existingItem = items.find(item => item.id === product.id);
            if (existingItem) {
                existingItem.quantity++;
                return items;
            } else {
                return [...items, {...product, quantity: 1 }];
            }
        });
    };

    const removeItemFromCart = (productId) => {
        cartItems.update(items => {
            return items.filter(item => item.id!== productId);
        });
    };
</script>

<div>
    <h2>Shopping Cart</h2>
    <ul>
        {#each $cartItems as item}
            <li>
                {item.name} - ${item.price} x {item.quantity}
                <button on:click={() => removeItemFromCart(item.id)}>Remove</button>
            </li>
        {/each}
    </ul>
    <p>Total Items: {$totalItems}</p>
    <p>Total Price: ${$totalPrice}</p>
    <button on:click={() => addItemToCart({ id: 3, name: 'New Product', price: 15 })}>Add New Item</button>
</div>

在这个案例中,cartItemswritable状态,用于存储购物车中的商品。totalPricetotalItemsreadable状态,分别根据cartItems的变化计算总价和商品数量。通过这种协作模式,购物车模块的状态管理变得清晰和易于维护。

单页应用的用户认证状态管理

在一个单页应用中,用户认证状态是一个关键部分。我们可以使用writable状态来表示用户是否已登录,以及用户的相关信息,同时使用readable状态来派生一些与认证相关的只读状态,比如是否显示登录/注销按钮等。

<script>
    import { writable, readable } from'svelte/store';

    const user = writable(null);
    const isLoggedIn = readable(false, set => {
        let unsubscribe;
        user.subscribe(u => {
            set(!!u);
        }).then(un => {
            unsubscribe = un;
        });

        return () => {
            if (unsubscribe) {
                unsubscribe();
            }
        };
    });

    const login = (username, password) => {
        // 模拟登录逻辑,这里可以调用后端API
        if (username === 'admin' && password === 'password') {
            user.set({ username: 'admin', role: 'admin' });
        }
    };

    const logout = () => {
        user.set(null);
    };
</script>

<div>
    {#if $isLoggedIn}
        <p>Welcome, {$user.username}</p>
        <button on:click={logout}>Logout</button>
    {:else}
        <input type="text" placeholder="Username">
        <input type="password" placeholder="Password">
        <button on:click={() => login('admin', 'password')}>Login</button>
    {/if}
</div>

在这个案例中,userwritable状态,存储用户信息。isLoggedInreadable状态,根据user的变化判断用户是否已登录。通过这种方式,我们可以方便地管理用户认证状态,并在组件中根据认证状态进行不同的渲染。

通过以上在实际项目中的应用案例,我们可以看到readablewritable的协作模式在不同场景下都能够有效地管理前端应用的状态,提高代码的可维护性和可扩展性。