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

Svelte 组件通信:父子组件之间的数据传递

2021-03-296.2k 阅读

Svelte 父子组件数据传递基础

父组件向子组件传递数据

在 Svelte 中,父组件向子组件传递数据是通过属性(props)来实现的。这是一种非常直观且简洁的方式,与其他前端框架如 React 类似,但又有其独特的简洁语法。

假设我们有一个父组件 App.svelte 和一个子组件 Child.svelte。首先创建 Child.svelte 组件,这个组件将接收父组件传递过来的数据并显示。

<!-- Child.svelte -->
<script>
    export let message;
</script>

<div>
    <p>{message}</p>
</div>

在上述代码中,export let message; 声明了一个名为 message 的属性,它将接收父组件传递过来的值。这里的 export 关键字用于将变量标记为可从外部访问,也就是供父组件传入数据。

接下来看父组件 App.svelte 如何使用这个子组件并传递数据:

<!-- App.svelte -->
<script>
    import Child from './Child.svelte';
    let text = 'Hello from parent';
</script>

<Child message={text} />

App.svelte 中,我们首先导入了 Child.svelte 组件。然后定义了一个变量 text,并将其值设置为 Hello from parent。接着,通过 <Child message={text} />text 的值传递给了子组件 Childmessage 属性。这样,子组件就能接收到父组件传递过来的数据并显示。

动态更新传递的数据

父组件传递给子组件的数据是可以动态更新的。这意味着当父组件中的数据发生变化时,子组件会自动重新渲染以反映这些变化。

修改 App.svelte 如下:

<!-- App.svelte -->
<script>
    import Child from './Child.svelte';
    let text = 'Initial message';
    function updateMessage() {
        text = 'Updated message';
    }
</script>

<Child message={text} />
<button on:click={updateMessage}>Update Message</button>

在上述代码中,我们添加了一个按钮,当点击按钮时,会调用 updateMessage 函数,这个函数会更新 text 的值。由于 text 作为属性传递给了子组件,子组件会自动检测到这个变化并重新渲染,显示新的消息。

传递复杂数据类型

父组件不仅可以传递简单的字符串、数字等基本数据类型,还可以传递对象、数组等复杂数据类型。

假设我们传递一个对象给子组件。先修改 Child.svelte 来接收对象:

<!-- Child.svelte -->
<script>
    export let user;
</script>

<div>
    <p>Name: {user.name}</p>
    <p>Age: {user.age}</p>
</div>

然后在 App.svelte 中传递对象:

<!-- App.svelte -->
<script>
    import Child from './Child.svelte';
    let user = {
        name: 'John',
        age: 30
    };
    function updateUser() {
        user.name = 'Jane';
        user.age = 31;
    }
</script>

<Child user={user} />
<button on:click={updateUser}>Update User</button>

这里我们定义了一个 user 对象,并将其传递给子组件 Child。当点击按钮更新 user 对象时,子组件同样会自动重新渲染,显示更新后的用户信息。

传递数组也是类似的方式。假设子组件要显示一个数组中的元素:

<!-- Child.svelte -->
<script>
    export let numbers;
</script>

<div>
    <ul>
        {#each numbers as number}
            <li>{number}</li>
        {/each}
    </ul>
</div>

App.svelte 中传递数组并更新:

<!-- App.svelte -->
<script>
    import Child from './Child.svelte';
    let numbers = [1, 2, 3];
    function updateNumbers() {
        numbers.push(4);
    }
</script>

<Child numbers={numbers} />
<button on:click={updateNumbers}>Add Number</button>

这样,当点击按钮添加新的数字到数组时,子组件会自动显示更新后的数组内容。

子组件向父组件传递数据

通过事件机制实现子组件向父组件传数据

Svelte 中,子组件向父组件传递数据主要通过自定义事件来实现。当子组件发生特定事件时,它可以触发这个自定义事件,并附带需要传递的数据,父组件可以监听这个事件并获取数据。

首先,在子组件 Child.svelte 中定义并触发自定义事件:

<!-- Child.svelte -->
<script>
    import { createEventDispatcher } from'svelte';
    const dispatch = createEventDispatcher();
    function sendDataToParent() {
        const data = 'Data from child';
        dispatch('childEvent', data);
    }
</script>

<button on:click={sendDataToParent}>Send Data to Parent</button>

在上述代码中,我们通过 createEventDispatcher 创建了一个事件分发器 dispatch。当按钮被点击时,sendDataToParent 函数被调用,它定义了要传递的数据 Data from child,并通过 dispatch('childEvent', data) 触发了一个名为 childEvent 的自定义事件,并附带了数据 data

接下来,在父组件 App.svelte 中监听这个自定义事件:

<!-- App.svelte -->
<script>
    import Child from './Child.svelte';
    function handleChildEvent(data) {
        console.log('Received data from child:', data);
    }
</script>

<Child on:childEvent={handleChildEvent} />

App.svelte 中,我们通过 <Child on:childEvent={handleChildEvent} /> 监听了子组件触发的 childEvent 事件。当事件触发时,handleChildEvent 函数会被调用,并且子组件传递过来的数据作为参数传入这个函数,这里我们只是简单地将数据打印到控制台。

传递复杂数据结构给父组件

子组件向父组件传递复杂数据结构同样是通过自定义事件。假设子组件要传递一个对象给父组件:

<!-- Child.svelte -->
<script>
    import { createEventDispatcher } from'svelte';
    const dispatch = createEventDispatcher();
    function sendObjectToParent() {
        const obj = {
            key1: 'value1',
            key2: 'value2'
        };
        dispatch('childObjectEvent', obj);
    }
</script>

<button on:click={sendObjectToParent}>Send Object to Parent</button>

在父组件 App.svelte 中监听并处理这个事件:

<!-- App.svelte -->
<script>
    import Child from './Child.svelte';
    function handleChildObjectEvent(obj) {
        console.log('Received object from child:', obj);
    }
</script>

<Child on:childObjectEvent={handleChildObjectEvent} />

这里子组件传递了一个对象 obj,父组件监听事件并在事件处理函数中获取这个对象进行处理,同样这里只是简单打印到控制台。

如果要传递数组,方式也是类似的。在子组件 Child.svelte 中:

<!-- Child.svelte -->
<script>
    import { createEventDispatcher } from'svelte';
    const dispatch = createEventDispatcher();
    function sendArrayToParent() {
        const arr = [1, 2, 3];
        dispatch('childArrayEvent', arr);
    }
</script>

<button on:click={sendArrayToParent}>Send Array to Parent</button>

在父组件 App.svelte 中:

<!-- App.svelte -->
<script>
    import Child from './Child.svelte';
    function handleChildArrayEvent(arr) {
        console.log('Received array from child:', arr);
    }
</script>

<Child on:childArrayEvent={handleChildArrayEvent} />

通过这种方式,子组件可以方便地将各种复杂数据结构传递给父组件进行进一步处理。

多个子组件向同一个父组件传递数据

在实际应用中,可能会有多个子组件都需要向同一个父组件传递数据。这种情况下,每个子组件都可以按照上述方式触发自定义事件,父组件分别监听不同子组件的不同事件。

假设我们有两个子组件 Child1.svelteChild2.svelte,它们都向父组件 App.svelte 传递数据。

先看 Child1.svelte

<!-- Child1.svelte -->
<script>
    import { createEventDispatcher } from'svelte';
    const dispatch = createEventDispatcher();
    function sendDataFromChild1() {
        const data = 'Data from Child1';
        dispatch('child1Event', data);
    }
</script>

<button on:click={sendDataFromChild1}>Send Data from Child1</button>

再看 Child2.svelte

<!-- Child2.svelte -->
<script>
    import { createEventDispatcher } from'svelte';
    const dispatch = createEventDispatcher();
    function sendDataFromChild2() {
        const data = 'Data from Child2';
        dispatch('child2Event', data);
    }
</script>

<button on:click={sendDataFromChild2}>Send Data from Child2</button>

然后在父组件 App.svelte 中监听这两个子组件的事件:

<!-- App.svelte -->
<script>
    import Child1 from './Child1.svelte';
    import Child2 from './Child2.svelte';
    function handleChild1Event(data) {
        console.log('Received data from Child1:', data);
    }
    function handleChild2Event(data) {
        console.log('Received data from Child2:', data);
    }
</script>

<Child1 on:child1Event={handleChild1Event} />
<Child2 on:child2Event={handleChild2Event} />

这样,父组件可以分别处理来自不同子组件的数据,每个子组件的事件触发和数据传递相互独立,互不干扰。

深层嵌套组件间的数据传递

逐层传递数据

当组件嵌套层次较深时,一种常见的方法是逐层传递数据。例如,有一个父组件 Parent.svelte,它包含一个子组件 Child.svelte,而 Child.svelte 又包含一个孙组件 GrandChild.svelte

首先看 GrandChild.svelte,它接收来自 Child.svelte 的数据:

<!-- GrandChild.svelte -->
<script>
    export let message;
</script>

<div>
    <p>{message}</p>
</div>

接着 Child.svelte 接收来自 Parent.svelte 的数据,并将其传递给 GrandChild.svelte

<!-- Child.svelte -->
<script>
    import GrandChild from './GrandChild.svelte';
    export let message;
</script>

<GrandChild message={message} />

最后 Parent.svelte 传递数据给 Child.svelte

<!-- Parent.svelte -->
<script>
    import Child from './Child.svelte';
    let text = 'Message from Parent';
</script>

<Child message={text} />

通过这种方式,数据从顶层的父组件逐层传递到深层的子组件。这种方法的优点是数据流向清晰,易于理解和维护。但缺点是如果嵌套层次过多,代码会变得冗长,并且中间层组件可能只是单纯地传递数据而没有实际业务逻辑,增加了不必要的复杂度。

使用上下文(Context)进行跨层传递

Svelte 提供了上下文(Context)机制来解决深层嵌套组件间数据传递的问题,它允许我们在不通过中间层组件显式传递数据的情况下,将数据传递给深层组件。

首先,在 Parent.svelte 中设置上下文:

<!-- Parent.svelte -->
<script>
    import { setContext } from'svelte';
    import Child from './Child.svelte';
    let sharedData = 'Shared data';
    setContext('sharedContext', sharedData);
</script>

<Child />

这里使用 setContext 函数设置了一个名为 sharedContext 的上下文,并将 sharedData 作为上下文的值。

然后,在 GrandChild.svelte 中获取上下文数据:

<!-- GrandChild.svelte -->
<script>
    import { getContext } from'svelte';
    const sharedData = getContext('sharedContext');
</script>

<div>
    <p>{sharedData}</p>
</div>

GrandChild.svelte 中,通过 getContext 函数获取名为 sharedContext 的上下文数据,并可以直接使用。这样,即使 Child.svelte 作为中间层组件,也不需要显式地传递数据,简化了嵌套组件间的数据传递。

上下文的更新与响应

上下文数据不仅可以传递,还可以更新,并且深层组件会响应这些更新。

修改 Parent.svelte 来更新上下文数据:

<!-- Parent.svelte -->
<script>
    import { setContext } from'svelte';
    import Child from './Child.svelte';
    let sharedData = 'Initial shared data';
    setContext('sharedContext', sharedData);
    function updateSharedData() {
        sharedData = 'Updated shared data';
        setContext('sharedContext', sharedData);
    }
</script>

<Child />
<button on:click={updateSharedData}>Update Shared Data</button>

GrandChild.svelte 中,由于它获取了上下文数据,当上下文数据更新时,它会自动重新渲染以显示新的数据。这种机制在一些需要共享全局状态或者在深层嵌套组件中同步数据的场景下非常有用。

双向数据绑定在父子组件间的应用

基本双向绑定概念

双向数据绑定是指在组件间,数据的变化可以在两个方向上流动,既可以从父组件传递到子组件,也可以从子组件反馈回父组件,并且这种反馈是自动的,无需手动触发事件等操作。

在 Svelte 中,双向绑定使用 bind: 指令来实现。假设我们有一个父组件 App.svelte 和一个子组件 InputChild.svelte,父组件希望通过子组件的输入框来更新自身的数据。

先看 InputChild.svelte

<!-- InputChild.svelte -->
<script>
    export let value;
</script>

<input type="text" bind:value={value} />

这里的 bind:value={value} 表示输入框的值与 value 属性双向绑定,输入框的值变化会更新 valuevalue 的变化也会更新输入框的值。

再看 App.svelte

<!-- App.svelte -->
<script>
    import InputChild from './InputChild.svelte';
    let inputValue = '';
</script>

<InputChild bind:value={inputValue} />
<p>Value in parent: {inputValue}</p>

App.svelte 中,通过 bind:value={inputValue}inputValue 与子组件的 value 属性双向绑定。当在子组件的输入框中输入内容时,inputValue 会自动更新,并且父组件中的 p 标签会显示更新后的值。

双向绑定复杂数据类型

双向绑定同样适用于复杂数据类型,如对象和数组。

假设我们双向绑定一个对象。先修改 ObjectChild.svelte

<!-- ObjectChild.svelte -->
<script>
    export let user;
</script>

<input type="text" bind:value={user.name} />
<input type="number" bind:value={user.age} />

这里输入框分别与 user 对象的 nameage 属性双向绑定。

然后在 App.svelte 中:

<!-- App.svelte -->
<script>
    import ObjectChild from './ObjectChild.svelte';
    let user = {
        name: 'Initial Name',
        age: 25
    };
</script>

<ObjectChild bind:user={user} />
<p>Name in parent: {user.name}</p>
<p>Age in parent: {user.age}</p>

App.svelte 中,通过 bind:user={user}user 对象双向绑定到子组件。当在子组件的输入框中修改 nameage 时,父组件中的 user 对象会自动更新,并且显示的信息也会相应改变。

对于数组的双向绑定,原理类似。假设 ArrayChild.svelte 有一个用于添加数组元素的输入框:

<!-- ArrayChild.svelte -->
<script>
    export let numbers;
    let newNumber;
    function addNumber() {
        if (newNumber) {
            numbers.push(newNumber);
            newNumber = '';
        }
    }
</script>

<input type="number" bind:value={newNumber} />
<button on:click={addNumber}>Add Number</button>
<ul>
    {#each numbers as number}
        <li>{number}</li>
    {/each}
</ul>

App.svelte 中:

<!-- App.svelte -->
<script>
    import ArrayChild from './ArrayChild.svelte';
    let numbers = [1, 2, 3];
</script>

<ArrayChild bind:numbers={numbers} />
<p>Numbers in parent: {numbers.join(', ')}</p>

这里通过 bind:numbers={numbers} 将数组双向绑定到子组件。当在子组件中添加新的数字时,父组件中的 numbers 数组会自动更新并显示。

双向绑定与事件结合

在实际应用中,双向绑定常常与事件结合使用。例如,我们可能希望在子组件数据更新时触发一个额外的操作。

修改 InputChild.svelte 如下:

<!-- InputChild.svelte -->
<script>
    import { createEventDispatcher } from'svelte';
    const dispatch = createEventDispatcher();
    export let value;
    function onInputChange() {
        dispatch('inputChanged', value);
    }
</script>

<input type="text" bind:value={value} on:input={onInputChange} />

这里当输入框的值发生变化时,不仅更新 value,还触发一个 inputChanged 事件,并传递当前 value 的值。

App.svelte 中监听这个事件:

<!-- App.svelte -->
<script>
    import InputChild from './InputChild.svelte';
    let inputValue = '';
    function handleInputChanged(newValue) {
        console.log('Input value changed in parent:', newValue);
    }
</script>

<InputChild bind:value={inputValue} on:inputChanged={handleInputChanged} />
<p>Value in parent: {inputValue}</p>

这样,当子组件输入框的值变化时,父组件不仅更新数据,还可以执行额外的操作,如打印日志等。

优化父子组件数据传递性能

减少不必要的重新渲染

在 Svelte 中,当组件的状态或传入的属性发生变化时,组件会重新渲染。为了优化性能,我们需要避免不必要的重新渲染。

一种方法是使用 $: 语句来控制数据的更新时机。例如,在父组件 App.svelte 中:

<!-- App.svelte -->
<script>
    import Child from './Child.svelte';
    let data = {
        value: 'Initial value'
    };
    let shouldUpdate = true;
    function updateData() {
        if (shouldUpdate) {
            data.value = 'Updated value';
        }
    }
</script>

<Child data={data} />
<button on:click={updateData}>Update Data</button>

在上述代码中,通过 shouldUpdate 变量来控制 data 的更新。只有当 shouldUpdatetrue 时,点击按钮才会更新 data,从而避免了不必要的子组件重新渲染。

对于子组件,如果某些属性变化不会影响其显示或逻辑,可以通过 $$props 来手动控制重新渲染。例如在 Child.svelte 中:

<!-- Child.svelte -->
<script>
    export let data;
    let previousData = data;
    $: if (data!== previousData) {
        // 执行必要的更新逻辑
        previousData = data;
    }
</script>

<div>
    <p>{data.value}</p>
</div>

这里通过比较 datapreviousData,只有当 data 真正发生变化时,才执行必要的更新逻辑,从而减少不必要的重新渲染。

批量更新数据

在父组件中,如果需要多次更新数据,可以考虑批量更新,以减少组件的重新渲染次数。

假设父组件 App.svelte 要更新多个相关的数据:

<!-- App.svelte -->
<script>
    import Child from './Child.svelte';
    let user = {
        name: 'Initial Name',
        age: 25
    };
    function updateUser() {
        // 批量更新
        user.name = 'Updated Name';
        user.age = 26;
    }
</script>

<Child user={user} />
<button on:click={updateUser}>Update User</button>

在上述代码中,updateUser 函数一次性更新了 user 对象的多个属性,这样只触发一次子组件的重新渲染。如果分别更新每个属性,可能会触发多次重新渲染,影响性能。

使用 {#if}{#await} 控制渲染

在父子组件中,合理使用 {#if}{#await} 可以控制组件的渲染时机,从而优化性能。

例如,在父组件 App.svelte 中,只有当某个条件满足时才渲染子组件:

<!-- App.svelte -->
<script>
    import Child from './Child.svelte';
    let isReady = false;
    function setReady() {
        isReady = true;
    }
</script>

{#if isReady}
    <Child />
{/if}
<button on:click={setReady}>Render Child</button>

这里通过 {#if isReady} 控制子组件的渲染,只有当 isReadytrue 时才渲染子组件,避免了不必要的渲染开销。

对于异步数据获取,{#await} 可以在数据加载完成后再渲染子组件:

<!-- App.svelte -->
<script>
    import Child from './Child.svelte';
    async function fetchData() {
        return new Promise((resolve) => {
            setTimeout(() => {
                resolve('Fetched data');
            }, 2000);
        });
    }
    const dataPromise = fetchData();
</script>

{#await dataPromise}
    <p>Loading...</p>
{:then data}
    <Child data={data} />
{:catch error}
    <p>Error: {error.message}</p>
{/await}

在上述代码中,{#await} 等待数据获取完成后才渲染子组件 Child,并在加载过程中显示加载提示,出错时显示错误信息,提高了用户体验和性能。