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

深入理解Svelte derived store:派生状态的最佳实践

2022-02-075.0k 阅读

什么是 Svelte 的 Derived Store

在 Svelte 应用程序开发中,store 是管理应用状态的核心概念。简单来说,store 就是一个包含状态数据以及更新和订阅该数据方法的对象。而 derived store(派生状态)是基于其他一个或多个 store 创建的新 store。它通过监听源 store 的变化,然后根据定义的逻辑生成新的状态。

例如,假设有两个 store,一个存储用户的购物车商品列表,另一个存储当前应用的货币单位。我们可能想要创建一个 derived store 来实时计算购物车商品的总价格,这个总价格会随着商品列表或货币单位的变化而变化。这就是 derived store 的应用场景之一,它可以将多个相关状态整合并计算出一个新的状态。

创建 Derived Store 的基本语法

在 Svelte 中,通过 derived 函数来创建派生状态。derived 函数接受两个主要参数:源 store(或源 stores 数组)以及一个回调函数。回调函数接收源 store 的值,并返回派生状态的值。以下是一个简单的示例:

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

    // 创建一个可写的 store
    const count = writable(0);

    // 创建一个基于 count 的派生 store
    const doubleCount = derived(count, ($count) => {
        return $count * 2;
    });
</script>

<p>Count: {$count}</p>
<p>Double Count: {$doubleCount}</p>

<button on:click={() => count.update(n => n + 1)}>Increment</button>

在上述代码中,我们首先创建了一个名为 count 的可写 store,初始值为 0。然后通过 derived 函数创建了 doubleCount 派生 store。doubleCount 的值是 count 值的两倍。每次 count 更新时,doubleCount 会自动重新计算。页面上显示了 countdoubleCount 的值,并且有一个按钮可以增加 count 的值,同时 doubleCount 也会相应更新。

依赖多个 Store

derived 函数不仅可以依赖单个 store,还可以依赖多个 store。当依赖多个 store 时,传递给 derived 的第一个参数是一个数组,数组中包含所有依赖的 store。回调函数接收与数组中 store 顺序对应的多个值。

<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;
    });
</script>

<p>Width: {$width}</p>
<p>Height: {$height}</p>
<p>Area: {$area}</p>

<button on:click={() => width.update(w => w + 10)}>Increase Width</button>
<button on:click={() => height.update(h => h + 10)}>Increase Height</button>

在这个例子中,area 派生 store 依赖于 widthheight 两个 store。当 widthheight 发生变化时,area 会重新计算并更新。页面展示了 widthheightarea 的值,并且有两个按钮分别用于增加 widthheight,观察 area 的实时更新。

处理异步操作

在实际应用中,派生状态的计算可能涉及异步操作,例如从 API 获取数据并结合本地状态进行计算。在这种情况下,derived 函数的回调函数可以返回一个 Promise。

<script>
    import { writable, derived } from'svelte/store';
    const baseUrl = 'https://example.com/api';
    const id = writable(1);

    const apiData = derived(id, async ($id) => {
        const response = await fetch(`${baseUrl}/${$id}`);
        const data = await response.json();
        return data;
    });
</script>

{#if $apiData}
    <p>API Data: {JSON.stringify($apiData)}</p>
{/if}

<button on:click={() => id.update(i => i + 1)}>Change ID</button>

这里,apiData 派生 store 依赖于 id store。当 id 变化时,apiData 会发起一个新的 API 请求,并更新为新的数据。页面根据 apiData 是否存在来展示数据,并且有一个按钮可以改变 id 值,触发新的 API 请求。

初始值的设置

derived 函数还可以接受一个可选的第三个参数,用于设置派生 store 的初始值。这在派生状态的计算需要一定时间或者依赖的 store 初始值尚未准备好时非常有用。

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

    const number = writable();
    const squared = derived(number, ($number) => {
        return $number? $number * $number : null;
    }, null);
</script>

<p>Squared: {$squared}</p>

<button on:click={() => number.set(5)}>Set Number</button>

在上述代码中,number store 初始值为 undefinedsquared 派生 store 依赖于 number,并且通过第三个参数设置初始值为 null。这样,在 number 被设置值之前,squared 不会报错,而是显示 null。当点击按钮设置 number 为 5 时,squared 会计算并显示 25。

清理函数

在派生 store 的回调函数中,如果执行了一些需要清理的操作(例如订阅外部事件、打开连接等),可以返回一个清理函数。这个清理函数会在派生 store 被销毁(例如组件卸载)或者源 store 发生变化时被调用。

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

    const intervalIdStore = writable(null);

    const counter = derived(intervalIdStore, ($intervalId) => {
        let count = 0;
        const intervalId = setInterval(() => {
            count++;
        }, 1000);

        return () => {
            clearInterval(intervalId);
        };
    });
</script>

<button on:click={() => {
    const newIntervalId = setInterval(() => { }, 1000);
    intervalIdStore.set(newIntervalId);
}}>Start Interval</button>

在这个例子中,counter 派生 store 依赖于 intervalIdStore。当 intervalIdStore 变化时,会创建一个新的定时器,并返回一个清理函数来清除旧的定时器。每次点击按钮会更新 intervalIdStore,触发清理函数清理旧定时器并创建新定时器。

与响应式声明的对比

在 Svelte 中,除了使用 derived 创建派生状态,还可以使用响应式声明($:)来实现类似功能。然而,它们之间有一些重要的区别。

响应式声明主要用于执行副作用操作,例如更新 DOM、记录日志等,并且它们不会返回一个可订阅的 store。而 derived 创建的是一个真正的 store,可以在应用的其他部分进行订阅和使用。

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

    const a = writable(1);
    const b = writable(2);

    let sum;
    $: sum = $a + $b;

    const sumStore = derived([a, b], ([$a, $b]) => {
        return $a + $b;
    });
</script>

<p>Sum (Reactive Declaration): {sum}</p>
<p>Sum (Derived Store): {$sumStore}</p>

在上述代码中,通过响应式声明计算了 ab 的和并存储在变量 sum 中,同时使用 derived 创建了 sumStore。虽然两者都能计算出 ab 的和,但 sumStore 是一个可订阅的 store,可以在其他组件或逻辑中使用,而 sum 只是一个局部变量。

在组件间共享 Derived Store

派生 store 可以在不同组件之间共享,就像普通 store 一样。这有助于在整个应用中统一管理派生状态。

// store.js
import { writable, derived } from'svelte/store';

const count = writable(0);
const doubleCount = derived(count, ($count) => {
    return $count * 2;
});

export { count, doubleCount };
// Component1.svelte
<script>
    import { count, doubleCount } from './store.js';
</script>

<p>Component 1 - Count: {$count}</p>
<p>Component 1 - Double Count: {$doubleCount}</p>

<button on:click={() => count.update(n => n + 1)}>Increment in Component 1</button>
// Component2.svelte
<script>
    import { count, doubleCount } from './store.js';
</script>

<p>Component 2 - Count: {$count}</p>
<p>Component 2 - Double Count: {$doubleCount}</p>

<button on:click={() => count.update(n => n - 1)}>Decrement in Component 2</button>

在上述代码中,store.js 定义了 countdoubleCount 两个 store,其中 doubleCount 是派生 store。Component1.svelteComponent2.svelte 都引入了这两个 store,并且可以分别对 count 进行操作,doubleCount 会在两个组件中同步更新,展示了派生 store 在组件间的共享性。

性能优化方面的考虑

在使用派生 store 时,性能优化是一个重要的方面。如果派生状态的计算非常复杂,频繁的更新可能会导致性能问题。一种优化方法是使用 throttledebounce 技术来限制源 store 变化时派生 store 的更新频率。

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

    const value = writable(0);

    const throttledDerived = derived(value, ($value) => {
        return $value * $value;
    });

    const throttledUpdate = throttle(() => {
        value.update(v => v + 1);
    }, 500);
</script>

<p>Throttled Derived: {$throttledDerived}</p>
<button on:click={throttledUpdate}>Increment with Throttle</button>

在这个例子中,通过 lodashthrottle 函数限制了 value store 的更新频率,从而减少了 throttledDerived 派生 store 的不必要计算,提升了性能。

复杂场景下的应用

在实际项目中,派生 store 常用于复杂的业务逻辑场景。例如,在一个电商应用中,可能有多个与用户购物车相关的 store,如商品列表、商品价格、促销活动等。通过派生 store 可以计算出购物车的总价、折扣后价格、预计运费等复杂状态。

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

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

    const discount = writable(0.1);

    const totalPriceBeforeDiscount = derived(cartItems, ($cartItems) => {
        return $cartItems.reduce((acc, item) => {
            return acc + item.price * item.quantity;
        }, 0);
    });

    const totalPriceAfterDiscount = derived([totalPriceBeforeDiscount, discount], ([$totalPriceBeforeDiscount, $discount]) => {
        return $totalPriceBeforeDiscount * (1 - $discount);
    });
</script>

<p>Total Price Before Discount: {$totalPriceBeforeDiscount}</p>
<p>Total Price After Discount: {$totalPriceAfterDiscount}</p>

<button on:click={() => cartItems.update(items => {
    const newItem = { id: 3, name: 'Product 3', price: 20, quantity: 1 };
    return [...items, newItem];
})}>Add Item to Cart</button>

<button on:click={() => discount.update(d => d + 0.05)}>Increase Discount</button>

在这个电商购物车的示例中,totalPriceBeforeDiscount 派生 store 计算购物车商品的总价,totalPriceAfterDiscount 派生 store 基于 totalPriceBeforeDiscountdiscount 计算折扣后的价格。通过按钮可以模拟添加商品到购物车和增加折扣的操作,观察派生 store 的实时更新。

结合 TypeScript 使用 Derived Store

当在 Svelte 项目中使用 TypeScript 时,正确定义派生 store 的类型可以提高代码的健壮性和可读性。

// store.ts
import { writable, derived, Writable } from'svelte/store';

interface CartItem {
    id: number;
    name: string;
    price: number;
    quantity: number;
}

const cartItems: Writable<CartItem[]> = writable([]);
const discount: Writable<number> = writable(0);

const totalPriceBeforeDiscount = derived(cartItems, ($cartItems): number => {
    return $cartItems.reduce((acc, item) => {
        return acc + item.price * item.quantity;
    }, 0);
});

const totalPriceAfterDiscount = derived([totalPriceBeforeDiscount, discount], ([$totalPriceBeforeDiscount, $discount]): number => {
    return $totalPriceBeforeDiscount * (1 - $discount);
});

export { cartItems, discount, totalPriceBeforeDiscount, totalPriceAfterDiscount };

在上述 TypeScript 代码中,首先定义了 CartItem 接口来描述购物车商品的结构。然后明确了 cartItemsdiscount store 的类型为 Writable。在定义派生 store 时,通过类型注解明确了返回值的类型,使得代码更加清晰和易于维护。

错误处理

在派生 store 的计算过程中,可能会发生错误,例如 API 请求失败。为了处理这些错误,可以在派生 store 的回调函数中使用 try...catch 块。

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

    const apiUrl = writable('https://example.com/api');
    const apiData = derived(apiUrl, async ($apiUrl) => {
        try {
            const response = await fetch($apiUrl);
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            const data = await response.json();
            return data;
        } catch (error) {
            console.error('Error fetching data:', error);
            return null;
        }
    });
</script>

{#if $apiData}
    <p>API Data: {JSON.stringify($apiData)}</p>
{:else}
    <p>Loading or error...</p>
{/if}

<button on:click={() => apiUrl.set('https://example.com/invalid-api')}>Change API URL</button>

在这个例子中,apiData 派生 store 在请求 API 时,如果发生错误,会捕获错误并返回 null。页面根据 apiData 是否为 null 来显示相应的信息,同时通过按钮改变 apiUrl 来模拟错误情况。

调试技巧

调试派生 store 时,可以在回调函数中添加日志输出,观察源 store 变化时派生状态的计算过程。另外,Svelte 提供的开发者工具也可以帮助查看 store 的状态和变化。

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

    const number1 = writable(1);
    const number2 = writable(2);

    const sum = derived([number1, number2], ([$number1, $number2]) => {
        console.log(`Calculating sum with number1: ${$number1}, number2: ${$number2}`);
        return $number1 + $number2;
    });
</script>

<p>Sum: {$sum}</p>

<button on:click={() => number1.update(n => n + 1)}>Increment number1</button>
<button on:click={() => number2.update(n => n + 1)}>Increment number2</button>

在上述代码中,通过在 sum 派生 store 的回调函数中添加 console.log 语句,可以在控制台观察到每次计算 sumnumber1number2 的值。同时,可以使用 Svelte 开发者工具查看 number1number2sum store 的实时状态。

通过以上对 Svelte 中 derived store 的深入探讨,从基本概念到复杂应用场景,从性能优化到错误处理和调试技巧,希望开发者能够更好地掌握派生状态的最佳实践,构建更加高效、健壮的 Svelte 应用程序。