Svelte derived store 详解:从多个 store 中派生新状态
Svelte 中的 Store 基础概念
在深入探讨 Svelte 的 derived
store 之前,我们先来回顾一下 Svelte 中 store 的基本概念。在 Svelte 里,store 是一种用来管理状态的机制。它通过一种简单而有效的方式,让我们可以在组件之间共享数据,并且当数据发生变化时,能够自动更新依赖它的组件。
一个基本的 Svelte store 通常是一个包含 subscribe
方法的对象。这个 subscribe
方法接受一个回调函数作为参数,当 store 的值发生变化时,这个回调函数就会被调用,从而通知订阅者数据的更新。以下是一个简单的示例:
<script>
const myStore = {
value: 0,
subscribe: (callback) => {
const unsubscribe = () => {
// 这里实际上什么也不做,因为我们没有在内部维护订阅者列表
// 但在更复杂的场景下,可能需要移除订阅者
};
callback(myStore.value);
return unsubscribe;
}
};
let value;
const unsubscribe = myStore.subscribe((newValue) => {
value = newValue;
console.log('The value has changed to:', value);
});
// 模拟数据变化
setTimeout(() => {
myStore.value = 1;
myStore.subscribe((newValue) => {
value = newValue;
console.log('The value has changed to:', value);
});
}, 2000);
</script>
<p>The current value is: {value}</p>
在上述代码中,我们手动创建了一个简单的 myStore
。它有一个初始值 0
,并且定义了 subscribe
方法。通过调用 subscribe
方法并传入一个回调函数,我们订阅了这个 store 的变化。当我们在 setTimeout
中改变 myStore.value
时,再次调用 subscribe
方法,回调函数会被触发,从而更新 value
并在控制台打印出新的值。
Svelte 也提供了一些内置的 store 创建函数,比如 writable
。writable
创建的 store 不仅有 subscribe
方法,还提供了 set
和 update
方法,让我们更方便地更新 store 的值。
<script>
import { writable } from'svelte/store';
const count = writable(0);
let currentCount;
const unsubscribe = count.subscribe((newCount) => {
currentCount = newCount;
console.log('Count updated:', currentCount);
});
setTimeout(() => {
count.set(5);
}, 2000);
setTimeout(() => {
count.update((prevCount) => prevCount + 1);
}, 4000);
</script>
<p>The current count is: {currentCount}</p>
在这个例子中,我们使用 writable
创建了一个 count
store,初始值为 0
。通过 subscribe
方法订阅变化,然后使用 set
方法直接设置新值为 5
,并使用 update
方法基于前一个值增加 1
。每次值的变化都会触发订阅的回调函数,更新 currentCount
并在控制台打印。
为什么需要 derived store
在实际的前端开发中,我们经常会遇到这样的场景:某个状态并不是独立存在的,而是依赖于其他一个或多个状态的计算结果。例如,在一个电商应用中,购物车的总价是依赖于每个商品的价格和数量来计算的。如果每个商品的价格和数量分别由不同的 store 管理,那么购物车总价就需要从这些 store 中派生出来。
如果不使用 derived
store,我们可能需要手动监听每个相关 store 的变化,并在变化发生时重新计算派生状态。这样做不仅代码繁琐,而且容易出错,特别是当依赖的 store 数量较多时。
derived
store 则提供了一种简洁而优雅的解决方案。它允许我们基于一个或多个 existing store 派生新的状态,并且当任何一个依赖的 store 发生变化时,自动重新计算并更新派生状态。这使得我们的代码更加模块化和易于维护,同时也提高了代码的可读性。
derived store 的基本使用
在 Svelte 中,derived
函数用于创建派生 store。它接受两个参数:第一个参数可以是单个 store 或者一个包含多个 store 的数组;第二个参数是一个回调函数,用于计算派生状态。
基于单个 store 派生
首先,让我们看一个基于单个 store 派生新状态的简单例子。假设我们有一个表示当前温度(单位为摄氏度)的 store,我们想要派生出一个表示华氏温度的 store。
<script>
import { writable, derived } from'svelte/store';
const celsius = writable(20);
const fahrenheit = derived(celsius, ($celsius) => {
return ($celsius * 1.8) + 32;
});
let currentFahrenheit;
const unsubscribe = fahrenheit.subscribe((newFahrenheit) => {
currentFahrenheit = newFahrenheit;
console.log('Fahrenheit updated:', currentFahrenheit);
});
setTimeout(() => {
celsius.set(25);
}, 2000);
</script>
<p>The current temperature in Fahrenheit is: {currentFahrenheit}</p>
在上述代码中,我们首先使用 writable
创建了一个 celsius
store,初始值为 20
。然后,通过 derived
函数基于 celsius
store 派生出 fahrenheit
store。derived
的回调函数接受 $celsius
作为参数,$
符号是 Svelte 中用于访问 store 当前值的语法糖。在回调函数中,我们根据摄氏度到华氏度的转换公式计算并返回华氏温度值。
当我们订阅 fahrenheit
store 并在 setTimeout
中改变 celsius
store 的值时,fahrenheit
store 会自动重新计算并更新,从而触发订阅的回调函数,更新 currentFahrenheit
并在控制台打印新的值。
基于多个 store 派生
更常见的情况是基于多个 store 派生新状态。比如,我们有一个表示商品价格和数量的 store,要计算商品的总价。
<script>
import { writable, derived } from'svelte/store';
const price = writable(10);
const quantity = writable(2);
const total = derived([price, quantity], ([$price, $quantity]) => {
return $price * $quantity;
});
let currentTotal;
const unsubscribe = total.subscribe((newTotal) => {
currentTotal = newTotal;
console.log('Total updated:', currentTotal);
});
setTimeout(() => {
price.set(15);
}, 2000);
setTimeout(() => {
quantity.set(3);
}, 4000);
</script>
<p>The total price is: {currentTotal}</p>
在这个例子中,我们创建了 price
和 quantity
两个 writable
store,分别表示商品价格和数量。然后通过 derived
函数,将这两个 store 作为数组传递给第一个参数。回调函数接受解构后的 $price
和 $quantity
,计算并返回总价。
当 price
或 quantity
任何一个 store 的值发生变化时(通过 setTimeout
模拟),total
store 会自动重新计算并更新,触发订阅回调,更新 currentTotal
并在控制台打印新的总价。
派生状态的初始化值
derived
函数还接受一个可选的第三个参数,用于设置派生状态的初始值。这在某些情况下非常有用,特别是当依赖的 store 初始值可能导致计算错误或者需要一个默认的派生值时。
<script>
import { writable, derived } from'svelte/store';
const numerator = writable(null);
const denominator = writable(null);
const quotient = derived([numerator, denominator], ([$numerator, $denominator]) => {
if ($numerator === null || $denominator === null || $denominator === 0) {
return null;
}
return $numerator / $denominator;
}, null);
let currentQuotient;
const unsubscribe = quotient.subscribe((newQuotient) => {
currentQuotient = newQuotient;
console.log('Quotient updated:', currentQuotient);
});
setTimeout(() => {
numerator.set(10);
denominator.set(2);
}, 2000);
setTimeout(() => {
denominator.set(0);
}, 4000);
</script>
<p>The quotient is: {currentQuotient}</p>
在上述代码中,numerator
和 denominator
初始值都为 null
。如果没有设置初始值,在刚开始订阅 quotient
store 时,由于 numerator
和 denominator
为 null
,计算会出现问题。通过设置初始值为 null
,我们确保了在刚开始时 currentQuotient
也为 null
,并且在计算过程中,如果 numerator
或 denominator
为 null
或者 denominator
为 0
,也返回 null
,避免了潜在的错误。
处理异步操作
在实际应用中,派生状态的计算可能涉及到异步操作,比如从 API 获取数据并结合本地 store 进行计算。Svelte 的 derived
同样可以很好地处理这种情况。
假设我们有一个表示用户 ID 的 store,并且我们需要根据这个 ID 从 API 获取用户信息,并计算用户的全名(假设 API 返回的用户对象包含 firstName
和 lastName
)。
<script>
import { writable, derived } from'svelte/store';
const userId = writable(1);
const userFullName = derived(userId, async ($userId) => {
const response = await fetch(`https://example.com/api/users/${$userId}`);
const user = await response.json();
return `${user.firstName} ${user.lastName}`;
});
let currentFullName;
const unsubscribe = userFullName.subscribe((newFullName) => {
currentFullName = newFullName;
console.log('User full name updated:', currentFullName);
});
setTimeout(() => {
userId.set(2);
}, 2000);
</script>
<p>The user's full name is: {currentFullName}</p>
在这个例子中,derived
的回调函数是一个异步函数。当 userId
store 的值发生变化时,会触发异步操作,从 API 获取用户信息并计算全名。derived
会自动处理异步返回的值,当 Promise 解决时,更新派生状态 userFullName
,从而触发订阅回调,更新 currentFullName
并在控制台打印新的全名。
性能优化
虽然 derived
store 非常方便,但在处理大量数据或者频繁变化的 store 时,可能会出现性能问题。因为每次依赖的 store 变化时,derived
的回调函数都会被执行。
为了优化性能,我们可以采取一些措施。一种方法是使用 throttle
或 debounce
技术。例如,如果有一个频繁变化的 store,我们可以使用 debounce
来延迟计算派生状态,避免不必要的频繁计算。
<script>
import { writable, derived } from'svelte/store';
import { debounce } from 'lodash';
const inputValue = writable('');
const debouncedValue = derived(inputValue, ($inputValue) => {
return $inputValue;
}, '');
const debouncedCallback = debounce((newValue) => {
console.log('Debounced value updated:', newValue);
}, 300);
let currentDebouncedValue;
const unsubscribe = debouncedValue.subscribe((newDebouncedValue) => {
currentDebouncedValue = newDebouncedValue;
debouncedCallback(newDebouncedValue);
});
setTimeout(() => {
inputValue.set('new value 1');
}, 1000);
setTimeout(() => {
inputValue.set('new value 2');
}, 1500);
setTimeout(() => {
inputValue.set('new value 3');
}, 2000);
</script>
<p>The debounced value is: {currentDebouncedValue}</p>
在上述代码中,我们使用 lodash
的 debounce
函数。当 inputValue
store 变化时,debouncedValue
会更新,但通过 debounce
延迟了 console.log
的执行,避免了在短时间内频繁打印,从而提高了性能。
另一种优化方法是在 derived
的回调函数中进行更精细的判断,只有当真正影响派生状态的依赖发生变化时才重新计算。例如,如果派生状态只依赖于某个对象的特定属性,而不是整个对象,我们可以在回调函数中比较该属性的值,只有当该属性变化时才重新计算。
<script>
import { writable, derived } from'svelte/store';
const user = writable({ name: 'John', age: 30 });
const userName = derived(user, ($user, set) => {
let previousName = $user.name;
set(previousName);
return () => {
// 清理函数,当不再需要派生状态时执行
};
});
let currentUserName;
const unsubscribe = userName.subscribe((newUserName) => {
currentUserName = newUserName;
console.log('User name updated:', currentUserName);
});
setTimeout(() => {
user.update((prevUser) => ({...prevUser, age: 31 }));
}, 2000);
setTimeout(() => {
user.update((prevUser) => ({...prevUser, name: 'Jane' }));
}, 4000);
</script>
<p>The user's name is: {currentUserName}</p>
在这个例子中,userName
派生状态只依赖于 user
对象的 name
属性。在 derived
的回调函数中,我们记录了 previousName
,并在 user
对象变化时,只有当 name
属性变化时才会更新 userName
,从而减少了不必要的计算。
与组件的结合使用
derived
store 在组件中使用非常方便,可以帮助我们更好地管理组件内部和组件之间的状态。
假设有一个父组件包含一个子组件,父组件管理商品的价格和数量,子组件显示商品的总价。我们可以使用 derived
store 来实现这种状态管理和传递。
<!-- Parent.svelte -->
<script>
import { writable, derived } from'svelte/store';
import Child from './Child.svelte';
const price = writable(10);
const quantity = writable(2);
const total = derived([price, quantity], ([$price, $quantity]) => {
return $price * $quantity;
});
</script>
<Child {total} />
<!-- Child.svelte -->
<script>
import { readable } from'svelte/store';
export let total;
let currentTotal;
const unsubscribe = total.subscribe((newTotal) => {
currentTotal = newTotal;
});
</script>
<p>The total price in child component is: {currentTotal}</p>
在上述代码中,父组件创建了 price
和 quantity
store,并派生出 total
store。然后将 total
store 传递给子组件。子组件通过 export let
接收 total
store,并订阅它来获取最新的总价并显示。
这种方式使得组件之间的状态管理更加清晰和可控,每个组件只需要关注自己相关的状态,而 derived
store 则负责处理状态的派生和更新。
错误处理
在使用 derived
store 时,特别是当涉及异步操作或者复杂计算时,可能会出现错误。我们需要妥善处理这些错误,以确保应用的稳定性和用户体验。
当 derived
的回调函数中抛出错误时,subscribe
的第二个参数可以作为错误处理函数。
<script>
import { writable, derived } from'svelte/store';
const numerator = writable(10);
const denominator = writable(0);
const quotient = derived([numerator, denominator], ([$numerator, $denominator]) => {
if ($denominator === 0) {
throw new Error('Division by zero');
}
return $numerator / $denominator;
});
let currentQuotient;
const unsubscribe = quotient.subscribe(
(newQuotient) => {
currentQuotient = newQuotient;
console.log('Quotient updated:', currentQuotient);
},
(error) => {
console.error('Error:', error.message);
}
);
</script>
<p>The quotient is: {currentQuotient}</p>
在这个例子中,当 denominator
为 0
时,derived
的回调函数抛出一个错误。通过在 subscribe
中提供第二个参数作为错误处理函数,我们可以捕获并处理这个错误,在控制台打印错误信息,而不是让应用崩溃。
如果派生状态的计算涉及异步操作,我们可以使用 try...catch
块来处理异步操作中的错误。
<script>
import { writable, derived } from'svelte/store';
const userId = writable(1);
const userFullName = derived(userId, async ($userId) => {
try {
const response = await fetch(`https://example.com/api/users/${$userId}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const user = await response.json();
return `${user.firstName} ${user.lastName}`;
} catch (error) {
console.error('Error fetching user:', error.message);
return null;
}
});
let currentFullName;
const unsubscribe = userFullName.subscribe((newFullName) => {
currentFullName = newFullName;
console.log('User full name updated:', currentFullName);
});
</script>
<p>The user's full name is: {currentFullName}</p>
在这个异步示例中,我们在 derived
的回调函数中使用 try...catch
块来捕获可能发生的网络错误或 JSON 解析错误。如果发生错误,我们在控制台打印错误信息,并返回 null
作为派生状态的值,以避免应用出现异常。
通过合理的错误处理,我们可以提高使用 derived
store 构建的应用的健壮性,确保在各种情况下都能提供良好的用户体验。
高级应用场景
除了前面提到的基本和常见的应用场景,derived
store 在一些更复杂的场景中也能发挥重要作用。
动态依赖的派生
有时候,派生状态的依赖可能不是固定的,而是根据其他状态动态变化的。例如,我们有一个表示当前选择的图表类型的 store,根据不同的图表类型,我们需要从不同的数据源(由不同的 store 表示)中派生出图表数据。
<script>
import { writable, derived } from'svelte/store';
const chartType = writable('bar');
const barData = writable([1, 2, 3, 4]);
const lineData = writable([5, 6, 7, 8]);
const chartDataSource = derived(chartType, ($chartType) => {
return $chartType === 'bar'? barData : lineData;
});
const chartData = derived(chartDataSource, ($dataSource) => {
return $dataSource;
});
let currentChartData;
const unsubscribe = chartData.subscribe((newChartData) => {
currentChartData = newChartData;
console.log('Chart data updated:', currentChartData);
});
setTimeout(() => {
chartType.set('line');
}, 2000);
</script>
<p>The current chart data is: {currentChartData}</p>
在这个例子中,chartDataSource
派生状态根据 chartType
的值动态选择 barData
或 lineData
。然后 chartData
又基于 chartDataSource
派生,这样当 chartType
变化时,chartData
会自动从新的数据源派生,实现了动态依赖的派生。
复杂的多步骤派生
在一些业务场景中,派生状态可能需要经过多个步骤的计算,并且每个步骤可能依赖于不同的 store 或派生状态。
假设我们正在开发一个财务应用,需要计算公司的净利润。净利润的计算需要先从不同的 store 获取总收入、总成本和税收减免信息,经过多个步骤的计算得出。
<script>
import { writable, derived } from'svelte/store';
const totalRevenue = writable(1000);
const totalCost = writable(500);
const taxDeduction = writable(100);
const grossProfit = derived([totalRevenue, totalCost], ([$totalRevenue, $totalCost]) => {
return $totalRevenue - $totalCost;
});
const taxableIncome = derived([grossProfit, taxDeduction], ([$grossProfit, $taxDeduction]) => {
return $grossProfit - $taxDeduction;
});
const netProfit = derived(taxableIncome, ($taxableIncome) => {
// 假设税率为 20%
return $taxableIncome * 0.8;
});
let currentNetProfit;
const unsubscribe = netProfit.subscribe((newNetProfit) => {
currentNetProfit = newNetProfit;
console.log('Net profit updated:', currentNetProfit);
});
setTimeout(() => {
totalRevenue.set(1200);
}, 2000);
setTimeout(() => {
taxDeduction.set(150);
}, 4000);
</script>
<p>The net profit is: {currentNetProfit}</p>
在这个例子中,我们首先从 totalRevenue
和 totalCost
派生出 grossProfit
,然后结合 taxDeduction
派生出 taxableIncome
,最后基于 taxableIncome
派生出 netProfit
。通过这种多步骤的派生,我们可以清晰地管理复杂的业务计算逻辑,并且当任何一个相关 store 变化时,最终的 netProfit
会自动更新。
通过这些高级应用场景的示例,我们可以看到 derived
store 在处理复杂状态管理和计算时的强大能力,它能够帮助我们构建灵活、可维护的前端应用。