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

Svelte writable store源码解析及优化技巧

2024-09-115.6k 阅读

Svelte writable store 基础概念

在 Svelte 应用开发中,状态管理是至关重要的一环。Svelte 提供了不同类型的 store 来帮助开发者管理应用的状态,其中 writable store 是最为常用的一种。

简单来说,writable store 是一个可写的状态容器,它允许我们在应用的不同部分读取和修改状态,并且能够自动通知依赖于该状态的组件进行更新。这一机制极大地简化了前端应用中状态管理的复杂度,避免了像传统 React 或 Vue 那样需要手动处理状态变更和组件更新的繁琐流程。

创建 writable store

在 Svelte 中,通过 writable 函数来创建一个 writable store。下面是一个简单的示例:

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

    // 创建一个名为 count 的 writable store,初始值为 0
    const count = writable(0);
</script>

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

在上述代码中,首先从 svelte/store 导入 writable 函数,然后使用它创建了一个名为 countwritable store,初始值设为 0。在按钮的点击事件中,通过 count.update 方法来更新 count 的值,这里 $count 用于在组件中读取 count 的当前值。

Svelte writable store 源码解析

要深入理解 writable store 的工作原理,我们需要剖析其源码。以下是简化后的 writable 函数实现:

function writable(start) {
    let value = start;
    const subscribers = new Set();

    const subscribe = (run, invalidate) => {
        subscribers.add(run);

        return () => {
            subscribers.delete(run);
        };
    };

    const set = updatedValue => {
        value = updatedValue;
        subscribers.forEach(run => run(value));
    };

    const update = fn => {
        set(fn(value));
    };

    return { subscribe, set, update };
}
  1. 初始化
    • writable 函数接收一个初始值 start,并在函数内部定义了一个变量 value 来存储当前状态值,初始化为 start
    • 创建了一个 Set 类型的 subscribers,用于存储所有订阅该 store 的回调函数。Set 的特性保证了订阅者的唯一性,避免重复订阅。
  2. subscribe 方法
    • subscribe 方法用于订阅 store 的状态变化。它接收两个参数,run 是一个回调函数,当状态发生变化时会被调用,invalidate 是一个可选的清理函数(在某些复杂场景下使用,这里暂不深入讨论)。
    • 该方法将 run 回调函数添加到 subscribers 集合中,并返回一个取消订阅的函数。当调用这个返回的函数时,会将 runsubscribers 中删除,从而实现取消订阅的功能。
  3. set 方法
    • set 方法用于直接设置 store 的新值。它接收一个新的状态值 updatedValue,将内部的 value 更新为 updatedValue,然后遍历 subscribers 集合,调用每个订阅者的 run 函数,并将新的 value 作为参数传递给它们。这样,所有依赖于该 store 的组件就会收到状态更新的通知,进而进行重新渲染。
  4. update 方法
    • update 方法用于基于当前状态值来更新 store。它接收一个函数 fn,该函数以当前的 value 作为参数,并返回一个新的值。update 方法内部调用 set 方法,并将 fn(value) 的结果作为新值传递给 set,从而实现基于当前状态的更新操作。

Svelte writable store 优化技巧

  1. 减少不必要的订阅
    • 在复杂的应用中,可能会有大量的组件订阅同一个 writable store。如果某些组件只在特定条件下才需要关注 store 的变化,那么在这些组件不需要时取消订阅可以避免不必要的计算和渲染。
    • 例如,在一个有分页功能的列表组件中,只有当用户切换到当前页面时,该列表组件才需要订阅数据 store。可以在组件的 onMountonDestroy 生命周期函数中进行订阅和取消订阅操作。
<script>
    import { writable, onMount, onDestroy } from'svelte/store';

    const dataStore = writable([]);

    let unsubscribe;
    onMount(() => {
        unsubscribe = dataStore.subscribe(data => {
            // 处理数据更新逻辑
        });
    });

    onDestroy(() => {
        if (unsubscribe) {
            unsubscribe();
        }
    });
</script>
  1. 批处理更新
    • 当需要对 writable store 进行多次更新时,如果每次更新都触发订阅者的回调,可能会导致性能问题。可以使用 svelte/store 中的 batch 函数来批处理更新。
    • batch 函数接收一个回调函数,在这个回调函数内对 store 进行的所有更新操作,只会在回调结束后触发一次订阅者的更新,而不是每次更新都触发。
<script>
    import { writable, batch } from'svelte/store';

    const countStore = writable(0);
    const nameStore = writable('');

    function complexUpdate() {
        batch(() => {
            countStore.update(c => c + 1);
            nameStore.set('new name');
        });
    }
</script>

<button on:click={complexUpdate}>Complex Update</button>
  1. 使用衍生 store 减少重复计算
    • 如果应用中有多个组件依赖于对 writable store 进行相同的计算操作,为了避免每次状态变化时都重复计算,可以创建衍生 store。
    • 例如,假设有一个存储用户列表的 writable store,多个组件需要获取用户列表的长度。可以创建一个衍生 store 来存储用户列表的长度,这样只有当用户列表发生变化时才会重新计算长度。
<script>
    import { writable, derived } from'svelte/store';

    const userList = writable([]);
    const userCount = derived(userList, list => list.length);
</script>

<p>User count: { $userCount }</p>
  1. 缓存 store 值
    • 在某些情况下,store 的值可能不会频繁变化,但组件可能会频繁读取。这时可以在组件内部缓存 store 的值,减少对 store 的直接读取次数。
    • 例如,在一个展示用户信息的组件中,用户信息可能只会在用户登录或修改资料时才会变化,但组件可能会在页面渲染、滚动等场景下多次读取用户信息。可以在组件内部定义一个变量来缓存用户信息。
<script>
    import { writable } from'svelte/store';

    const userInfoStore = writable({});
    let cachedUserInfo;

    userInfoStore.subscribe(info => {
        cachedUserInfo = info;
    });
</script>

<p>User name: { cachedUserInfo.name }</p>
  1. 避免在订阅回调中执行昂贵操作
    • 订阅回调函数在 store 状态变化时会被频繁调用,所以应避免在其中执行复杂的计算、网络请求等昂贵操作。
    • 如果确实需要进行这些操作,可以将它们推迟到下一个微任务或宏任务队列中执行。例如,可以使用 setTimeoutPromise.then 来延迟执行昂贵操作。
<script>
    import { writable } from'svelte/store';

    const dataStore = writable('');

    dataStore.subscribe(data => {
        setTimeout(() => {
            // 执行昂贵操作,如复杂计算或网络请求
        }, 0);
    });
</script>
  1. 优化订阅者回调逻辑
    • 仔细检查订阅者回调函数中的逻辑,确保只执行必要的操作。可以通过条件判断来避免在不需要更新时执行更新逻辑。
    • 例如,在一个根据用户登录状态显示不同 UI 的组件中,只有当用户登录状态发生变化时才需要更新 UI,而不是每次 store 状态变化都更新。
<script>
    import { writable } from'svelte/store';

    const isLoggedInStore = writable(false);

    let previousIsLoggedIn;
    isLoggedInStore.subscribe(isLoggedIn => {
        if (isLoggedIn!== previousIsLoggedIn) {
            // 更新 UI 逻辑
            previousIsLoggedIn = isLoggedIn;
        }
    });
</script>
  1. 使用本地状态代替频繁更新的 store
    • 如果某些状态只在组件内部使用,并且更新频繁,使用组件的本地状态会比使用 writable store 更高效。因为 writable store 的更新会触发所有订阅者的回调,而本地状态的更新只影响当前组件。
    • 例如,在一个输入框组件中,用户输入的实时内容可以使用组件的本地变量存储,只有在用户提交或输入结束等特定时刻,才将值同步到 writable store
<script>
    import { writable } from'svelte/store';

    const formDataStore = writable('');
    let localInputValue = '';

    function handleInput(e) {
        localInputValue = e.target.value;
    }

    function handleSubmit() {
        formDataStore.set(localInputValue);
    }
</script>

<input type="text" bind:value={localInputValue} on:input={handleInput}>
<button on:click={handleSubmit}>Submit</button>
  1. 使用 immer 优化复杂对象更新
    • writable store 存储的是复杂对象时,直接修改对象可能会导致 Svelte 无法正确检测到变化。可以使用 immer 库来优化对象更新。
    • immer 提供了一种更简洁和可靠的方式来更新复杂对象,它基于“草稿状态”的概念,允许我们以一种看似直接修改对象的方式进行操作,但实际上会生成一个新的不可变对象。
<script>
    import { writable } from'svelte/store';
    import produce from 'immer';

    const complexObjectStore = writable({
        nested: {
            value: 1
        }
    });

    function updateComplexObject() {
        complexObjectStore.update(obj => produce(obj, draft => {
            draft.nested.value++;
        }));
    }
</script>

<button on:click={updateComplexObject}>Update Complex Object</button>
  1. 分析性能瓶颈
    • 使用浏览器的性能分析工具,如 Chrome DevTools 的 Performance 面板,来分析应用中 writable store 相关操作的性能瓶颈。
    • 通过性能分析,可以确定哪些订阅回调函数执行时间过长,哪些更新操作过于频繁,从而有针对性地进行优化。
  2. 代码结构优化
    • 合理组织 writable store 的创建和使用,将相关的 store 放在同一个模块中进行管理,避免代码过于分散。
    • 例如,可以创建一个 stores.js 文件,将所有应用级别的 writable store 集中在该文件中定义和导出,方便维护和管理。
// stores.js
import { writable } from'svelte/store';

export const userStore = writable({});
export const settingsStore = writable({});
<script>
    import { userStore, settingsStore } from './stores.js';
</script>
  1. 防抖和节流
    • 如果 writable store 的更新频率过高,可以使用防抖(debounce)或节流(throttle)技术来控制更新频率。
    • 防抖是指在一定时间内,如果再次触发事件,则重新计时,只有在计时结束后才执行实际操作。节流则是指在一定时间内,无论触发多少次事件,都只执行一次实际操作。
    • 以下是一个使用防抖的示例,假设我们有一个搜索框,输入内容会更新 writable store,为了避免频繁更新,可以使用防抖。
<script>
    import { writable } from'svelte/store';
    let timer;

    const searchQueryStore = writable('');

    function debouncedUpdate(query) {
        clearTimeout(timer);
        timer = setTimeout(() => {
            searchQueryStore.set(query);
        }, 300);
    }
</script>

<input type="text" on:input={e => debouncedUpdate(e.target.value)}>
  1. 使用 Svelte 响应式声明式语法优化
    • Svelte 提供了强大的响应式声明式语法,利用好这一特性可以简化 writable store 的使用和优化。
    • 例如,在处理多个 store 之间的关系时,可以使用 $: 语法来创建响应式变量。假设我们有两个 store,一个存储商品价格,一个存储购买数量,我们可以通过响应式变量实时计算总价。
<script>
    import { writable } from'svelte/store';

    const priceStore = writable(10);
    const quantityStore = writable(1);

    $: totalPrice = $priceStore * $quantityStore;
</script>

<p>Total price: { totalPrice }</p>
  1. 延迟加载 store
    • 在一些大型应用中,某些 writable store 可能在应用启动时并不需要立即加载,可以采用延迟加载的方式。
    • 例如,对于一些用户特定的设置 store,只有当用户登录后才需要加载和初始化。可以通过动态导入或条件判断来实现延迟加载。
<script>
    let userSettingsStore;

    async function loadUserSettings() {
        if (!userSettingsStore) {
            const { writable } = await import('svelte/store');
            userSettingsStore = writable({});
            // 从服务器加载用户设置数据并设置到 store 中
        }
    }
</script>
  1. 测试优化
    • 在进行优化后,要通过单元测试和集成测试来确保 writable store 的功能和性能没有受到影响。
    • 例如,使用 Jest 和 Svelte - Testing - Library 来编写测试用例,验证 store 的更新、订阅和取消订阅等功能是否正常。
import { writable } from'svelte/store';
import { render, fireEvent } from '@testing-library/svelte';
import MyComponent from './MyComponent.svelte';

describe('MyComponent', () => {
    it('should update store on button click', () => {
        const countStore = writable(0);
        const { getByText } = render(MyComponent, { countStore });

        fireEvent.click(getByText('Click me'));
        expect(countStore.subscribe).toHaveBeenCalledWith(expect.any(Function));
    });
});
  1. 考虑 SSR 优化
    • 如果应用使用服务器端渲染(SSR),在处理 writable store 时需要特别注意。
    • 例如,在服务器端创建 writable store 时,要确保其初始值的获取方式与客户端一致,避免出现同构问题。同时,可以对 store 的数据进行序列化和反序列化,以便在服务器和客户端之间正确传递状态。
// server.js
import { writable } from'svelte/store';

const initialData = { /* 从数据库或其他数据源获取初始数据 */ };
const myStore = writable(initialData);

export const serializedStore = JSON.stringify({
    value: myStore.get()
});
<!-- client - side -->
<script>
    import { writable } from'svelte/store';

    const { value } = JSON.parse('<%= serializedStore %>');
    const myStore = writable(value);
</script>

通过以上对 Svelte writable store 的源码解析和一系列优化技巧的应用,开发者可以打造出性能更优、结构更清晰的 Svelte 应用,提升用户体验和开发效率。在实际项目中,应根据具体的应用场景和需求,灵活选择和组合这些优化方法,以达到最佳的优化效果。