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

Svelte 状态管理:高效处理应用状态

2022-11-066.6k 阅读

Svelte 状态管理基础概念

在 Svelte 应用开发中,状态管理是关键环节。状态,简单来说,就是应用程序在运行过程中随时可能改变的数据。比如一个待办事项应用,任务列表、任务的完成状态等都属于状态。有效的状态管理能让应用更易于维护、扩展以及实现交互逻辑。

Svelte 与其他框架如 React、Vue 不同,它的状态管理有着自身独特的机制。在 React 中,通常通过 setState 或者 useState 等方式来更新状态,并且需要手动处理状态变化带来的 DOM 更新。Vue 则通过数据劫持和发布 - 订阅模式来管理状态。而 Svelte 在编译阶段就将状态变化与 DOM 更新紧密关联,使得状态管理更加直接和高效。

声明状态变量

在 Svelte 中声明状态变量非常简单。例如,我们创建一个简单的计数器应用:

<script>
    let count = 0;
</script>

<button on:click={() => count++}>Increment</button>
<p>The count is {count}</p>

在上述代码中,首先通过 let count = 0 声明了一个状态变量 count,初始值为 0。然后在按钮的点击事件中,通过 count++ 来更新 count 的值。Svelte 会自动检测到 count 的变化,并实时更新页面上显示 count 值的 <p> 标签。

响应式声明

Svelte 提供了响应式声明的功能,当状态变量发生变化时,相关的表达式或语句会自动重新执行。例如:

<script>
    let a = 5;
    let b = 10;
    $: c = a + b;
</script>

<button on:click={() => a++}>Increment a</button>
<p>a: {a}</p>
<p>b: {b}</p>
<p>c: {c}</p>

这里使用 $: 符号来创建一个响应式声明,当 ab 的值发生变化时,c = a + b 这条语句会自动重新执行,从而更新 c 的值,页面上显示 c 的部分也会随之更新。

组件间状态共享

在大型应用中,组件间状态共享是不可避免的。Svelte 提供了多种方式来实现这一需求。

父子组件通信

  1. 父传子:父组件可以通过向子组件传递属性(props)来共享状态。例如,创建一个父组件 Parent.svelte 和子组件 Child.svelte
    • Child.svelte
<script>
    export let message;
</script>

<p>{message}</p>
- `Parent.svelte`:
<script>
    import Child from './Child.svelte';
    let text = 'Hello from parent';
</script>

<Child message={text} />

Parent.svelte 中,通过 message={text}text 传递给 Child.svelteChild.svelte 中通过 export let message 接收这个属性并显示。

  1. 子传父:子组件可以通过事件来向父组件传递数据。继续上面的例子,在 Child.svelte 中添加一个按钮,点击按钮向父组件传递数据。
    • Child.svelte
<script>
    export let message;
    const sendDataToParent = () => {
        const newData = 'Data from child';
        $: dispatch('custom-event', newData);
    };
</script>

<button on:click={sendDataToParent}>Send data to parent</button>
<p>{message}</p>
- `Parent.svelte`:
<script>
    import Child from './Child.svelte';
    let text = 'Hello from parent';
    const handleChildEvent = (event) => {
        text = event.detail;
    };
</script>

<Child message={text} on:custom-event={handleChildEvent} />
<p>{text}</p>

Child.svelte 中,通过 dispatch('custom - event', newData) 触发一个自定义事件,并传递数据 newData。在 Parent.svelte 中,通过 on:custom - event={handleChildEvent} 监听这个事件,并在 handleChildEvent 函数中更新 text 的值。

非父子组件通信

  1. 通过共享 store:Svelte 的 store 是一种用于管理共享状态的机制。可以使用 svelte/store 模块来创建 store。例如,创建一个名为 sharedStore.js 的文件:
import { writable } from'svelte/store';

export const sharedData = writable('Initial shared data');

然后在不同的组件中使用这个 store。假设我们有 ComponentA.svelteComponentB.svelte。 - ComponentA.svelte

<script>
    import { sharedData } from './sharedStore.js';
    const updateSharedData = () => {
        sharedData.update(data => 'Updated from ComponentA');
    };
</script>

<button on:click={updateSharedData}>Update shared data from A</button>
- `ComponentB.svelte`:
<script>
    import { sharedData } from './sharedStore.js';
    let data;
    sharedData.subscribe(value => data = value);
</script>

<p>Shared data in B: {data}</p>

ComponentA.svelte 中,通过 sharedData.update 方法来更新共享状态。在 ComponentB.svelte 中,通过 sharedData.subscribe 方法订阅共享状态的变化,并更新本地变量 data 以显示最新的值。

  1. 事件总线模式:虽然 Svelte 官方没有直接提供事件总线的实现,但可以通过自定义事件和一个全局的事件分发器来模拟。例如,创建一个 EventBus.js 文件:
const eventBus = {
    events: {},
    on(eventName, callback) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        this.events[eventName].push(callback);
    },
    emit(eventName, data) {
        if (this.events[eventName]) {
            this.events[eventName].forEach(callback => callback(data));
        }
    }
};

export default eventBus;

然后在组件中使用事件总线。假设我们有 ComponentC.svelteComponentD.svelte。 - ComponentC.svelte

<script>
    import eventBus from './EventBus.js';
    const sendEvent = () => {
        eventBus.emit('custom - event - from - c', 'Data from ComponentC');
    };
</script>

<button on:click={sendEvent}>Send event from C</button>
- `ComponentD.svelte`:
<script>
    import eventBus from './EventBus.js';
    let receivedData;
    eventBus.on('custom - event - from - c', data => {
        receivedData = data;
    });
</script>

<p>Data received from C in D: {receivedData}</p>

ComponentC.svelte 中,通过 eventBus.emit 触发一个自定义事件并传递数据。在 ComponentD.svelte 中,通过 eventBus.on 监听这个事件并处理接收到的数据。

复杂状态管理场景

嵌套状态管理

在实际应用中,状态可能是嵌套的。例如,一个电商应用可能有一个商品列表,每个商品又有自己的详细信息,如价格、库存等。假设我们有一个 ProductList.svelte 组件来展示商品列表,每个商品是一个 Product.svelte 组件。

  1. 创建 Product.svelte
<script>
    export let product;
    const increaseStock = () => {
        product.stock++;
    };
</script>

<p>{product.name}</p>
<p>Price: {product.price}</p>
<p>Stock: {product.stock}</p>
<button on:click={increaseStock}>Increase stock</button>
  1. 创建 ProductList.svelte
<script>
    import Product from './Product.svelte';
    let products = [
        { name: 'Product 1', price: 10, stock: 5 },
        { name: 'Product 2', price: 15, stock: 3 }
    ];
</script>

{#each products as product}
    <Product product={product} />
{/each}

在这个例子中,ProductList.svelte 管理着一个商品数组,每个商品的详细信息是嵌套在数组元素中的对象。Product.svelte 组件可以直接操作和显示每个商品的状态,而 ProductList.svelte 负责整体商品列表的管理。

异步状态管理

在处理异步操作时,如 API 调用,状态管理也需要特殊处理。假设我们要从一个 API 获取用户列表,并在页面上显示。

  1. 创建 UserList.svelte
<script>
    let users = [];
    let isLoading = false;
    const fetchUsers = async () => {
        isLoading = true;
        try {
            const response = await fetch('https://example.com/api/users');
            const data = await response.json();
            users = data;
        } catch (error) {
            console.error('Error fetching users:', error);
        } finally {
            isLoading = false;
        }
    };

    // 页面加载时自动获取用户
    onMount(() => {
        fetchUsers();
    });
</script>

{#if isLoading}
    <p>Loading...</p>
{:else if users.length > 0}
    <ul>
        {#each users as user}
            <li>{user.name}</li>
        {/each}
    </ul>
{:else}
    <p>No users found.</p>
{/if}

在上述代码中,isLoading 用于表示数据是否正在加载,users 用于存储从 API 获取到的用户数据。在 fetchUsers 函数中,通过 await 处理异步操作,并在合适的时机更新 isLoadingusers 的状态。通过 {#if} 块根据不同的状态显示相应的内容。

状态持久化

在一些应用中,需要将状态持久化,例如用户的设置、购物车内容等,以便用户下次打开应用时能恢复到之前的状态。可以使用浏览器的本地存储(localStorage)来实现。

  1. 保存状态到本地存储
<script>
    let settings = {
        theme: 'light',
        fontSize: 16
    };

    const saveSettingsToLocalStorage = () => {
        localStorage.setItem('settings', JSON.stringify(settings));
    };

    // 监听 settings 的变化并保存
    $: saveSettingsToLocalStorage();
</script>
  1. 从本地存储加载状态
<script>
    let settings;
    const loadSettingsFromLocalStorage = () => {
        const storedSettings = localStorage.getItem('settings');
        if (storedSettings) {
            settings = JSON.parse(storedSettings);
        } else {
            settings = {
                theme: 'light',
                fontSize: 16
            };
        }
    };

    // 页面加载时加载设置
    onMount(() => {
        loadSettingsFromLocalStorage();
    });
</script>

在上述代码中,首先通过 localStorage.setItemsettings 对象转换为 JSON 字符串并保存到本地存储。然后在页面加载时,通过 localStorage.getItem 获取存储的设置,并转换回对象形式。通过 onMount 钩子函数确保在组件挂载时执行加载操作。

Svelte 状态管理库

除了 Svelte 自带的状态管理机制,还有一些优秀的状态管理库可以进一步提升开发效率。

MobX - Svelte

MobX 是一个流行的状态管理库,与 Svelte 结合可以提供更强大的状态管理功能。首先安装 mobxmobx - svelte

npm install mobx mobx - svelte
  1. 创建 MobX store
import { makeObservable, observable, action } from'mobx';

class CounterStore {
    constructor() {
        this.count = 0;
        makeObservable(this, {
            count: observable,
            increment: action
        });
    }

    increment() {
        this.count++;
    }
}

export const counterStore = new CounterStore();
  1. 在 Svelte 组件中使用 MobX store
<script>
    import { useStore } from'mobx - svelte';
    import { counterStore } from './CounterStore.js';

    const store = useStore(counterStore);
</script>

<button on:click={() => store.increment()}>Increment with MobX</button>
<p>Count with MobX: {store.count}</p>

在上述代码中,首先创建了一个 MobX store CounterStore,其中 count 是可观察的状态,increment 是用于更新状态的 action。然后在 Svelte 组件中,通过 useStore 钩子函数将 MobX store 引入,并在按钮点击事件中调用 increment 方法更新状态,同时在页面上显示 count 的值。

Zustand

Zustand 是另一个轻量级的状态管理库,非常适合 Svelte 应用。安装 zustand

npm install zustand
  1. 创建 Zustand store
import create from 'zustand';

const useCounterStore = create(set => ({
    count: 0,
    increment: () => set(state => ({ count: state.count + 1 }))
}));

export default useCounterStore;
  1. 在 Svelte 组件中使用 Zustand store
<script>
    import { onMount } from'svelte';
    import useCounterStore from './useCounterStore.js';

    const { count, increment } = useCounterStore();

    onMount(() => {
        // 可以在组件挂载时执行一些操作
    });
</script>

<button on:click={increment}>Increment with Zustand</button>
<p>Count with Zustand: {count}</p>

在这个例子中,通过 create 函数创建了一个 Zustand store useCounterStore,包含 count 状态和 increment 方法。在 Svelte 组件中,通过解构获取 countincrement,并在按钮点击事件中调用 increment 来更新状态,同时显示 count 的值。

性能优化与状态管理

避免不必要的状态更新

在 Svelte 中,虽然状态更新与 DOM 更新的关联很高效,但如果频繁进行不必要的状态更新,仍然会影响性能。例如,在一个包含大量数据的列表中,如果每次更新一个小的状态变化都导致整个列表重新渲染,就会造成性能浪费。

  1. 优化前
<script>
    let items = Array.from({ length: 1000 }, (_, i) => ({ id: i, value: `Item ${i}` }));
    let filterText = '';

    const handleFilterChange = (event) => {
        filterText = event.target.value;
    };
</script>

<input type="text" bind:value={filterText} placeholder="Filter items" />

{#each items.filter(item => item.value.includes(filterText)) as item}
    <p>{item.value}</p>
{/each}

在这个例子中,每次 filterText 变化时,items.filter 操作会重新执行,即使 items 本身没有变化。这可能会导致不必要的计算和重新渲染。

  1. 优化后
<script>
    let items = Array.from({ length: 1000 }, (_, i) => ({ id: i, value: `Item ${i}` }));
    let filterText = '';
    let filteredItems;

    const handleFilterChange = (event) => {
        filterText = event.target.value;
        filteredItems = items.filter(item => item.value.includes(filterText));
    };

    // 初始化时进行一次过滤
    $: filteredItems = items.filter(item => item.value.includes(filterText));
</script>

<input type="text" bind:value={filterText} placeholder="Filter items" />

{#each filteredItems as item}
    <p>{item.value}</p>
{/each}

优化后,通过提前计算并缓存 filteredItems,只有当 filterTextitems 真正发生变化时,才会重新计算 filteredItems,避免了不必要的重复计算。

批量状态更新

在某些情况下,可能需要同时更新多个状态变量。如果逐个更新,可能会导致多次 DOM 更新,影响性能。Svelte 虽然会尽量优化这种情况,但手动进行批量更新可以进一步提升性能。

  1. 优化前
<script>
    let a = 0;
    let b = 0;

    const updateValues = () => {
        a++;
        b++;
    };
</script>

<button on:click={updateValues}>Update values</button>
<p>a: {a}</p>
<p>b: {b}</p>

在这个例子中,ab 分别更新,可能会导致两次 DOM 更新。

  1. 优化后
<script>
    let a = 0;
    let b = 0;

    const updateValues = () => {
        let tempA = a + 1;
        let tempB = b + 1;
        a = tempA;
        b = tempB;
    };
</script>

<button on:click={updateValues}>Update values</button>
<p>a: {a}</p>
<p>b: {b}</p>

优化后,通过先计算临时变量,然后一次性更新 ab,这样只会触发一次 DOM 更新,提高了性能。

状态管理与测试

在 Svelte 应用开发中,对状态管理部分进行测试是确保应用质量的重要环节。可以使用一些测试框架,如 Vitest 来进行测试。

测试状态变量

假设我们有一个简单的 Counter.svelte 组件,测试其状态变量 count 的初始值和更新逻辑。

  1. Counter.svelte
<script>
    let count = 0;
    const increment = () => {
        count++;
    };
</script>

<button on:click={increment}>Increment</button>
<p>Count: {count}</p>
  1. 测试文件 Counter.test.js
import { render, fireEvent } from '@testing - library/svelte';
import Counter from './Counter.svelte';

describe('Counter component', () => {
    it('should have initial count of 0', () => {
        const { getByText } = render(Counter);
        expect(getByText('Count: 0')).toBeInTheDocument();
    });

    it('should increment count on button click', () => {
        const { getByText } = render(Counter);
        fireEvent.click(getByText('Increment'));
        expect(getByText('Count: 1')).toBeInTheDocument();
    });
});

在上述测试中,首先使用 render 函数渲染 Counter.svelte 组件,然后通过 getByText 获取页面上的元素,并使用 expect 进行断言。第一个测试用例验证 count 的初始值为 0,第二个测试用例验证点击按钮后 count 会增加。

测试响应式声明

对于包含响应式声明的组件,同样可以进行测试。假设我们有一个 Sum.svelte 组件,测试其响应式计算的结果。

  1. Sum.svelte
<script>
    let a = 5;
    let b = 10;
    $: c = a + b;
</script>

<button on:click={() => a++}>Increment a</button>
<p>a: {a}</p>
<p>b: {b}</p>
<p>c: {c}</p>
  1. 测试文件 Sum.test.js
import { render, fireEvent } from '@testing - library/svelte';
import Sum from './Sum.svelte';

describe('Sum component', () => {
    it('should calculate correct sum initially', () => {
        const { getByText } = render(Sum);
        expect(getByText('c: 15')).toBeInTheDocument();
    });

    it('should recalculate sum when a is incremented', () => {
        const { getByText } = render(Sum);
        fireEvent.click(getByText('Increment a'));
        expect(getByText('c: 16')).toBeInTheDocument();
    });
});

这里的测试用例分别验证了初始状态下响应式计算的 c 值是否正确,以及当 a 变化时 c 是否能正确重新计算。

通过对状态管理的各个方面进行测试,可以确保应用在不同状态变化下的正确性和稳定性。

在 Svelte 开发中,深入理解和掌握状态管理的各种技巧和工具,对于构建高效、可维护的应用至关重要。从基础的状态声明和响应式,到复杂的组件间状态共享、异步状态处理,再到性能优化和测试,每一个环节都紧密相连,共同构成了一个完整的状态管理体系。无论是小型项目还是大型应用,合理运用这些知识都能帮助开发者更好地实现业务需求,提升用户体验。