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

Svelte 状态管理与异步操作:处理异步数据流

2021-11-271.7k 阅读

Svelte 状态管理与异步操作:处理异步数据流

在现代前端开发中,处理异步操作和管理状态是构建复杂应用的关键。Svelte 作为一款轻量级的前端框架,为开发者提供了优雅且高效的方式来处理这两个方面。本文将深入探讨 Svelte 中如何进行状态管理以及如何处理异步数据流。

Svelte 状态管理基础

在 Svelte 中,状态管理相对直观。当你声明一个变量,它就成为了组件的状态。例如,在一个简单的计数器组件中:

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

<button on:click={increment}>
    Count: {count}
</button>

这里的 count 就是组件的状态,每次点击按钮,count 增加,Svelte 会自动检测到状态变化并更新 DOM。

响应式声明 Svelte 使用 $: 符号来创建响应式声明。例如:

<script>
    let a = 1;
    let b;
    $: b = a * 2;
</script>

<p>a: {a}</p>
<p>b: {b}</p>

每当 a 的值发生变化,b 会自动重新计算。这种响应式声明非常适合在状态之间建立关联关系。

存储(Stores) Svelte 的存储是一种更高级的状态管理机制,特别适用于跨组件共享状态。Svelte 提供了 writablereadablederived 三种类型的存储。

  • writable 存储:可写存储允许你创建一个可变的状态,并提供 subscribesetupdate 方法。
<script>
    import { writable } from'svelte/store';
    const countStore = writable(0);
    const increment = () => {
        countStore.update(n => n + 1);
    };
</script>

<button on:click={increment}>
    Count: {$countStore}
</button>

这里通过 writable 创建了 countStore$countStore 语法用于在组件中订阅存储的值。update 方法接受一个函数,该函数接收当前值并返回新值。

  • readable 存储:可读存储适用于创建不可变的数据源,比如当前时间。
<script>
    import { readable } from'svelte/store';
    const now = readable(new Date(), function start(set) {
        const interval = setInterval(() => {
            set(new Date());
        }, 1000);
        return function stop() {
            clearInterval(interval);
        };
    });
</script>

<p>The time is {$now}</p>

readable 接受初始值和一个可选的启动函数。启动函数返回一个清理函数,在组件卸载时调用。

  • derived 存储:派生存储基于其他存储创建新的存储。例如:
<script>
    import { writable, derived } from'svelte/store';
    const count = writable(0);
    const doubleCount = derived(count, $count => $count * 2);
</script>

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

derived 第一个参数是源存储,第二个参数是一个回调函数,当源存储变化时,回调函数执行并返回新的值。

异步操作基础

在 JavaScript 中,异步操作通常通过 Promiseasync/await 来处理。Svelte 在处理异步操作方面与标准 JavaScript 紧密结合。

async/await 语法 假设我们有一个异步函数 fetchData 来获取一些数据:

<script>
    const fetchData = async () => {
        const response = await fetch('https://example.com/api/data');
        return response.json();
    };
    let data;
    const loadData = async () => {
        data = await fetchData();
    };
</script>

<button on:click={loadData}>Load Data</button>
{#if data}
    <pre>{JSON.stringify(data, null, 2)}</pre>
{/if}

这里 fetchData 是一个异步函数,await 关键字用于等待 Promise 解决。在 loadData 函数中,我们等待 fetchData 完成并将结果赋值给 data

处理异步操作中的加载状态 在实际应用中,我们通常需要显示加载状态。可以通过添加一个标志变量来实现:

<script>
    const fetchData = async () => {
        const response = await fetch('https://example.com/api/data');
        return response.json();
    };
    let data;
    let isLoading = false;
    const loadData = async () => {
        isLoading = true;
        try {
            data = await fetchData();
        } finally {
            isLoading = false;
        }
    };
</script>

<button on:click={loadData}>
    {#if isLoading}
        Loading...
    {:else}
        Load Data
    {/if}
</button>
{#if data}
    <pre>{JSON.stringify(data, null, 2)}</pre>
{/if}

这里 isLoading 用于表示加载状态,在开始加载时设置为 true,加载完成(无论成功或失败)后设置为 false

Svelte 中处理异步数据流

异步存储 我们可以创建一个异步存储来管理异步获取的数据。例如,我们创建一个用于获取用户信息的异步存储:

<script>
    import { writable } from'svelte/store';
    const fetchUser = async () => {
        const response = await fetch('https://example.com/api/user');
        return response.json();
    };
    const userStore = (function () {
        const { subscribe, set, update } = writable(null);
        const loadUser = async () => {
            const user = await fetchUser();
            set(user);
        };
        return {
            subscribe,
            loadUser
        };
    })();
</script>

<button on:click={() => userStore.loadUser()}>Load User</button>
{#if $userStore}
    <pre>{JSON.stringify($userStore, null, 2)}</pre>
{/if}

这里通过闭包创建了 userStore,它具有 subscribeloadUser 方法。loadUser 方法用于异步获取用户数据并设置到存储中。

处理异步操作的多个状态 除了加载状态,我们还可能需要处理错误状态。可以进一步扩展上述的异步存储:

<script>
    import { writable } from'svelte/store';
    const fetchUser = async () => {
        const response = await fetch('https://example.com/api/user');
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        return response.json();
    };
    const userStore = (function () {
        const { subscribe, set, update } = writable({
            data: null,
            isLoading: false,
            error: null
        });
        const loadUser = async () => {
            update(state => ({...state, isLoading: true }));
            try {
                const user = await fetchUser();
                set({...$userStore, data: user, isLoading: false });
            } catch (error) {
                set({...$userStore, error, isLoading: false });
            }
        };
        return {
            subscribe,
            loadUser
        };
    })();
</script>

<button on:click={() => userStore.loadUser()}>
    {#if $userStore.isLoading}
        Loading...
    {:else if $userStore.error}
        Error: {$userStore.error.message}
    {:else}
        Load User
    {/if}
</button>
{#if $userStore.data}
    <pre>{JSON.stringify($userStore.data, null, 2)}</pre>
{/if}

这里 userStore 存储包含了 data(用户数据)、isLoading(加载状态)和 error(错误信息)。loadUser 方法在异步操作过程中更新这些状态。

使用 derived 处理异步依赖 假设我们有两个异步存储,一个用于获取用户信息,另一个用于获取用户的订单列表。并且订单列表依赖于用户信息中的用户 ID。

<script>
    import { writable, derived } from'svelte/store';
    const fetchUser = async () => {
        const response = await fetch('https://example.com/api/user');
        return response.json();
    };
    const fetchOrders = async (userId) => {
        const response = await fetch(`https://example.com/api/orders?userId=${userId}`);
        return response.json();
    };
    const userStore = (function () {
        const { subscribe, set, update } = writable(null);
        const loadUser = async () => {
            const user = await fetchUser();
            set(user);
        };
        return {
            subscribe,
            loadUser
        };
    })();
    const orderStore = derived(userStore, async ($user, set) => {
        if (!$user) {
            set(null);
            return;
        }
        const orders = await fetchOrders($user.id);
        set(orders);
    });
</script>

<button on:click={() => userStore.loadUser()}>Load User and Orders</button>
{#if $userStore}
    <p>User: {JSON.stringify($userStore, null, 2)}</p>
{/if}
{#if $orderStore}
    <p>Orders: {JSON.stringify($orderStore, null, 2)}</p>
{/if}

这里 orderStore 是基于 userStore 派生的。当 userStore 中的用户信息加载完成后,orderStore 会根据用户 ID 异步获取订单列表。

异步操作与响应式声明

在 Svelte 中,异步操作和响应式声明可以很好地结合。例如,我们有一个输入框,当用户输入时,根据输入内容异步搜索数据:

<script>
    import { onMount } from'svelte';
    const fetchSearchResults = async (query) => {
        const response = await fetch(`https://example.com/api/search?q=${query}`);
        return response.json();
    };
    let query = '';
    let results;
    $: {
        if (query) {
            (async () => {
                results = await fetchSearchResults(query);
            })();
        } else {
            results = null;
        }
    }
</script>

<input type="text" bind:value={query} placeholder="Search...">
{#if results}
    <pre>{JSON.stringify(results, null, 2)}</pre>
{/if}

这里通过响应式声明,当 query 发生变化时,会根据 query 的值异步获取搜索结果。

使用 await 在响应式声明中 在 Svelte 3.23.0 及更高版本中,你可以在响应式声明中直接使用 await

<script>
    import { onMount } from'svelte';
    const fetchSearchResults = async (query) => {
        const response = await fetch(`https://example.com/api/search?q=${query}`);
        return response.json();
    };
    let query = '';
    let results;
    $: results = query? await fetchSearchResults(query) : null;
</script>

<input type="text" bind:value={query} placeholder="Search...">
{#if results}
    <pre>{JSON.stringify(results, null, 2)}</pre>
{/if}

这种方式使得异步操作在响应式声明中更加简洁。

处理复杂异步场景

并发异步操作 有时候我们需要同时发起多个异步操作,并等待所有操作完成。例如,我们需要同时获取用户信息和用户设置:

<script>
    import { writable } from'svelte/store';
    const fetchUser = async () => {
        const response = await fetch('https://example.com/api/user');
        return response.json();
    };
    const fetchSettings = async () => {
        const response = await fetch('https://example.com/api/settings');
        return response.json();
    };
    const userStore = writable(null);
    const settingsStore = writable(null);
    const loadData = async () => {
        const [user, settings] = await Promise.all([fetchUser(), fetchSettings()]);
        userStore.set(user);
        settingsStore.set(settings);
    };
</script>

<button on:click={loadData}>Load User and Settings</button>
{#if $userStore}
    <p>User: {JSON.stringify($userStore, null, 2)}</p>
{/if}
{#if $settingsStore}
    <p>Settings: {JSON.stringify($settingsStore, null, 2)}</p>
{/if}

这里使用 Promise.all 来并发执行 fetchUserfetchSettings,并在两个操作都完成后更新相应的存储。

顺序异步操作 在某些情况下,我们需要按顺序执行异步操作。例如,先获取用户信息,然后根据用户信息中的令牌获取用户的详细资料:

<script>
    import { writable } from'svelte/store';
    const fetchUser = async () => {
        const response = await fetch('https://example.com/api/user');
        return response.json();
    };
    const fetchProfile = async (token) => {
        const response = await fetch(`https://example.com/api/profile?token=${token}`);
        return response.json();
    };
    const userStore = writable(null);
    const profileStore = writable(null);
    const loadData = async () => {
        const user = await fetchUser();
        const profile = await fetchProfile(user.token);
        userStore.set(user);
        profileStore.set(profile);
    };
</script>

<button on:click={loadData}>Load User and Profile</button>
{#if $userStore}
    <p>User: {JSON.stringify($userStore, null, 2)}</p>
{/if}
{#if $profileStore}
    <p>Profile: {JSON.stringify($profileStore, null, 2)}</p>
{/if}

这里先执行 fetchUser,获取用户信息后,再使用用户信息中的令牌执行 fetchProfile

错误处理在复杂异步场景中 在并发和顺序异步操作中,正确处理错误非常重要。在并发操作中,Promise.all 会在任何一个 Promise 被拒绝时立即拒绝:

<script>
    import { writable } from'svelte/store';
    const fetchUser = async () => {
        const response = await fetch('https://example.com/api/user');
        if (!response.ok) {
            throw new Error('User fetch failed');
        }
        return response.json();
    };
    const fetchSettings = async () => {
        const response = await fetch('https://example.com/api/settings');
        if (!response.ok) {
            throw new Error('Settings fetch failed');
        }
        return response.json();
    };
    const userStore = writable(null);
    const settingsStore = writable(null);
    const errorStore = writable(null);
    const loadData = async () => {
        try {
            const [user, settings] = await Promise.all([fetchUser(), fetchSettings()]);
            userStore.set(user);
            settingsStore.set(settings);
        } catch (error) {
            errorStore.set(error);
        }
    };
</script>

<button on:click={loadData}>Load User and Settings</button>
{#if $errorStore}
    <p>Error: {$errorStore.message}</p>
{:else if $userStore && $settingsStore}
    <p>User: {JSON.stringify($userStore, null, 2)}</p>
    <p>Settings: {JSON.stringify($settingsStore, null, 2)}</p>
{/if}

在顺序操作中,错误处理类似,在每个异步函数中捕获并处理错误,或者在调用链的上层统一处理:

<script>
    import { writable } from'svelte/store';
    const fetchUser = async () => {
        const response = await fetch('https://example.com/api/user');
        if (!response.ok) {
            throw new Error('User fetch failed');
        }
        return response.json();
    };
    const fetchProfile = async (token) => {
        const response = await fetch(`https://example.com/api/profile?token=${token}`);
        if (!response.ok) {
            throw new Error('Profile fetch failed');
        }
        return response.json();
    };
    const userStore = writable(null);
    const profileStore = writable(null);
    const errorStore = writable(null);
    const loadData = async () => {
        try {
            const user = await fetchUser();
            const profile = await fetchProfile(user.token);
            userStore.set(user);
            profileStore.set(profile);
        } catch (error) {
            errorStore.set(error);
        }
    };
</script>

<button on:click={loadData}>Load User and Profile</button>
{#if $errorStore}
    <p>Error: {$errorStore.message}</p>
{:else if $userStore && $profileStore}
    <p>User: {JSON.stringify($userStore, null, 2)}</p>
    <p>Profile: {JSON.stringify($profileStore, null, 2)}</p>
{/if}

性能优化与异步操作

节流与防抖 在处理异步操作时,尤其是在用户输入触发异步搜索等场景下,节流和防抖是常用的性能优化手段。

  • 防抖(Debounce):防抖意味着在一定时间内如果事件再次触发,会重新计时。例如,在搜索框输入时,我们不想每次输入都立即发起搜索请求:
<script>
    import { onMount } from'svelte';
    const fetchSearchResults = async (query) => {
        const response = await fetch(`https://example.com/api/search?q=${query}`);
        return response.json();
    };
    let query = '';
    let results;
    let debounceTimer;
    const debouncedSearch = (newQuery) => {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(async () => {
            results = await fetchSearchResults(newQuery);
        }, 300);
    };
</script>

<input type="text" bind:value={query} on:input={e => debouncedSearch(e.target.value)} placeholder="Search...">
{#if results}
    <pre>{JSON.stringify(results, null, 2)}</pre>
{/if}

这里使用 setTimeout 实现了防抖逻辑,只有在用户停止输入 300 毫秒后才会发起搜索请求。

  • 节流(Throttle):节流是指在一定时间内,只允许事件触发一次。例如,在滚动事件中,我们不想频繁触发异步加载更多数据的操作:
<script>
    import { onMount } from'svelte';
    const fetchMoreData = async () => {
        const response = await fetch('https://example.com/api/more-data');
        return response.json();
    };
    let data = [];
    let throttleTimer;
    const throttledLoadMore = () => {
        if (throttleTimer) return;
        throttleTimer = setTimeout(async () => {
            const newData = await fetchMoreData();
            data = [...data, ...newData];
            clearTimeout(throttleTimer);
            throttleTimer = null;
        }, 500);
    };
    onMount(() => {
        window.addEventListener('scroll', throttledLoadMore);
        return () => {
            window.removeEventListener('scroll', throttledLoadMore);
        };
    });
</script>

{#each data as item}
    <p>{item}</p>
{/each}

这里通过 setTimeout 和一个标志变量实现了节流逻辑,每 500 毫秒允许触发一次加载更多数据的操作。

取消异步操作 在某些情况下,我们可能需要取消正在进行的异步操作。例如,在用户切换页面时,取消正在进行的异步数据获取。在 JavaScript 中,AbortController 可以用于取消 fetch 操作:

<script>
    import { onDestroy } from'svelte';
    const fetchData = async (signal) => {
        const response = await fetch('https://example.com/api/data', { signal });
        return response.json();
    };
    let data;
    let controller;
    const loadData = async () => {
        controller = new AbortController();
        const { signal } = controller;
        try {
            data = await fetchData(signal);
        } catch (error) {
            if (error.name === 'AbortError') {
                console.log('Fetch aborted');
            } else {
                console.error('Fetch error:', error);
            }
        }
    };
    onDestroy(() => {
        controller?.abort();
    });
</script>

<button on:click={loadData}>Load Data</button>
{#if data}
    <pre>{JSON.stringify(data, null, 2)}</pre>
{/if}

这里通过 AbortController 创建了一个信号,在组件销毁时,使用 abort 方法取消正在进行的 fetch 操作。

与第三方库集成处理异步数据流

在实际项目中,我们经常需要与第三方库集成,这些库可能也涉及异步操作。例如,使用 Chart.js 绘制图表时,数据可能需要异步获取。

<script>
    import { onMount } from'svelte';
    import { Chart } from 'chart.js';
    const fetchChartData = async () => {
        const response = await fetch('https://example.com/api/chart-data');
        return response.json();
    };
    let chart;
    onMount(async () => {
        const data = await fetchChartData();
        const ctx = document.getElementById('myChart').getContext('2d');
        chart = new Chart(ctx, {
            type: 'bar',
            data: {
                labels: data.labels,
                datasets: [{
                    label: 'My First Dataset',
                    data: data.values,
                    backgroundColor: [
                        'rgba(255, 99, 132, 0.2)',
                        'rgba(54, 162, 235, 0.2)',
                        'rgba(255, 206, 86, 0.2)',
                        'rgba(75, 192, 192, 0.2)',
                        'rgba(153, 102, 255, 0.2)',
                        'rgba(255, 159, 64, 0.2)'
                    ],
                    borderColor: [
                        'rgb(255, 99, 132)',
                        'rgb(54, 162, 235)',
                        'rgb(255, 206, 86)',
                        'rgb(75, 192, 192)',
                        'rgb(153, 102, 255)',
                        'rgb(255, 159, 64)'
                    ],
                    borderWidth: 1
                }]
            },
            options: {
                scales: {
                    y: {
                        beginAtZero: true
                    }
                }
            }
        });
    });
</script>

<canvas id="myChart"></canvas>

这里先异步获取图表数据,然后在数据获取完成后使用 Chart.js 绘制图表。

处理第三方库的异步回调 有些第三方库可能使用回调函数来处理异步操作。例如,Google Maps API

<script>
    import { onMount } from'svelte';
    const loadMap = () => {
        return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = `https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap`;
            script.async = true;
            script.defer = true;
            window.initMap = () => {
                const map = new window.google.maps.Map(document.getElementById('map'), {
                    center: { lat: -34.397, lng: 150.644 },
                    zoom: 8
                });
                resolve(map);
            };
            script.onerror = reject;
            document.head.appendChild(script);
        });
    };
    let map;
    onMount(async () => {
        map = await loadMap();
    });
</script>

<div id="map" style="height: 400px;"></div>

这里通过将 Google Maps API 的加载过程包装成 Promise,以便在 Svelte 中更方便地处理异步操作。

通过上述内容,我们深入了解了 Svelte 中状态管理与异步操作处理异步数据流的各种方式,从基础的状态声明到复杂的异步场景,以及性能优化和与第三方库的集成,希望这些知识能帮助你构建更强大、高效的前端应用。