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

Svelte 自定义 store 入门:创建符合业务需求的 store

2022-10-053.7k 阅读

Svelte 自定义 store 基础概念

在 Svelte 中,store 是一种管理应用状态的便捷方式。Svelte 内置了一些基础的 store 类型,如 writablereadablederived。然而,在实际的业务场景中,我们常常需要创建符合特定业务需求的自定义 store。

一个 store 在 Svelte 中本质上是一个对象,它至少包含一个 subscribe 方法。这个 subscribe 方法接受一个回调函数作为参数,当 store 的值发生变化时,会调用这个回调函数,从而通知所有订阅者状态的改变。

创建简单的自定义 store

让我们从一个简单的计数器示例开始。假设我们想要一个自定义的计数器 store,它可以增加、减少计数,并且能获取当前的计数值。

<script>
    // 创建自定义 store
    const createCounterStore = () => {
        let count = 0;
        const subscribers = new Set();

        const subscribe = (callback) => {
            subscribers.add(callback);
            callback(count);
            return () => subscribers.delete(callback);
        };

        const increment = () => {
            count++;
            subscribers.forEach(callback => callback(count));
        };

        const decrement = () => {
            count--;
            subscribers.forEach(callback => callback(count));
        };

        return {
            subscribe,
            increment,
            decrement
        };
    };

    const counter = createCounterStore();
    let currentCount;
    const unsubscribe = counter.subscribe((value) => {
        currentCount = value;
    });
</script>

<button on:click={counter.increment}>Increment</button>
<button on:click={counter.decrement}>Decrement</button>
<p>Current count: {currentCount}</p>

<button on:click={unsubscribe}>Unsubscribe</button>

在上述代码中,createCounterStore 函数返回一个对象,这个对象包含 subscribeincrementdecrement 方法。subscribe 方法用于注册回调函数,当状态改变时会调用这些回调。incrementdecrement 方法用于修改计数器的值,并通知所有订阅者。

基于现有 store 创建自定义 store

有时候,我们可能希望基于 Svelte 内置的 store 来创建自定义 store,以复用一些基础功能。例如,我们基于 writable store 创建一个带有数据持久化功能的自定义 store。

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

    const createPersistentStore = (key, initialValue) => {
        const storedValue = localStorage.getItem(key);
        const initial = storedValue!== null? JSON.parse(storedValue) : initialValue;
        const store = writable(initial);

        store.subscribe((value) => {
            localStorage.setItem(key, JSON.stringify(value));
        });

        return store;
    };

    const nameStore = createPersistentStore('user - name', 'Guest');
    let name;
    nameStore.subscribe((value) => {
        name = value;
    });
</script>

<input type="text" bind:value={name}>
<p>Stored name: {name}</p>

在这段代码中,createPersistentStore 函数接收一个键名 key 和初始值 initialValue。它首先从 localStorage 中读取数据,如果存在则作为初始值,否则使用传入的 initialValue。然后基于 writable 创建一个 store,并在 store 的值发生变化时,将新值存储到 localStorage 中。

处理异步操作的自定义 store

在实际业务中,经常会遇到需要处理异步操作的情况。例如,我们创建一个用于获取用户数据的自定义 store,这个 store 会在初始化时异步获取数据,并在数据更新时通知订阅者。

<script>
    const createUserStore = () => {
        let user;
        const subscribers = new Set();

        const subscribe = (callback) => {
            subscribers.add(callback);
            if (user) {
                callback(user);
            }
            return () => subscribers.delete(callback);
        };

        const fetchUser = async () => {
            try {
                const response = await fetch('/api/user');
                const data = await response.json();
                user = data;
                subscribers.forEach(callback => callback(user));
            } catch (error) {
                console.error('Error fetching user:', error);
            }
        };

        fetchUser();

        return {
            subscribe,
            fetchUser
        };
    };

    const userStore = createUserStore();
    let currentUser;
    userStore.subscribe((value) => {
        currentUser = value;
    });
</script>

{#if currentUser}
    <p>Name: {currentUser.name}</p>
    <p>Email: {currentUser.email}</p>
{:else}
    <p>Loading user...</p>
{/if}

<button on:click={userStore.fetchUser}>Refresh User</button>

在上述代码中,createUserStore 函数返回的对象包含 subscribefetchUser 方法。fetchUser 方法用于异步获取用户数据,当数据获取成功后,会更新 user 变量并通知所有订阅者。subscribe 方法会在 store 初始化时,如果已经有数据则立即通知订阅者,否则等待数据获取完成后通知。

自定义 store 的组合与复用

在大型应用中,我们可能会有多个自定义 store,并且希望能够复用和组合它们的功能。例如,我们有一个用于管理用户登录状态的 authStore 和一个用于获取用户信息的 userStore,我们希望创建一个新的 store 来同时管理这两个状态。

<script>
    const createAuthStore = () => {
        let isLoggedIn = false;
        const subscribers = new Set();

        const subscribe = (callback) => {
            subscribers.add(callback);
            callback(isLoggedIn);
            return () => subscribers.delete(callback);
        };

        const login = () => {
            isLoggedIn = true;
            subscribers.forEach(callback => callback(isLoggedIn));
        };

        const logout = () => {
            isLoggedIn = false;
            subscribers.forEach(callback => callback(isLoggedIn));
        };

        return {
            subscribe,
            login,
            logout
        };
    };

    const createUserStore = () => {
        let user;
        const subscribers = new Set();

        const subscribe = (callback) => {
            subscribers.add(callback);
            if (user) {
                callback(user);
            }
            return () => subscribers.delete(callback);
        };

        const fetchUser = async () => {
            try {
                const response = await fetch('/api/user');
                const data = await response.json();
                user = data;
                subscribers.forEach(callback => callback(user));
            } catch (error) {
                console.error('Error fetching user:', error);
            }
        };

        return {
            subscribe,
            fetchUser
        };
    };

    const createCombinedStore = () => {
        const authStore = createAuthStore();
        const userStore = createUserStore();
        let combinedState = {
            isLoggedIn: false,
            user: null
        };
        const subscribers = new Set();

        const subscribe = (callback) => {
            subscribers.add(callback);
            callback(combinedState);
            const unsubscribeAuth = authStore.subscribe((isLoggedIn) => {
                combinedState.isLoggedIn = isLoggedIn;
                if (isLoggedIn) {
                    userStore.fetchUser();
                }
                subscribers.forEach(callback => callback(combinedState));
            });
            const unsubscribeUser = userStore.subscribe((user) => {
                combinedState.user = user;
                subscribers.forEach(callback => callback(combinedState));
            });

            return () => {
                unsubscribeAuth();
                unsubscribeUser();
                subscribers.delete(callback);
            };
        };

        return {
            subscribe
        };
    };

    const combinedStore = createCombinedStore();
    let state;
    combinedStore.subscribe((value) => {
        state = value;
    });
</script>

{#if state.isLoggedIn}
    <p>Welcome, {state.user.name}</p>
    <button on:click={() => authStore.logout()}>Logout</button>
{:else}
    <p>You are not logged in.</p>
    <button on:click={() => authStore.login()}>Login</button>
{/if}

在上述代码中,createCombinedStore 函数组合了 authStoreuserStore 的功能。它创建了一个新的 combinedState 来存储登录状态和用户信息,并通过订阅 authStoreuserStore 的变化来更新 combinedState,同时通知所有订阅者。

自定义 store 与组件的交互

自定义 store 在与 Svelte 组件交互时非常灵活。组件可以订阅 store 的变化并相应地更新 UI,也可以通过调用 store 的方法来修改状态。

<script>
    const createThemeStore = () => {
        let theme = 'light';
        const subscribers = new Set();

        const subscribe = (callback) => {
            subscribers.add(callback);
            callback(theme);
            return () => subscribers.delete(callback);
        };

        const setTheme = (newTheme) => {
            theme = newTheme;
            subscribers.forEach(callback => callback(theme));
        };

        return {
            subscribe,
            setTheme
        };
    };

    const themeStore = createThemeStore();
    let currentTheme;
    themeStore.subscribe((value) => {
        currentTheme = value;
    });
</script>

<style>
   .light {
        background - color: white;
        color: black;
    }

   .dark {
        background - color: black;
        color: white;
    }
</style>

<div class={currentTheme}>
    <p>The current theme is {currentTheme}</p>
    <button on:click={() => themeStore.setTheme('light')}>Light Theme</button>
    <button on:click={() => themeStore.setTheme('dark')}>Dark Theme</button>
</div>

在这个示例中,createThemeStore 创建了一个用于管理应用主题的自定义 store。组件通过订阅 themeStore 来获取当前主题,并在 UI 中显示。同时,通过按钮点击调用 setTheme 方法来修改主题,进而更新 UI。

自定义 store 的类型定义

在使用 TypeScript 与 Svelte 时,为自定义 store 提供准确的类型定义非常重要。这可以帮助我们在开发过程中捕获类型错误,提高代码的健壮性。

import { type Subscriber } from'svelte/store';

// 计数器 store 的类型定义
interface CounterStore {
    subscribe: (callback: Subscriber<number>) => () => void;
    increment: () => void;
    decrement: () => void;
}

const createCounterStore = (): CounterStore => {
    let count = 0;
    const subscribers = new Set<Subscriber<number>>();

    const subscribe = (callback: Subscriber<number>): () => void => {
        subscribers.add(callback);
        callback(count);
        return () => subscribers.delete(callback);
    };

    const increment = (): void => {
        count++;
        subscribers.forEach(callback => callback(count));
    };

    const decrement = (): void => {
        count--;
        subscribers.forEach(callback => callback(count));
    };

    return {
        subscribe,
        increment,
        decrement
    };
};

const counter: CounterStore = createCounterStore();
let currentCount: number;
const unsubscribe = counter.subscribe((value) => {
    currentCount = value;
});

在上述 TypeScript 代码中,我们定义了 CounterStore 接口,它描述了计数器 store 的结构。subscribe 方法接受一个 Subscriber<number> 类型的回调函数,并返回一个取消订阅的函数。incrementdecrement 方法不接受参数,也不返回值。通过这种方式,我们可以确保在使用 counter store 时,类型是正确的。

自定义 store 的性能优化

在应用中使用自定义 store 时,性能优化是一个重要的考虑因素。特别是当有大量订阅者或者频繁的状态更新时,不当的实现可能会导致性能问题。

一种常见的优化方法是使用防抖(Debounce)或节流(Throttle)技术。例如,在一个搜索框的场景中,我们可能不希望每次用户输入都立即触发搜索操作,而是在用户停止输入一段时间后再触发。

<script>
    const createSearchStore = () => {
        let searchTerm = '';
        const subscribers = new Set();
        let debounceTimer;

        const subscribe = (callback) => {
            subscribers.add(callback);
            callback(searchTerm);
            return () => subscribers.delete(callback);
        };

        const setSearchTerm = (newTerm) => {
            clearTimeout(debounceTimer);
            searchTerm = newTerm;
            debounceTimer = setTimeout(() => {
                subscribers.forEach(callback => callback(searchTerm));
            }, 300);
        };

        return {
            subscribe,
            setSearchTerm
        };
    };

    const searchStore = createSearchStore();
    let currentSearch;
    searchStore.subscribe((value) => {
        currentSearch = value;
    });
</script>

<input type="text" bind:value={currentSearch} on:input={(e) => searchStore.setSearchTerm(e.target.value)}>
<p>Search term: {currentSearch}</p>

在上述代码中,setSearchTerm 方法使用 setTimeout 实现了防抖功能。当用户输入新的搜索词时,会清除之前的定时器,并设置一个新的定时器,在 300 毫秒后通知订阅者。这样可以避免在用户快速输入时频繁触发不必要的更新。

另一种优化方式是减少不必要的状态更新通知。例如,在一个列表展示的场景中,如果列表中的某一项数据发生了变化,但整体列表结构没有改变,我们可以通过比较新旧状态来决定是否通知所有订阅者。

<script>
    const createListStore = () => {
        let list = [];
        const subscribers = new Set();

        const subscribe = (callback) => {
            subscribers.add(callback);
            callback(list);
            return () => subscribers.delete(callback);
        };

        const updateListItem = (index, newItem) => {
            const newList = [...list];
            newList[index] = newItem;
            if (!deepEqual(newList, list)) {
                list = newList;
                subscribers.forEach(callback => callback(list));
            }
        };

        const deepEqual = (a, b) => {
            if (Array.isArray(a) && Array.isArray(b)) {
                if (a.length!== b.length) return false;
                for (let i = 0; i < a.length; i++) {
                    if (!deepEqual(a[i], b[i])) return false;
                }
                return true;
            } else if (typeof a === 'object' && typeof b === 'object') {
                const aKeys = Object.keys(a);
                const bKeys = Object.keys(b);
                if (aKeys.length!== bKeys.length) return false;
                for (let key of aKeys) {
                    if (!deepEqual(a[key], b[key])) return false;
                }
                return true;
            } else {
                return a === b;
            }
        };

        return {
            subscribe,
            updateListItem
        };
    };

    const listStore = createListStore();
    let currentList;
    listStore.subscribe((value) => {
        currentList = value;
    });
</script>

{#each currentList as item, index}
    <input type="text" bind:value={item} on:input={(e) => listStore.updateListItem(index, e.target.value)}>
{/each}

在这段代码中,updateListItem 方法会创建一个新的列表,并使用 deepEqual 函数比较新旧列表。只有当列表发生实际变化时,才会通知订阅者,从而减少不必要的 UI 更新。

自定义 store 在复杂业务场景中的应用

在复杂的业务场景中,自定义 store 可以帮助我们更好地组织和管理状态。例如,在一个电商购物车系统中,我们可能需要多个自定义 store 来分别管理商品列表、购物车、用户信息等。

<script>
    // 商品 store
    const createProductStore = () => {
        let products = [];
        const subscribers = new Set();

        const subscribe = (callback) => {
            subscribers.add(callback);
            callback(products);
            return () => subscribers.delete(callback);
        };

        const fetchProducts = async () => {
            try {
                const response = await fetch('/api/products');
                const data = await response.json();
                products = data;
                subscribers.forEach(callback => callback(products));
            } catch (error) {
                console.error('Error fetching products:', error);
            }
        };

        return {
            subscribe,
            fetchProducts
        };
    };

    const productStore = createProductStore();
    let allProducts;
    productStore.subscribe((value) => {
        allProducts = value;
    });

    // 购物车 store
    const createCartStore = () => {
        let cart = [];
        const subscribers = new Set();

        const subscribe = (callback) => {
            subscribers.add(callback);
            callback(cart);
            return () => subscribers.delete(callback);
        };

        const addToCart = (product) => {
            cart.push(product);
            subscribers.forEach(callback => callback(cart));
        };

        const removeFromCart = (index) => {
            cart = cart.filter((_, i) => i!== index);
            subscribers.forEach(callback => callback(cart));
        };

        return {
            subscribe,
            addToCart,
            removeFromCart
        };
    };

    const cartStore = createCartStore();
    let currentCart;
    cartStore.subscribe((value) => {
        currentCart = value;
    });

    // 用户信息 store
    const createUserStore = () => {
        let user;
        const subscribers = new Set();

        const subscribe = (callback) => {
            subscribers.add(callback);
            if (user) {
                callback(user);
            }
            return () => subscribers.delete(callback);
        };

        const fetchUser = async () => {
            try {
                const response = await fetch('/api/user');
                const data = await response.json();
                user = data;
                subscribers.forEach(callback => callback(user));
            } catch (error) {
                console.error('Error fetching user:', error);
            }
        };

        return {
            subscribe,
            fetchUser
        };
    };

    const userStore = createUserStore();
    let currentUser;
    userStore.subscribe((value) => {
        currentUser = value;
    });

    // 初始化数据
    productStore.fetchProducts();
    userStore.fetchUser();
</script>

{#if currentUser}
    <h2>Welcome, {currentUser.name}</h2>
{:else}
    <h2>Please login</h2>
{/if}

{#if allProducts}
    <h2>Products</h2>
    {#each allProducts as product}
        <div>
            <p>{product.name}</p>
            <p>{product.price}</p>
            <button on:click={() => cartStore.addToCart(product)}>Add to Cart</button>
        </div>
    {/each}
{:else}
    <p>Loading products...</p>
{/if}

{#if currentCart.length > 0}
    <h2>Cart</h2>
    {#each currentCart as item, index}
        <div>
            <p>{item.name}</p>
            <p>{item.price}</p>
            <button on:click={() => cartStore.removeFromCart(index)}>Remove from Cart</button>
        </div>
    {/each}
{:else}
    <p>Your cart is empty.</p>
{/if}

在这个电商购物车系统示例中,createProductStore 用于管理商品列表,createCartStore 用于管理购物车,createUserStore 用于管理用户信息。每个 store 都有自己的 subscribe 方法和业务相关的操作方法。组件通过订阅这些 store 来获取最新状态并更新 UI,不同的 store 之间相互协作,共同完成复杂的业务逻辑。

通过以上对 Svelte 自定义 store 的深入探讨,我们了解了如何创建符合业务需求的 store,以及在不同场景下如何优化和应用它们。希望这些知识能帮助你在前端开发中更好地管理应用状态,提升用户体验。