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

Svelte状态管理对比:writable与derived的选择策略

2021-11-273.4k 阅读

Svelte 状态管理基础

在深入探讨 writablederived 的选择策略之前,我们先来回顾一下 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 提供的用于创建可写状态的函数。它返回一个包含 subscribesetupdate 方法的对象。

以下是一个简单的 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 创建了一个初始值为 0count 状态。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

应用场景

  1. 用户输入状态:当你需要处理用户输入,例如文本框的值、滑块的位置等,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 的值会自动更新。

  1. 应用的主要数据状态:如果你的应用有一些主要的数据,例如用户信息、购物车商品列表等,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 状态依赖于 widthheight 两个状态。当 widthheight 任何一个发生变化时,area 会重新计算。

应用场景

  1. 计算属性:与 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 派生出来的状态,用于计算购物车中所有商品的总价。

  1. 复杂状态转换:当你需要对现有状态进行复杂的转换或处理时,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 的选择策略

从数据来源角度选择

  1. 直接用户输入或外部数据:如果状态直接来源于用户输入(如表单数据)或外部 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>

usernamepassword 直接接收用户输入,使用 writable 可以方便地管理这些输入值。

  1. 基于现有状态计算得出:如果状态是基于其他状态计算得出的,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 可以确保只有当文章内容发生变化时才重新计算阅读时间。

从性能角度选择

  1. 频繁更新的状态:如果状态会频繁更新,并且更新操作比较简单,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 可能带来的不必要计算。

  1. 复杂计算且不频繁更新的状态:对于需要进行复杂计算且依赖状态不频繁更新的情况,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 可以避免每次都重新计算面积。

从代码结构和可维护性角度选择

  1. 简单状态管理:对于简单的状态管理,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 直接管理计数器状态,代码简单易懂。

  1. 复杂状态依赖关系:当状态之间存在复杂的依赖关系时,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 清晰地表达了 totalPricediscountedPrice 与其他状态的依赖关系,使得代码结构更清晰,易于维护。

从副作用处理角度选择

  1. 需要直接处理副作用的状态:如果状态在更新时需要执行一些副作用操作,如发送 API 请求、更新本地存储等,writable 更便于处理。因为 writablesetupdate 方法可以直接在更新状态时添加副作用逻辑。 例如,在一个待办事项应用中,当添加新的待办事项时,需要将新事项保存到本地存储:
<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 方法直接将新的待办事项列表保存到本地存储。

  1. 不涉及副作用的派生状态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>

实际项目中的案例分析

案例一:社交媒体应用

在一个社交媒体应用中,有用户的粉丝数量、关注数量以及关注者与粉丝的比例等状态。

  1. 粉丝数量和关注数量:这两个状态直接来源于后端 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>
  1. 关注者与粉丝的比例:这个状态是基于粉丝数量和关注数量计算得出的,适合使用 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>

通过这样的选择,使得状态管理清晰,数据来源和计算逻辑明确。

案例二:项目管理应用

在一个项目管理应用中,有项目列表、每个项目的任务列表以及所有任务的总数等状态。

  1. 项目列表和任务列表:项目列表可能会通过用户创建、删除项目等操作直接更新,任务列表也会随着用户添加、完成任务等操作直接变化,因此都适合使用 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}
  1. 所有任务的总数:这个状态是基于所有项目的任务列表计算得出的,适合使用 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>

在这个案例中,通过合理选择 writablederived,使得应用的状态管理符合数据的性质和应用的逻辑。

总结常见误区

  1. 在 derived 中处理副作用:如前文所述,derived 的回调函数应该是纯函数,只进行计算操作。在其中处理副作用会导致意外行为,并且难以调试。副作用应该在订阅 derived 状态时或者在 writable 的更新方法中处理。
  2. 过度使用 derived:虽然 derived 很强大,但不应该在所有情况下都使用它。对于简单的、直接更新的状态,writable 更简单高效。过度使用 derived 可能会增加不必要的计算开销和代码复杂性。
  3. 忽略 writable 的更新方法writable 不仅可以通过 set 直接设置值,update 方法在基于当前值进行更新时非常有用。例如在需要根据当前状态进行复杂更新操作时,update 可以确保更新逻辑的原子性和一致性。
  4. 未正确处理 derived 的多依赖情况:当 derived 依赖多个状态时,要确保回调函数能够正确处理所有依赖状态的变化。在回调函数中要使用解构来获取每个依赖状态的值,并且要考虑到所有可能的状态变化情况,以避免计算错误。

通过清晰理解 writablederived 的特性、应用场景以及选择策略,开发者可以在 Svelte 项目中构建出高效、可维护的状态管理体系。在实际项目中,要根据具体的业务需求和数据特点,灵活选择合适的状态管理方式,以实现最佳的开发效果。