Svelte状态管理对比:writable与derived的选择策略
Svelte 状态管理基础
在深入探讨 writable
与 derived
的选择策略之前,我们先来回顾一下 Svelte 状态管理的基础知识。
Svelte 是一种新型的前端框架,其状态管理机制简洁而强大。Svelte 应用的状态可以被定义为存储数据的变量,当这些状态发生变化时,Svelte 会自动更新 DOM 以反映这些变化。
在 Svelte 中,状态管理主要依赖于响应式系统。通过 $:
前缀,我们可以创建响应式声明。例如:
<script>
let count = 0;
$: doubled = count * 2;
</script>
<button on:click={() => count++}>Increment</button>
<p>{count} doubled is {doubled}</p>
在这个例子中,doubled
是一个响应式变量,它会随着 count
的变化而自动更新。每当 count
增加,doubled
也会相应更新,并且 DOM 中的文本也会实时变化。
writable 状态
定义与基本用法
writable
是 Svelte 提供的用于创建可写状态的函数。它返回一个包含 subscribe
、set
和 update
方法的对象。
以下是一个简单的 writable
示例:
<script>
import { writable } from'svelte/store';
const count = writable(0);
let value;
const unsubscribe = count.subscribe((val) => {
value = val;
});
</script>
<button on:click={() => count.update((n) => n + 1)}>Increment</button>
<p>The current value is {value}</p>
在上述代码中,我们使用 writable
创建了一个初始值为 0
的 count
状态。subscribe
方法用于注册一个回调函数,当状态值发生变化时,该回调函数会被调用。这里我们将最新的值赋给 value
变量,以便在模板中显示。update
方法则用于基于当前值更新状态,set
方法则可以直接设置状态值。例如:
<script>
import { writable } from'svelte/store';
const count = writable(0);
let value;
count.subscribe((val) => {
value = val;
});
setTimeout(() => {
count.set(10);
}, 2000);
</script>
<p>The current value is {value}</p>
在这个例子中,两秒后,count
的值会通过 set
方法被直接设置为 10
。
应用场景
- 用户输入状态:当你需要处理用户输入,例如文本框的值、滑块的位置等,
writable
是一个很好的选择。
<script>
import { writable } from'svelte/store';
const inputValue = writable('');
let value;
inputValue.subscribe((val) => {
value = val;
});
</script>
<input type="text" bind:value={$inputValue}>
<p>You entered: {value}</p>
这里 inputValue
用于存储用户在文本框中输入的值,通过 bind:value
指令与输入框进行双向绑定,当用户输入时,inputValue
的值会自动更新。
- 应用的主要数据状态:如果你的应用有一些主要的数据,例如用户信息、购物车商品列表等,
writable
可以用来管理这些状态。
<script>
import { writable } from'svelte/store';
const user = writable({ name: 'John', age: 30 });
let userData;
user.subscribe((data) => {
userData = data;
});
</script>
<p>{userData.name} is {userData.age} years old.</p>
在这个例子中,user
存储了用户的基本信息,应用的其他部分可以通过订阅 user
来获取最新的用户数据。
derived 状态
定义与基本用法
derived
用于创建一个基于其他状态派生出来的状态。它接收两个参数:一个是依赖的状态(可以是一个或多个 writable
状态),另一个是回调函数。回调函数会在依赖状态变化时被调用,并返回派生状态的值。
以下是一个简单的 derived
示例:
<script>
import { writable, derived } from'svelte/store';
const count = writable(0);
const doubled = derived(count, ($count) => {
return $count * 2;
});
let doubleValue;
doubled.subscribe((val) => {
doubleValue = val;
});
</script>
<button on:click={() => count.update((n) => n + 1)}>Increment</button>
<p>The doubled value is {doubleValue}</p>
在上述代码中,doubled
是基于 count
派生出来的状态。每当 count
变化时,derived
中的回调函数会被调用,重新计算 doubled
的值。
多依赖派生状态
derived
也可以依赖多个状态。例如:
<script>
import { writable, derived } from'svelte/store';
const width = writable(100);
const height = writable(200);
const area = derived([width, height], ([$width, $height]) => {
return $width * $height;
});
let areaValue;
area.subscribe((val) => {
areaValue = val;
});
</script>
<input type="number" bind:value={$width}>
<input type="number" bind:value={$height}>
<p>The area is {areaValue}</p>
在这个例子中,area
状态依赖于 width
和 height
两个状态。当 width
或 height
任何一个发生变化时,area
会重新计算。
应用场景
- 计算属性:与 Vue 或 React 中的计算属性类似,当你需要根据其他状态计算出一个新的值时,
derived
非常有用。例如在一个电商应用中,计算购物车商品的总价:
<script>
import { writable, derived } from'svelte/store';
const cartItems = writable([
{ name: 'Product 1', price: 10, quantity: 2 },
{ name: 'Product 2', price: 20, quantity: 1 }
]);
const totalPrice = derived(cartItems, ($cartItems) => {
let total = 0;
for (const item of $cartItems) {
total += item.price * item.quantity;
}
return total;
});
let totalValue;
totalPrice.subscribe((val) => {
totalValue = val;
});
</script>
<p>The total price of the cart is {totalValue}</p>
这里 totalPrice
是基于 cartItems
派生出来的状态,用于计算购物车中所有商品的总价。
- 复杂状态转换:当你需要对现有状态进行复杂的转换或处理时,
derived
可以帮助你将这些逻辑封装起来。例如,将一个日期对象格式化为特定的字符串格式:
<script>
import { writable, derived } from'svelte/store';
const date = writable(new Date());
const formattedDate = derived(date, ($date) => {
const options = { year: 'numeric', month: 'long', day: 'numeric' };
return $date.toLocaleDateString('en - US', options);
});
let formattedValue;
formattedDate.subscribe((val) => {
formattedValue = val;
});
</script>
<p>The formatted date is {formattedValue}</p>
在这个例子中,formattedDate
是基于 date
派生出来的状态,将日期对象格式化为特定的字符串。
writable 与 derived 的选择策略
从数据来源角度选择
- 直接用户输入或外部数据:如果状态直接来源于用户输入(如表单数据)或外部 API 响应,
writable
是首选。因为这些数据是应用状态的原始来源,需要能够被直接修改和更新。 例如,在一个登录表单中:
<script>
import { writable } from'svelte/store';
const username = writable('');
const password = writable('');
const handleSubmit = () => {
// 这里可以处理登录逻辑,使用 $username 和 $password
};
</script>
<input type="text" bind:value={$username}>
<input type="password" bind:value={$password}>
<button on:click={handleSubmit}>Login</button>
username
和 password
直接接收用户输入,使用 writable
可以方便地管理这些输入值。
- 基于现有状态计算得出:如果状态是基于其他状态计算得出的,
derived
是更好的选择。这样可以将计算逻辑封装起来,并且只有当依赖的状态变化时才会重新计算。 例如,在一个显示文章阅读时间的功能中:
<script>
import { writable, derived } from'svelte/store';
const articleContent = writable('This is a sample article. It has some text.');
const readingTime = derived(articleContent, ($articleContent) => {
const words = $articleContent.split(' ').length;
return words / 200; // 假设每分钟阅读200字
});
let timeValue;
readingTime.subscribe((val) => {
timeValue = val;
});
</script>
<p>Estimated reading time: {timeValue} minutes</p>
readingTime
是基于 articleContent
派生出来的,通过 derived
可以确保只有当文章内容发生变化时才重新计算阅读时间。
从性能角度选择
- 频繁更新的状态:如果状态会频繁更新,并且更新操作比较简单,
writable
可能更适合。因为derived
在每次依赖状态更新时都需要执行回调函数,可能会带来一定的性能开销。 例如,在一个实时显示当前时间的功能中:
<script>
import { writable } from'svelte/store';
const currentTime = writable(new Date());
setInterval(() => {
currentTime.set(new Date());
}, 1000);
let timeValue;
currentTime.subscribe((val) => {
timeValue = val;
});
</script>
<p>The current time is {timeValue.toLocaleTimeString()}</p>
这里 currentTime
使用 writable
直接更新,避免了 derived
可能带来的不必要计算。
- 复杂计算且不频繁更新的状态:对于需要进行复杂计算且依赖状态不频繁更新的情况,
derived
可以提高性能。因为它会缓存计算结果,只有当依赖状态变化时才重新计算。 例如,在一个图形绘制应用中,计算一个复杂图形的面积,这个图形的属性可能不会频繁变化:
<script>
import { writable, derived } from'svelte/store';
const shapeProps = writable({ side1: 10, side2: 20, angle: 45 });
const shapeArea = derived(shapeProps, ($shapeProps) => {
// 复杂的面积计算逻辑
const { side1, side2, angle } = $shapeProps;
const radian = angle * (Math.PI / 180);
return 0.5 * side1 * side2 * Math.sin(radian);
});
let areaValue;
shapeArea.subscribe((val) => {
areaValue = val;
});
</script>
<p>The area of the shape is {areaValue}</p>
这里 shapeArea
依赖于 shapeProps
,由于 shapeProps
不会频繁变化,使用 derived
可以避免每次都重新计算面积。
从代码结构和可维护性角度选择
- 简单状态管理:对于简单的状态管理,
writable
可以使代码更简洁明了。当状态之间没有复杂的依赖关系时,直接使用writable
可以减少代码量。 例如,在一个简单的计数器应用中:
<script>
import { writable } from'svelte/store';
const count = writable(0);
</script>
<button on:click={() => count.update((n) => n + 1)}>Increment</button>
<p>The count is {count}</p>
这种情况下,writable
直接管理计数器状态,代码简单易懂。
- 复杂状态依赖关系:当状态之间存在复杂的依赖关系时,
derived
可以将这些关系清晰地表达出来,提高代码的可维护性。 例如,在一个电商应用的购物车功能中,购物车总价、折扣价、最终价格等状态可能存在复杂的依赖关系:
<script>
import { writable, derived } from'svelte/store';
const cartItems = writable([
{ name: 'Product 1', price: 10, quantity: 2 },
{ name: 'Product 2', price: 20, quantity: 1 }
]);
const discount = writable(0.1);
const totalPrice = derived(cartItems, ($cartItems) => {
let total = 0;
for (const item of $cartItems) {
total += item.price * item.quantity;
}
return total;
});
const discountedPrice = derived([totalPrice, discount], ([$totalPrice, $discount]) => {
return $totalPrice * (1 - $discount);
});
let totalValue, discountedValue;
totalPrice.subscribe((val) => {
totalValue = val;
});
discountedPrice.subscribe((val) => {
discountedValue = val;
});
</script>
<p>Total price: {totalValue}</p>
<p>Discounted price: {discountedValue}</p>
这里通过 derived
清晰地表达了 totalPrice
和 discountedPrice
与其他状态的依赖关系,使得代码结构更清晰,易于维护。
从副作用处理角度选择
- 需要直接处理副作用的状态:如果状态在更新时需要执行一些副作用操作,如发送 API 请求、更新本地存储等,
writable
更便于处理。因为writable
的set
和update
方法可以直接在更新状态时添加副作用逻辑。 例如,在一个待办事项应用中,当添加新的待办事项时,需要将新事项保存到本地存储:
<script>
import { writable } from'svelte/store';
const todos = writable([]);
const newTodo = writable('');
const addTodo = () => {
if ($newTodo) {
todos.update((t) => {
const newTodos = [...t, $newTodo];
localStorage.setItem('todos', JSON.stringify(newTodos));
return newTodos;
});
newTodo.set('');
}
};
</script>
<input type="text" bind:value={$newTodo}>
<button on:click={addTodo}>Add Todo</button>
<ul>
{#each $todos as todo}
<li>{todo}</li>
{/each}
</ul>
这里在 todos
更新时,通过 update
方法直接将新的待办事项列表保存到本地存储。
- 不涉及副作用的派生状态:
derived
主要用于计算派生状态,通常不应该在其中处理副作用。因为derived
的回调函数应该是纯函数,只根据输入返回计算结果。如果在derived
中处理副作用,可能会导致意外的行为。 例如,以下是错误的在derived
中处理副作用的示例:
<script>
import { writable, derived } from'svelte/store';
const count = writable(0);
const doubled = derived(count, ($count) => {
console.log('Side effect in derived'); // 不应该在这里处理副作用
return $count * 2;
});
</script>
在这个例子中,console.log
是一个副作用操作,不适合放在 derived
的回调函数中。如果需要在派生状态变化时执行副作用,可以在订阅 derived
状态时进行处理:
<script>
import { writable, derived } from'svelte/store';
const count = writable(0);
const doubled = derived(count, ($count) => {
return $count * 2;
});
doubled.subscribe((val) => {
console.log('Side effect when doubled value changes:', val);
});
</script>
实际项目中的案例分析
案例一:社交媒体应用
在一个社交媒体应用中,有用户的粉丝数量、关注数量以及关注者与粉丝的比例等状态。
- 粉丝数量和关注数量:这两个状态直接来源于后端 API 的数据获取,并且可能会随着用户的操作(如关注/取消关注其他用户)而直接更新。因此,适合使用
writable
来管理。
<script>
import { writable } from'svelte/store';
const followersCount = writable(0);
const followingCount = writable(0);
// 模拟从 API 获取数据
setTimeout(() => {
followersCount.set(100);
followingCount.set(50);
}, 2000);
</script>
<p>Followers: {followersCount}</p>
<p>Following: {followingCount}</p>
- 关注者与粉丝的比例:这个状态是基于粉丝数量和关注数量计算得出的,适合使用
derived
。
<script>
import { writable, derived } from'svelte/store';
const followersCount = writable(0);
const followingCount = writable(0);
const ratio = derived([followersCount, followingCount], ([$followersCount, $followingCount]) => {
if ($followingCount === 0) {
return 0;
}
return $followersCount / $followingCount;
});
let ratioValue;
ratio.subscribe((val) => {
ratioValue = val;
});
</script>
<p>Followers to following ratio: {ratioValue}</p>
通过这样的选择,使得状态管理清晰,数据来源和计算逻辑明确。
案例二:项目管理应用
在一个项目管理应用中,有项目列表、每个项目的任务列表以及所有任务的总数等状态。
- 项目列表和任务列表:项目列表可能会通过用户创建、删除项目等操作直接更新,任务列表也会随着用户添加、完成任务等操作直接变化,因此都适合使用
writable
。
<script>
import { writable } from'svelte/store';
const projects = writable([
{ id: 1, name: 'Project 1', tasks: [] },
{ id: 2, name: 'Project 2', tasks: [] }
]);
const addTask = (projectId, task) => {
projects.update((ps) => {
return ps.map((p) => {
if (p.id === projectId) {
return {
...p,
tasks: [...p.tasks, task]
};
}
return p;
});
});
};
</script>
{#each $projects as project}
<h2>{project.name}</h2>
<ul>
{#each project.tasks as task}
<li>{task}</li>
{/each}
</ul>
<input type="text" placeholder="Add task">
<button on:click={() => addTask(project.id, 'New task')}>Add Task</button>
{/each}
- 所有任务的总数:这个状态是基于所有项目的任务列表计算得出的,适合使用
derived
。
<script>
import { writable, derived } from'svelte/store';
const projects = writable([
{ id: 1, name: 'Project 1', tasks: [] },
{ id: 2, name: 'Project 2', tasks: [] }
]);
const totalTasks = derived(projects, ($projects) => {
let count = 0;
for (const project of $projects) {
count += project.tasks.length;
}
return count;
});
let totalCount;
totalTasks.subscribe((val) => {
totalCount = val;
});
</script>
<p>Total number of tasks: {totalCount}</p>
在这个案例中,通过合理选择 writable
和 derived
,使得应用的状态管理符合数据的性质和应用的逻辑。
总结常见误区
- 在 derived 中处理副作用:如前文所述,
derived
的回调函数应该是纯函数,只进行计算操作。在其中处理副作用会导致意外行为,并且难以调试。副作用应该在订阅derived
状态时或者在writable
的更新方法中处理。 - 过度使用 derived:虽然
derived
很强大,但不应该在所有情况下都使用它。对于简单的、直接更新的状态,writable
更简单高效。过度使用derived
可能会增加不必要的计算开销和代码复杂性。 - 忽略 writable 的更新方法:
writable
不仅可以通过set
直接设置值,update
方法在基于当前值进行更新时非常有用。例如在需要根据当前状态进行复杂更新操作时,update
可以确保更新逻辑的原子性和一致性。 - 未正确处理 derived 的多依赖情况:当
derived
依赖多个状态时,要确保回调函数能够正确处理所有依赖状态的变化。在回调函数中要使用解构来获取每个依赖状态的值,并且要考虑到所有可能的状态变化情况,以避免计算错误。
通过清晰理解 writable
和 derived
的特性、应用场景以及选择策略,开发者可以在 Svelte 项目中构建出高效、可维护的状态管理体系。在实际项目中,要根据具体的业务需求和数据特点,灵活选择合适的状态管理方式,以实现最佳的开发效果。