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

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

2022-02-072.3k 阅读

理解 Svelte 组件通信基础

在 Svelte 应用开发中,组件是构建用户界面的基本单元。组件之间常常需要交换数据和信息,这就涉及到组件通信。父子组件间的数据传递是其中最常见的一种通信方式。在 Svelte 里,理解组件通信机制的核心在于认识到每个组件都是相对独立的,但又能通过特定方式与其他组件交互。

Svelte 组件本质上是一个包含 HTML、CSS 和 JavaScript 的自包含单元。每个组件都有自己的状态(state)和行为(behavior)。当组件被实例化时,它可以接收来自父组件的数据,同时也可以向父组件发送数据,这种交互构成了 Svelte 应用动态性和灵活性的基础。

从父组件传递数据到子组件

  1. 属性(Props)的使用
    • 在 Svelte 中,父组件向子组件传递数据主要通过属性(props)来实现。属性就像是组件的“输入参数”。例如,我们创建一个简单的父组件 App.svelte 和一个子组件 Child.svelte
    • 首先创建 Child.svelte,代码如下:
<script>
    export let message;
</script>

<p>{message}</p>
  • 在上述代码中,通过 export let message; 声明了一个名为 message 的属性,这个属性将从父组件接收数据。在模板部分,直接将 message 渲染在 <p> 标签内。
  • 然后在 App.svelte 中使用 Child.svelte 组件并传递数据,代码如下:
<script>
    import Child from './Child.svelte';
    let parentMessage = 'Hello from parent!';
</script>

<Child message={parentMessage} />
  • App.svelte 中,先导入了 Child.svelte 组件,然后定义了一个变量 parentMessage。在使用 <Child> 组件时,将 parentMessage 作为 message 属性传递给了 Child 组件。这样,Child 组件就能接收到父组件传递的 message 数据并进行展示。
  1. 传递复杂数据类型
    • 除了传递简单的字符串类型数据,也可以传递对象、数组等复杂数据类型。例如,修改 Child.svelte 来接收一个对象:
<script>
    export let user;
</script>

<p>{user.name} is {user.age} years old.</p>
  • App.svelte 中传递一个用户对象:
<script>
    import Child from './Child.svelte';
    let user = {
        name: 'John',
        age: 30
    };
</script>

<Child user={user} />
  • 这里,父组件 App.svelteuser 对象作为 user 属性传递给 Child.svelte。子组件可以通过解构 user 对象来获取其中的属性并进行展示。
  1. 属性的默认值
    • 在子组件中,可以为属性设置默认值。当父组件没有传递该属性时,子组件将使用默认值。例如,修改 Child.svelte 为:
<script>
    export let message = 'Default message';
</script>

<p>{message}</p>
  • 此时,如果在 App.svelte 中使用 <Child> 组件时没有传递 message 属性:
<script>
    import Child from './Child.svelte';
</script>

<Child />
  • 子组件 Child.svelte 将显示“Default message”。而如果传递了 message 属性,如 <Child message="New message" />,则会显示“New message”。

响应式数据传递

  1. 父组件数据变化影响子组件
    • Svelte 的响应式系统使得当父组件传递给子组件的属性值发生变化时,子组件会自动更新。例如,在 App.svelte 中添加一个按钮来改变传递给子组件的数据:
<script>
    import Child from './Child.svelte';
    let count = 0;
    const increment = () => {
        count++;
    };
</script>

<Child value={count} />
<button on:click={increment}>Increment</button>
  • Child.svelte 中接收 value 属性并显示:
<script>
    export let value;
</script>

<p>The value is: {value}</p>
  • 当在 App.svelte 中点击按钮时,count 的值会增加,由于 count 作为 value 属性传递给了 Child 组件,Child 组件会自动更新并显示新的值。
  1. 单向数据流原则
    • Svelte 遵循单向数据流原则,即数据从父组件流向子组件。子组件不能直接修改父组件传递过来的属性值。例如,在 Child.svelte 中尝试直接修改 message 属性:
<script>
    export let message;
    const wrongModify = () => {
        message = 'New value';
    };
</script>

<button on:click={wrongModify}>Wrong Modify</button>
<p>{message}</p>
  • App.svelte 中:
<script>
    import Child from './Child.svelte';
    let parentMessage = 'Initial message';
</script>

<Child message={parentMessage} />
  • 当点击 Child.svelte 中的“Wrong Modify”按钮时,虽然 message 在子组件内部看似改变了,但这并不会影响到父组件中的 parentMessage。如果需要改变父组件的数据,需要通过其他机制,比如事件机制,这将在后面介绍。

子组件向父组件传递数据

  1. 使用自定义事件
    • 子组件向父组件传递数据主要通过自定义事件实现。首先,在 Child.svelte 中创建并触发一个自定义事件:
<script>
    import { createEventDispatcher } from'svelte';
    const dispatch = createEventDispatcher();
    const sendDataToParent = () => {
        let dataToSend = 'Data from child';
        dispatch('childEvent', dataToSend);
    };
</script>

<button on:click={sendDataToParent}>Send Data to Parent</button>
  • 在上述代码中,通过 createEventDispatcher 创建了一个事件分发器 dispatch。当点击按钮时,触发名为 childEvent 的自定义事件,并传递数据 'Data from child'
  • App.svelte 中监听这个自定义事件:
<script>
    import Child from './Child.svelte';
    const handleChildEvent = (event) => {
        console.log('Received data from child:', event.detail);
    };
</script>

<Child on:childEvent={handleChildEvent} />
  • App.svelte 中,使用 on:childEvent 来监听 Child 组件触发的 childEvent 事件。当事件触发时,handleChildEvent 函数会被调用,event.detail 中包含了子组件传递过来的数据。
  1. 传递复杂数据结构
    • 同样可以传递复杂数据结构,比如对象或数组。修改 Child.svelte 来传递一个对象:
<script>
    import { createEventDispatcher } from'svelte';
    const dispatch = createEventDispatcher();
    const sendDataToParent = () => {
        let dataToSend = {
            key: 'value',
            array: [1, 2, 3]
        };
        dispatch('childEvent', dataToSend);
    };
</script>

<button on:click={sendDataToParent}>Send Data to Parent</button>
  • App.svelte 中接收并处理这个对象:
<script>
    import Child from './Child.svelte';
    const handleChildEvent = (event) => {
        console.log('Received object from child:', event.detail);
    };
</script>

<Child on:childEvent={handleChildEvent} />
  • 这样,父组件就能接收到子组件传递的复杂数据对象并进行相应处理。
  1. 双向绑定的实现原理(基于父子组件通信)
    • 在 Svelte 中,双向绑定看起来是一种方便的语法糖,让数据在父子组件间双向流动。例如,有一个 InputComponent.svelte 作为子组件:
<script>
    export let value;
    const handleChange = (event) => {
        let newVal = event.target.value;
        dispatch('input', newVal);
    };
</script>

<input type="text" bind:value on:input={handleChange}>
  • App.svelte 中使用这个组件并进行双向绑定:
<script>
    import InputComponent from './InputComponent.svelte';
    let textValue = '';
    const handleInput = (event) => {
        textValue = event.detail;
    };
</script>

<InputComponent bind:value={textValue} on:input={handleInput} />
<p>The value is: {textValue}</p>
  • 这里 bind:value 语法实际上是一种简写。当输入框的值改变时,InputComponent.svelte 触发 input 事件并传递新的值。App.svelte 监听这个事件并更新 textValue。同时,textValue 的初始值作为 value 属性传递给 InputComponent.svelte,从而实现了双向绑定。

多层父子组件间的数据传递

  1. 层层传递属性
    • 在复杂的组件结构中,可能存在多层父子组件关系。例如,有 Grandparent.svelteParent.svelteChild.svelte 三个组件。Grandparent.svelte 作为最顶层组件,Parent.svelteGrandparent.svelte 的子组件,Child.svelteParent.svelte 的子组件。
    • Child.svelte 中接收属性并显示:
<script>
    export let message;
</script>

<p>{message}</p>
  • Parent.svelte 中导入 Child.svelte 并传递属性:
<script>
    import Child from './Child.svelte';
    export let message;
</script>

<Child message={message} />
  • Grandparent.svelte 中导入 Parent.svelte 并传递属性:
<script>
    import Parent from './Parent.svelte';
    let grandMessage = 'Message from grandparent';
</script>

<Parent message={grandMessage} />
  • 通过这种层层传递属性的方式,Grandparent.svelte 的数据可以传递到 Child.svelte
  1. 使用上下文(Context)
    • 对于多层嵌套组件间传递数据,使用上下文可以简化传递过程。首先,在 Grandparent.svelte 中设置上下文:
<script>
    import { setContext } from'svelte';
    let sharedData = 'Shared data in context';
    setContext('sharedDataContext', sharedData);
</script>

{#if false}
    <!-- This block is just to hold child components without rendering -->
    <Parent />
{/if}
  • Child.svelte 中获取上下文数据:
<script>
    import { getContext } from'svelte';
    let sharedData = getContext('sharedDataContext');
</script>

<p>{sharedData}</p>
  • 这里,Grandparent.svelte 使用 setContext 设置了一个名为 sharedDataContext 的上下文,并将 sharedData 放入其中。Child.svelte 使用 getContext 获取这个上下文数据,无需通过中间的 Parent.svelte 层层传递属性。但需要注意,上下文数据是共享的,可能会导致数据管理的复杂性,应谨慎使用。

处理组件通信中的性能问题

  1. 不必要的重新渲染
    • 在父子组件通信中,可能会出现不必要的重新渲染问题。例如,如果父组件传递给子组件的属性值频繁变化,但子组件实际上并不依赖这些变化来更新其显示,就会导致不必要的重新渲染。
    • 假设 Child.svelte 有一个 isVisible 属性,用于控制组件的显示隐藏,而另一个 unrelatedProp 属性子组件并不依赖:
<script>
    export let isVisible;
    export let unrelatedProp;
</script>

{#if isVisible}
    <p>Child component is visible</p>
{/if}
  • App.svelte 中频繁改变 unrelatedProp
<script>
    import Child from './Child.svelte';
    let isVisible = true;
    let unrelatedProp = 0;
    const incrementUnrelated = () => {
        unrelatedProp++;
    };
</script>

<Child isVisible={isVisible} unrelatedProp={unrelatedProp} />
<button on:click={incrementUnrelated}>Increment Unrelated</button>
  • 每次点击按钮,unrelatedProp 变化会导致 Child 组件重新渲染,即使 isVisible 没有变化,子组件显示的内容也不依赖 unrelatedProp
  1. 优化方法
    • 一种优化方法是使用 $: derived 来创建衍生状态,只有当真正依赖的状态变化时才触发更新。例如,修改 Child.svelte 为:
<script>
    import { derived } from'svelte/store';
    export let isVisible;
    export let unrelatedProp;
    let visibleStore = derived({ isVisible }, ($store) => {
        return $store.isVisible;
    });
</script>

{#if $visibleStore}
    <p>Child component is visible</p>
{/if}
  • 这样,只有当 isVisible 变化时,Child 组件内部依赖的 $visibleStore 才会变化,从而触发重新渲染,避免了因 unrelatedProp 变化导致的不必要重新渲染。
  • 另一种方法是使用 shouldUpdate 生命周期函数。在 Child.svelte 中添加:
<script>
    export let isVisible;
    export let unrelatedProp;
    const shouldUpdate = (newProps) => {
        return newProps.isVisible!== isVisible;
    };
</script>

{#if isVisible}
    <p>Child component is visible</p>
{/if}
  • 这里 shouldUpdate 函数返回 truefalse,只有当返回 true 时,组件才会重新渲染。通过比较新老 isVisible 属性值,只有当 isVisible 变化时才会重新渲染,忽略了 unrelatedProp 的变化。

与 React 和 Vue 父子组件通信的对比

  1. 与 React 的对比
    • 在 React 中,父组件向子组件传递数据也是通过属性(props),这一点与 Svelte 相似。例如,在 React 中创建一个父组件 App.js 和子组件 Child.js
    • Child.js 代码如下:
import React from'react';

const Child = ({ message }) => {
    return <p>{message}</p>;
};

export default Child;
  • App.js 代码如下:
import React, { useState } from'react';
import Child from './Child';

const App = () => {
    const [parentMessage, setParentMessage] = useState('Hello from parent');
    return <Child message={parentMessage} />;
};

export default App;
  • 然而,React 的单向数据流更为严格,子组件通过回调函数将数据传递给父组件时,需要父组件提前将回调函数作为属性传递给子组件。例如,在 Child.js 中添加一个按钮来更新父组件数据:
import React from'react';

const Child = ({ message, updateParent }) => {
    const handleClick = () => {
        updateParent('New message from child');
    };
    return (
        <div>
            <p>{message}</p>
            <button onClick={handleClick}>Update Parent</button>
        </div>
    );
};

export default Child;
  • App.js 中传递回调函数:
import React, { useState } from'react';
import Child from './Child';

const App = () => {
    const [parentMessage, setParentMessage] = useState('Hello from parent');
    const handleChildUpdate = (newMessage) => {
        setParentMessage(newMessage);
    };
    return <Child message={parentMessage} updateParent={handleChildUpdate} />;
};

export default App;
  • 而在 Svelte 中,子组件可以通过自定义事件更直接地向父组件传递数据,不需要父组件提前传递回调函数。
  1. 与 Vue 的对比
    • 在 Vue 中,父组件向子组件传递数据同样通过属性。例如,创建一个父组件 App.vue 和子组件 Child.vueChild.vue 代码如下:
<template>
    <p>{{ message }}</p>
</template>

<script>
export default {
    props: ['message']
};
</script>
  • App.vue 代码如下:
<template>
    <Child :message="parentMessage" />
</template>

<script>
import Child from './Child.vue';
export default {
    components: {
        Child
    },
    data() {
        return {
            parentMessage: 'Hello from parent'
        };
    }
};
</script>
  • 子组件向父组件传递数据时,Vue 使用 $emit 方法触发自定义事件。例如,在 Child.vue 中添加一个按钮来传递数据:
<template>
    <div>
        <p>{{ message }}</p>
        <button @click="sendDataToParent">Send Data to Parent</button>
    </div>
</template>

<script>
export default {
    props: ['message'],
    methods: {
        sendDataToParent() {
            this.$emit('childEvent', 'Data from child');
        }
    }
};
</script>
  • App.vue 中监听事件:
<template>
    <Child :message="parentMessage" @childEvent="handleChildEvent" />
</template>

<script>
import Child from './Child.vue';
export default {
    components: {
        Child
    },
    data() {
        return {
            parentMessage: 'Hello from parent'
        };
    },
    methods: {
        handleChildEvent(data) {
            console.log('Received data from child:', data);
        }
    }
};
</script>
  • 虽然 Vue 和 Svelte 在子组件向父组件传递数据上都使用自定义事件,但 Svelte 的语法相对更简洁,事件分发器的使用更为直观,且 Svelte 的响应式系统在数据变化跟踪和组件更新上有其独特的优势。

通过深入理解 Svelte 父子组件间的数据传递机制,开发者可以构建出更加灵活、高效且易于维护的前端应用。无论是简单的属性传递,还是复杂的多层组件通信以及性能优化,掌握这些知识都是 Svelte 开发的关键。