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

Svelte高级Store:derived计算属性详解

2021-08-067.3k 阅读

Svelte中的Store基础回顾

在深入探讨Svelte的derived计算属性之前,我们先来简单回顾一下Svelte中Store的基本概念。

在Svelte里,Store是一种状态管理机制。一个简单的Store可以通过writable函数创建。例如:

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

    const count = writable(0);
</script>

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

这里,writable创建了一个可写的Store,它有一个初始值0count.update方法用于更新Store的值,并且在模板中,通过{$count}的语法来订阅并显示Store的值。

为什么需要derived计算属性

在实际开发中,我们经常会遇到需要根据现有状态计算出一个新值的情况。比如,我们有一个表示购物车商品数量的Store,同时我们可能需要一个计算属性来表示商品总价,这个总价是商品单价和数量的乘积。

虽然我们可以在组件中直接通过常规的JavaScript计算来获取这个值,但这样做会带来一些问题。例如,如果商品数量或者单价频繁变化,每次变化都要重新计算总价,而且这个计算逻辑会分散在各个使用到总价的地方,不利于维护和复用。

这时候,derived计算属性就派上用场了。它可以根据一个或多个现有Store的值,创建一个新的Store,并且只有当依赖的Store值发生变化时,才会重新计算新的值。

derived的基本使用

derived函数接受两个参数,第一个参数是一个或多个Store,第二个参数是一个回调函数。回调函数会在依赖的Store值变化时被调用,并且返回一个新的值,这个新值会成为新创建的derived Store的值。

单个Store依赖的情况

假设我们有一个表示摄氏温度的Store,现在我们想创建一个表示华氏温度的derived Store。华氏温度和摄氏温度的转换公式是:F = C * 1.8 + 32。代码如下:

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

    const celsius = writable(20);
    const fahrenheit = derived(celsius, $celsius => $celsius * 1.8 + 32);
</script>

<label>
    Celsius:
    <input type="number" bind:value={$celsius}>
</label>

<p>Fahrenheit: {$fahrenheit}</p>

在这个例子中,derived的第一个参数是celsius这个Store。回调函数接受$celsius作为参数,$celsius表示celsius当前的值。每当celsius的值发生变化时,回调函数就会被调用,重新计算出华氏温度并更新fahrenheit这个derived Store的值。

多个Store依赖的情况

当有多个Store作为依赖时,derived的使用方式稍有不同。比如,我们有一个表示商品单价的Store和一个表示商品数量的Store,我们要计算商品总价。代码如下:

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

    const price = writable(10);
    const quantity = writable(2);
    const total = derived([price, quantity], ([$price, $quantity]) => $price * $quantity);
</script>

<label>
    Price:
    <input type="number" bind:value={$price}>
</label>

<label>
    Quantity:
    <input type="number" bind:value={$quantity}>
</label>

<p>Total: {$total}</p>

这里,derived的第一个参数是一个数组,包含了pricequantity两个Store。回调函数的参数也是一个数组,[$price, $quantity]分别对应pricequantity当前的值。只要price或者quantity的值发生变化,回调函数就会被调用,重新计算总价并更新total这个derived Store的值。

derived的高级特性

初始值的设置

derived函数其实还接受第三个可选参数,这个参数用于设置derived Store的初始值。例如:

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

    const count = writable(0);
    const doubleCount = derived(count, $count => $count * 2, 2);
</script>

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

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

在这个例子中,doubleCount这个derived Store的初始值被设置为2。即使count的初始值为0doubleCount一开始显示的值也是2。只有当count的值发生变化时,doubleCount才会根据回调函数重新计算值。

取消订阅

当一个组件不再需要使用某个derived Store时,应该取消订阅以避免内存泄漏。在Svelte中,derived Store返回的对象有一个subscribe方法,它返回一个取消订阅函数。例如:

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

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

    let unsubscribe;
    const startListening = () => {
        unsubscribe = doubleCount.subscribe(value => {
            console.log('Double count is:', value);
        });
    };

    const stopListening = () => {
        unsubscribe && unsubscribe();
    };
</script>

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

<button on:click={startListening}>
    Start listening
</button>

<button on:click={stopListening}>
    Stop listening
</button>

在这个例子中,startListening函数通过调用doubleCount.subscribe来订阅doubleCount的值变化,并将返回的取消订阅函数存储在unsubscribe变量中。stopListening函数则通过调用unsubscribe函数来取消订阅。

处理异步操作

有时候,我们在计算derived Store的值时可能需要进行异步操作。比如,我们有一个表示用户ID的Store,我们要根据这个ID从服务器获取用户的详细信息。

首先,我们假设有一个模拟的异步获取用户信息的函数:

const fetchUser = async id => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve({ name: `User ${id}`, age: id * 2 });
        }, 1000);
    });
};

然后,我们在Svelte组件中使用derived来处理这个异步操作:

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

    const userId = writable(1);
    const userInfo = derived(userId, async $userId => {
        const user = await fetchUser($userId);
        return user;
    });
</script>

<label>
    User ID:
    <input type="number" bind:value={$userId}>
</label>

{#if $userInfo}
    <p>Name: {$userInfo.name}</p>
    <p>Age: {$userInfo.age}</p>
{:else}
    <p>Loading...</p>
{/if}

在这个例子中,derived的回调函数是一个异步函数。当userId的值发生变化时,异步函数会被调用,它会等待fetchUser函数返回结果,然后更新userInfo这个derived Store的值。在模板中,我们使用if块来根据userInfo是否有值来显示相应的内容。

derived与响应式声明的对比

在Svelte中,除了使用derived来创建计算属性,还可以使用响应式声明。例如:

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

    const price = writable(10);
    const quantity = writable(2);
    let total;
    $: total = $price * $quantity;
</script>

<label>
    Price:
    <input type="number" bind:value={$price}>
</label>

<label>
    Quantity:
    <input type="number" bind:value={$quantity}>
</label>

<p>Total: {total}</p>

这里使用$:来创建了一个响应式声明,当pricequantity的值发生变化时,total会重新计算。

虽然这种方式和derived在功能上有相似之处,但它们还是有一些区别的。

首先,derived创建的是一个Store,它可以在多个组件之间共享,而响应式声明是组件内部的局部变量,不能直接在其他组件中使用。

其次,derived在处理复杂逻辑和异步操作时更加方便和直观。例如,在上面处理异步获取用户信息的例子中,如果使用响应式声明来实现,代码会变得更加复杂,需要手动处理异步状态等问题。

在大型项目中的应用场景

在大型前端项目中,derived计算属性有着广泛的应用场景。

数据聚合与处理

在一个电商项目中,可能有多个Store分别表示不同类别的商品库存。通过derived,我们可以创建一个总的库存Store,将各个类别商品的库存数量进行汇总。这样,在需要显示总库存的地方,只需要订阅这个derived Store即可,而不需要关心具体每个类别商品库存的变化细节。

动态UI状态计算

假设我们有一个表示用户登录状态的Store,以及一个表示用户权限的Store。通过derived,我们可以创建一个新的Store来表示用户是否有权限访问某个特定功能。这样,在UI组件中,只需要根据这个derived Store的值来决定是否显示相关功能按钮,而不需要在每个相关组件中重复编写复杂的权限判断逻辑。

与其他状态管理库的结合

虽然Svelte自身的Store机制已经很强大,但在一些大型项目中,可能会结合其他状态管理库,如Redux或MobX。derived可以很好地与这些库结合使用。例如,我们可以将Redux中的状态转换为Svelte的Store,然后通过derived进行进一步的计算和处理,以满足Svelte组件的使用需求。

常见问题及解决方法

性能问题

如果derived依赖的Store频繁变化,可能会导致性能问题,因为每次依赖变化都会触发回调函数重新计算。为了解决这个问题,可以考虑使用防抖(debounce)或节流(throttle)技术。

例如,我们可以使用lodash库中的debounce函数来包装derived回调函数中的计算逻辑。首先安装lodash

npm install lodash

然后在Svelte组件中使用:

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

    const count = writable(0);
    const expensiveCalculation = derived(count, $count => {
        // 这里假设是一个复杂的计算
        return $count * $count * $count;
    }, 0);

    const debouncedCalculation = derived(count, $count => {
        const debouncedFunc = debounce(() => {
            // 这里假设是一个复杂的计算
            return $count * $count * $count;
        }, 300);
        return debouncedFunc();
    }, 0);
</script>

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

<p>Expensive Calculation: {$expensiveCalculation}</p>
<p>Debounced Calculation: {$debouncedCalculation}</p>

在这个例子中,debouncedCalculation使用了debounce函数,它会在count值变化后延迟300毫秒才执行复杂的计算,这样可以减少不必要的计算次数,提高性能。

循环依赖问题

在使用derived时,如果不小心可能会引入循环依赖。例如,derived Store A依赖derived Store B,而derived Store B又依赖derived Store A。这种情况下,Svelte会抛出错误。

要解决这个问题,需要仔细检查依赖关系,确保没有循环依赖。可以通过梳理状态管理逻辑,将复杂的依赖关系进行拆解和重构,使得每个derived Store的依赖关系清晰明确。

类型推断问题

在使用TypeScript与Svelte结合时,derived可能会遇到类型推断不准确的问题。例如,回调函数返回值的类型可能没有被正确推断。

为了解决这个问题,可以手动指定derived Store的类型。例如:

<script lang="ts">
    import { writable, derived } from'svelte/store';

    const count = writable(0);
    const doubleCount: { subscribe: (cb: (value: number) => void) => () => void } = derived(count, $count => $count * 2);
</script>

这里通过手动指定doubleCount的类型,确保了类型的准确性。虽然这种方式略显繁琐,但可以有效解决类型推断问题。

总结

derived计算属性是Svelte中一个非常强大的功能,它可以帮助我们轻松地创建基于现有Store的计算值。通过合理使用derived,我们可以更好地组织和管理状态,提高代码的可维护性和复用性。在实际项目中,要根据具体的需求和场景,充分发挥derived的优势,同时注意避免常见问题,以实现高效、稳定的前端应用开发。无论是简单的小型项目,还是复杂的大型项目,derived都有着不可或缺的作用。通过不断实践和探索,我们可以更加熟练地运用derived来解决各种状态管理和计算相关的问题。