Svelte 自定义 store 入门:创建符合业务需求的 store
Svelte 自定义 store 基础概念
在 Svelte 中,store 是一种管理应用状态的便捷方式。Svelte 内置了一些基础的 store 类型,如 writable
、readable
和 derived
。然而,在实际的业务场景中,我们常常需要创建符合特定业务需求的自定义 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
函数返回一个对象,这个对象包含 subscribe
、increment
和 decrement
方法。subscribe
方法用于注册回调函数,当状态改变时会调用这些回调。increment
和 decrement
方法用于修改计数器的值,并通知所有订阅者。
基于现有 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
函数返回的对象包含 subscribe
和 fetchUser
方法。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
函数组合了 authStore
和 userStore
的功能。它创建了一个新的 combinedState
来存储登录状态和用户信息,并通过订阅 authStore
和 userStore
的变化来更新 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>
类型的回调函数,并返回一个取消订阅的函数。increment
和 decrement
方法不接受参数,也不返回值。通过这种方式,我们可以确保在使用 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,以及在不同场景下如何优化和应用它们。希望这些知识能帮助你在前端开发中更好地管理应用状态,提升用户体验。