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

Svelte 状态管理进阶:如何组合多个 store 实现复杂逻辑

2021-09-223.6k 阅读

Svelte 状态管理进阶:如何组合多个 store 实现复杂逻辑

Svelte 中 store 的基础概念回顾

在深入探讨如何组合多个 store 之前,我们先来简单回顾一下 Svelte 中 store 的基本概念。在 Svelte 里,store 是一种管理应用状态的方式。它是一个具有 subscribe 方法的对象,该方法允许我们监听状态的变化。

import { writable } from'svelte/store';

// 创建一个简单的 writable store
const count = writable(0);

// 订阅状态变化
const unsubscribe = count.subscribe((value) => {
    console.log(`The count has changed to: ${value}`);
});

// 更新状态
count.set(1);

// 取消订阅
unsubscribe();

上述代码通过 writable 函数创建了一个名为 count 的 store,初始值为 0。我们可以通过 subscribe 方法注册一个回调函数,每当 count 的值发生变化时,这个回调函数就会被调用。set 方法用于更新 store 的值。当我们不再需要监听状态变化时,可以调用 unsubscribe 函数取消订阅。

简单组合多个 store 的场景

  1. 加法运算场景 假设我们有两个 writable store,分别表示两个数字,我们希望得到它们的和。
<script>
    import { writable } from'svelte/store';

    const num1 = writable(5);
    const num2 = writable(3);

    // 创建一个新的 store 来表示它们的和
    const sum = derived([num1, num2], ([$num1, $num2]) => {
        return $num1 + $num2;
    });

    let unsubscribeSum;
    // 订阅 sum 的变化
    unsubscribeSum = sum.subscribe((value) => {
        console.log(`The sum is: ${value}`);
    });

    // 更新 num1 和 num2
    num1.set(7);
    num2.set(4);
</script>

在这段代码中,我们使用 derived 函数来创建一个新的 store sumderived 函数接受两个参数,第一个参数是一个包含依赖 store(这里是 num1num2)的数组,第二个参数是一个回调函数。当任何依赖 store 的值发生变化时,这个回调函数就会被调用,回调函数的参数是依赖 store 当前的值(这里用 $num1$num2 表示),回调函数的返回值会作为新 store sum 的值。

  1. 逻辑判断场景 假设有两个布尔类型的 store,我们希望根据这两个 store 的值来判断一个复杂的逻辑条件。
<script>
    import { writable } from'svelte/store';

    const isLoggedIn = writable(false);
    const hasPermission = writable(false);

    const canAccess = derived([isLoggedIn, hasPermission], ([$isLoggedIn, $hasPermission]) => {
        return $isLoggedIn && $hasPermission;
    });

    let unsubscribeAccess;
    unsubscribeAccess = canAccess.subscribe((value) => {
        if (value) {
            console.log('User can access the resource.');
        } else {
            console.log('User cannot access the resource.');
        }
    });

    // 更新状态
    isLoggedIn.set(true);
    hasPermission.set(true);
</script>

这里我们通过 derived 创建了 canAccess store,它的值取决于 isLoggedInhasPermission。只有当用户登录(isLoggedIntrue)并且有权限(hasPermissiontrue)时,canAccess 才为 true

嵌套组合 store

  1. 多层依赖组合 考虑一个稍微复杂的场景,我们有三个 store:baseValuemultiplieradder。我们希望通过 baseValue 乘以 multiplier 再加上 adder 得到最终结果。
<script>
    import { writable, derived } from'svelte/store';

    const baseValue = writable(2);
    const multiplier = writable(3);
    const adder = writable(4);

    const intermediate = derived([baseValue, multiplier], ([$baseValue, $multiplier]) => {
        return $baseValue * $multiplier;
    });

    const finalResult = derived([intermediate, adder], ([$intermediate, $adder]) => {
        return $intermediate + $adder;
    });

    let unsubscribeFinal;
    unsubscribeFinal = finalResult.subscribe((value) => {
        console.log(`The final result is: ${value}`);
    });

    // 更新状态
    baseValue.set(3);
    multiplier.set(4);
    adder.set(5);
</script>

在这个例子中,我们首先通过 derived 创建了 intermediate store,它依赖于 baseValuemultiplier。然后,我们又基于 intermediateadder 创建了 finalResult store。这样就形成了一个嵌套的依赖关系,任何底层 store 的变化都会影响到最终的 finalResult

  1. 动态嵌套组合 有时候,我们可能需要根据某些条件动态地组合 store。比如,我们有一个开关 isAdvancedCalculation,当它为 true 时,我们希望使用更复杂的计算逻辑(结合更多的 store),当它为 false 时,使用简单的计算逻辑。
<script>
    import { writable, derived } from'svelte/store';

    const baseValue = writable(2);
    const simpleMultiplier = writable(3);
    const complexMultiplier = writable(5);
    const adder = writable(4);
    const isAdvancedCalculation = writable(false);

    const simpleCalculation = derived([baseValue, simpleMultiplier, adder], ([$baseValue, $simpleMultiplier, $adder]) => {
        return $baseValue * $simpleMultiplier + $adder;
    });

    const complexCalculation = derived([baseValue, complexMultiplier, adder], ([$baseValue, $complexMultiplier, $adder]) => {
        return ($baseValue * $complexMultiplier) * 2 + $adder;
    });

    const finalCalculation = derived([isAdvancedCalculation, simpleCalculation, complexCalculation], ([$isAdvancedCalculation, $simpleCalculation, $complexCalculation]) => {
        return $isAdvancedCalculation? $complexCalculation : $simpleCalculation;
    });

    let unsubscribeFinal;
    unsubscribeFinal = finalCalculation.subscribe((value) => {
        console.log(`The final calculation result is: ${value}`);
    });

    // 更新状态
    isAdvancedCalculation.set(true);
    baseValue.set(3);
</script>

在这个示例中,我们创建了 simpleCalculationcomplexCalculation 两个不同的计算结果 store,然后通过 finalCalculation 根据 isAdvancedCalculation 的值来决定使用哪一个结果。

处理异步操作的 store 组合

  1. 基于异步数据的计算 假设我们有一个 API 调用返回的数据 store userData,并且有一个本地配置 store displayOption。我们希望根据 displayOptionuserData 进行一些处理,例如格式化显示。
<script>
    import { writable, derived } from'svelte/store';
    import { onMount } from'svelte';

    const userData = writable(null);
    const displayOption = writable('default');

    onMount(() => {
        // 模拟 API 调用
        setTimeout(() => {
            userData.set({ name: 'John', age: 30 });
        }, 1000);
    });

    const formattedUserData = derived([userData, displayOption], ([$userData, $displayOption]) => {
        if (!$userData) return '';
        if ($displayOption === 'default') {
            return `Name: ${$userData.name}, Age: ${$userData.age}`;
        } else if ($displayOption === 'compact') {
            return `${$userData.name} (${$userData.age})`;
        }
        return '';
    });

    let unsubscribeFormatted;
    unsubscribeFormatted = formattedUserData.subscribe((value) => {
        console.log(`Formatted user data: ${value}`);
    });

    // 更新显示选项
    displayOption.set('compact');
</script>

在这段代码中,userData 是通过模拟的异步操作(setTimeout 模拟 API 调用)来更新的。formattedUserData 依赖于 userDatadisplayOption,当 userData 有值且 displayOption 变化时,会重新计算格式化后的数据。

  1. 多个异步 store 的组合 有时候我们可能有多个异步 API 调用,并且需要将这些异步返回的数据进行组合。比如,我们有一个获取用户信息的 API 和一个获取用户偏好设置的 API。
<script>
    import { writable, derived } from'svelte/store';
    import { onMount } from'svelte';

    const userInfo = writable(null);
    const userPreferences = writable(null);

    onMount(() => {
        // 模拟获取用户信息的 API 调用
        setTimeout(() => {
            userInfo.set({ name: 'Jane', age: 25 });
        }, 1000);

        // 模拟获取用户偏好设置的 API 调用
        setTimeout(() => {
            userPreferences.set({ theme: 'dark', language: 'en' });
        }, 1500);
    });

    const combinedUserInfo = derived([userInfo, userPreferences], ([$userInfo, $userPreferences]) => {
        if (!$userInfo ||!$userPreferences) return null;
        return {
           ...$userInfo,
           ...$userPreferences
        };
    });

    let unsubscribeCombined;
    unsubscribeCombined = combinedUserInfo.subscribe((value) => {
        if (value) {
            console.log(`Combined user info: ${JSON.stringify(value)}`);
        }
    });
</script>

在这个例子中,userInfouserPreferences 都是通过异步操作更新的 store。combinedUserInfo 使用 derived 来组合这两个 store 的数据,只有当两个 store 都有值时,才会生成组合后的信息。

使用 store 组合实现复杂业务逻辑

  1. 购物车场景 在一个电商应用的购物车模块中,我们可能有多个相关的 store。例如,cartItems store 表示购物车中的商品列表,coupon store 表示用户使用的优惠券,shippingOption 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 coupon = writable(null);

    const shippingOption = writable({ cost: 5 });

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

    const discount = derived(coupon, ($coupon) => {
        if (!$coupon) return 0;
        return $coupon.amount;
    });

    const total = derived([subTotal, discount, shippingOption], ([$subTotal, $discount, $shippingOption]) => {
        return $subTotal - $discount + $shippingOption.cost;
    });

    let unsubscribeTotal;
    unsubscribeTotal = total.subscribe((value) => {
        console.log(`The total price of the order is: ${value}`);
    });

    // 更新购物车商品数量
    cartItems.update((items) => {
        items[0].quantity = 3;
        return items;
    });

    // 使用优惠券
    coupon.set({ amount: 3 });
</script>

在这个购物车场景中,我们首先通过 cartItems 计算出 subTotal。然后根据 coupon 计算出 discount。最后,基于 subTotaldiscountshippingOption 计算出最终的 total。这样通过组合多个 store,实现了购物车复杂的价格计算逻辑。

  1. 任务管理场景 在一个任务管理应用中,我们可能有 tasks store 表示所有任务列表,filter store 表示用户选择的任务过滤条件(如只显示已完成任务或未完成任务),sortOption store 表示用户选择的排序方式(如按截止日期排序或按优先级排序)。
<script>
    import { writable, derived } from'svelte/store';

    const tasks = writable([
        { id: 1, title: 'Task 1', completed: false, dueDate: '2023 - 10 - 10', priority: 'high' },
        { id: 2, title: 'Task 2', completed: true, dueDate: '2023 - 10 - 15', priority: 'low' }
    ]);

    const filter = writable('all');
    const sortOption = writable('dueDate');

    const filteredTasks = derived([tasks, filter], ([$tasks, $filter]) => {
        if ($filter === 'all') {
            return $tasks;
        } else if ($filter === 'completed') {
            return $tasks.filter(task => task.completed);
        } else if ($filter === 'incomplete') {
            return $tasks.filter(task =>!task.completed);
        }
        return [];
    });

    const sortedTasks = derived([filteredTasks, sortOption], ([$filteredTasks, $sortOption]) => {
        if ($sortOption === 'dueDate') {
            return $filteredTasks.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
        } else if ($sortOption === 'priority') {
            const priorities = { high: 0, medium: 1, low: 2 };
            return $filteredTasks.sort((a, b) => priorities[a.priority] - priorities[b.priority]);
        }
        return $filteredTasks;
    });

    let unsubscribeSorted;
    unsubscribeSorted = sortedTasks.subscribe((value) => {
        console.log(`Sorted and filtered tasks: ${JSON.stringify(value)}`);
    });

    // 更新过滤条件
    filter.set('completed');

    // 更新排序选项
    sortOption.set('priority');
</script>

在这个任务管理场景中,我们首先根据 filtertasks 进行过滤得到 filteredTasks,然后再根据 sortOptionfilteredTasks 进行排序得到 sortedTasks。通过这种方式,我们能够根据用户的不同操作动态地组合和处理任务数据,以满足复杂的业务需求。

优化组合 store 的性能

  1. 减少不必要的计算 在使用 derived 组合多个 store 时,要注意避免不必要的计算。例如,如果一个 derived store 的依赖 store 变化频繁,但某些依赖实际上对计算结果没有影响,我们可以通过更精细的逻辑来避免不必要的重新计算。
<script>
    import { writable, derived } from'svelte/store';

    const counter = writable(0);
    const userData = writable({ name: 'Alice', age: 30 });

    const userInfo = derived(userData, ($userData) => {
        return `Name: ${$userData.name}, Age: ${$userData.age}`;
    });

    const combinedInfo = derived([counter, userInfo], ([$counter, $userInfo]) => {
        // 这里 counter 实际上对 userInfo 的生成没有影响
        // 可以将 userInfo 的计算逻辑提取出来,避免每次 counter 变化时都重新计算 userInfo
        return {
            counter: $counter,
            userInfo: $userInfo
        };
    });

    let unsubscribeCombined;
    unsubscribeCombined = combinedInfo.subscribe((value) => {
        console.log(`Combined info: ${JSON.stringify(value)}`);
    });

    // 更新 counter
    counter.set(1);
</script>

在这个例子中,userInfo 的计算只依赖于 userData,而 combinedInfo 虽然依赖于 counteruserInfo,但 counter 的变化不影响 userInfo 的生成。因此,我们可以在 combinedInfo 的计算中,避免每次 counter 变化时都重新计算 userInfo

  1. 缓存计算结果 对于一些计算代价较高的 derived store,我们可以考虑缓存计算结果。例如,假设我们有一个复杂的计算逻辑,需要对一个大数组进行多次过滤和排序。
<script>
    import { writable, derived } from'svelte/store';

    const largeArray = writable([...Array(1000).keys()]);
    const filterValue = writable(500);
    const sortOrder = writable('asc');

    let cachedResult;
    const processedArray = derived([largeArray, filterValue, sortOrder], ([$largeArray, $filterValue, $sortOrder]) => {
        if (cachedResult && cachedResult.filterValue === $filterValue && cachedResult.sortOrder === $sortOrder) {
            return cachedResult.array;
        }
        let filteredArray = $largeArray.filter(num => num > $filterValue);
        if ($sortOrder === 'asc') {
            filteredArray.sort((a, b) => a - b);
        } else {
            filteredArray.sort((a, b) => b - a);
        }
        cachedResult = {
            filterValue: $filterValue,
            sortOrder: $sortOrder,
            array: filteredArray
        };
        return filteredArray;
    });

    let unsubscribeProcessed;
    unsubscribeProcessed = processedArray.subscribe((value) => {
        console.log(`Processed array: ${JSON.stringify(value)}`);
    });

    // 更新过滤值
    filterValue.set(400);
</script>

在这个示例中,我们通过 cachedResult 来缓存 processedArray 的计算结果。当 filterValuesortOrder 没有变化时,直接返回缓存的结果,避免了重复的高代价计算。

错误处理与健壮性

  1. 处理 store 初始值为 null 或 undefined 的情况 在组合多个 store 时,很可能会遇到某些 store 初始值为 nullundefined 的情况。例如,在异步获取数据的场景下,数据可能还未加载完成。
<script>
    import { writable, derived } from'svelte/store';
    import { onMount } from'svelte';

    const userData = writable(null);
    const settings = writable(null);

    onMount(() => {
        // 模拟异步获取用户数据
        setTimeout(() => {
            userData.set({ name: 'Bob', age: 28 });
        }, 1000);

        // 模拟异步获取设置
        setTimeout(() => {
            settings.set({ theme: 'light' });
        }, 1500);
    });

    const combinedData = derived([userData, settings], ([$userData, $settings]) => {
        if (!$userData ||!$settings) return null;
        return {
           ...$userData,
           ...$settings
        };
    });

    let unsubscribeCombined;
    unsubscribeCombined = combinedData.subscribe((value) => {
        if (value) {
            console.log(`Combined data: ${JSON.stringify(value)}`);
        } else {
            console.log('Data is not ready yet.');
        }
    });
</script>

在这段代码中,userDatasettings 初始值都为 null。在 combinedData 的计算中,我们首先检查这两个 store 是否都有值,如果有一个为 null,则返回 null,以避免在数据不完整时出现错误。

  1. 处理异步操作中的错误 当异步操作(如 API 调用)出现错误时,我们需要妥善处理。例如,在获取用户数据的 API 调用失败时,我们可以更新 store 来表示错误状态。
<script>
    import { writable, derived } from'svelte/store';
    import { onMount } from'svelte';

    const userData = writable(null);
    const userDataError = writable(null);

    onMount(() => {
        // 模拟 API 调用失败
        setTimeout(() => {
            const success = false;
            if (success) {
                userData.set({ name: 'Charlie', age: 32 });
            } else {
                userDataError.set('Failed to fetch user data');
            }
        }, 1000);
    });

    const userInfo = derived([userData, userDataError], ([$userData, $userDataError]) => {
        if ($userDataError) {
            return `Error: ${$userDataError}`;
        } else if ($userData) {
            return `Name: ${$userData.name}, Age: ${$userData.age}`;
        }
        return 'Loading...';
    });

    let unsubscribeUserInfo;
    unsubscribeUserInfo = userInfo.subscribe((value) => {
        console.log(`User info: ${value}`);
    });
</script>

在这个例子中,我们创建了 userDataError store 来表示获取用户数据时的错误。在 userInfo 的计算中,根据 userDataErroruserData 的状态返回不同的信息,从而提高了应用的健壮性。

总结多个 store 组合的最佳实践

  1. 清晰的依赖关系 在组合多个 store 时,要确保依赖关系清晰。每个 derived store 的依赖应该明确,并且尽量避免创建过于复杂或难以理解的依赖链。这样有助于代码的维护和调试。例如,在购物车场景中,subTotaldiscountshippingOptiontotal 的依赖关系一目了然,易于理解和修改。

  2. 命名规范 为 store 和相关的计算逻辑使用清晰、有意义的命名。比如在任务管理场景中,tasksfiltersortOptionfilteredTaskssortedTasks 这些命名能够准确地描述其功能,使得代码的可读性大大提高。

  3. 性能优化意识 时刻关注性能问题,通过减少不必要的计算和缓存计算结果等方式来优化组合 store 的性能。如在处理大数组的计算场景中,通过缓存结果避免了重复的高代价操作。

  4. 错误处理 要充分考虑各种可能出现的错误情况,如 store 初始值为 nullundefined,以及异步操作中的错误等。通过合理的错误处理机制,提高应用的健壮性,如在异步获取数据场景中对错误状态的处理。

通过遵循这些最佳实践,我们能够更有效地使用 Svelte 的 store 组合来实现复杂的逻辑,打造出高效、健壮且易于维护的前端应用。