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

Vue组件化开发 父子组件通信的多种方式

2021-08-227.4k 阅读

基于 props 的单向数据流通信

在 Vue 的父子组件通信中,props 是实现父组件向子组件传递数据的最基本方式。Vue 遵循单向数据流原则,即数据总是从父组件流向子组件,这样使得应用的数据流更加清晰和易于维护。

父组件传递数据给子组件

首先,在父组件中定义数据,并通过 props 将其传递给子组件。例如,假设我们有一个父组件 App.vue 和一个子组件 Child.vue

父组件 App.vue 代码如下

<template>
  <div id="app">
    <Child :message="parentMessage" />
  </div>
</template>

<script>
import Child from './components/Child.vue';

export default {
  components: {
    Child
  },
  data() {
    return {
      parentMessage: '这是来自父组件的数据'
    };
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

在上述代码中,我们在父组件的模板里使用 <Child> 标签引入子组件,并通过 :message 这个自定义的 prop 将 parentMessage 数据传递给子组件。这里的 :message 可以理解为一个“桥梁”,将父组件的数据与子组件关联起来。

子组件 Child.vue 代码如下

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

<script>
export default {
  props: ['message'],
  data() {
    return {};
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

子组件通过 props 选项声明了一个名为 message 的属性,这个属性就接收了来自父组件传递过来的数据。然后在模板中,我们可以直接使用 {{ message }} 来显示该数据。

动态绑定 props

props 不仅可以传递静态数据,还可以动态绑定各种类型的数据,如数字、布尔值、对象等。例如,传递一个动态变化的数字:

父组件 App.vue 代码更新如下

<template>
  <div id="app">
    <button @click="increment">增加数字</button>
    <Child :number="parentNumber" />
  </div>
</template>

<script>
import Child from './components/Child.vue';

export default {
  components: {
    Child
  },
  data() {
    return {
      parentNumber: 0
    };
  },
  methods: {
    increment() {
      this.parentNumber++;
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

这里我们添加了一个按钮,点击按钮会调用 increment 方法来增加 parentNumber 的值。然后通过 :number prop 将 parentNumber 动态传递给子组件。

子组件 Child.vue 代码更新如下

<template>
  <div>
    <p>来自父组件的数字: {{ number }}</p>
  </div>
</template>

<script>
export default {
  props: ['number'],
  data() {
    return {};
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

子组件声明 number prop 来接收父组件传递的动态数字,并在模板中显示。这样,当父组件的 parentNumber 变化时,子组件会实时更新显示。

Prop 的验证

为了确保传递的数据符合预期,Vue 允许我们对 props 进行类型检查和其他验证。例如,我们可以在子组件中对 message prop 进行如下验证:

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

<script>
export default {
  props: {
    message: {
      type: String,
      required: true,
      default: '默认消息'
    }
  },
  data() {
    return {};
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

在上述代码中,type 定义了 message prop 应该是字符串类型。required: true 表示这个 prop 是必需的,如果父组件没有传递该 prop,Vue 会在控制台发出警告。default 则指定了如果父组件没有传递该 prop 时的默认值。

自定义事件实现子组件向父组件通信

虽然 props 实现了父到子的数据传递,但如果子组件需要向父组件反馈信息,就需要用到自定义事件。

子组件触发事件

在子组件中,我们使用 $emit 方法来触发一个自定义事件,并可以附带数据。例如,假设子组件有一个按钮,点击按钮时向父组件传递一个消息。

子组件 Child.vue 代码如下

<template>
  <div>
    <button @click="sendMessageToParent">点击我向父组件发送消息</button>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  },
  methods: {
    sendMessageToParent() {
      const message = '这是子组件发送的消息';
      this.$emit('child-message', message);
    }
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

sendMessageToParent 方法中,我们通过 this.$emit('child - message', message) 触发了一个名为 child - message 的自定义事件,并传递了 message 数据。

父组件监听事件

父组件在使用子组件时,可以通过 v - on 语法(缩写为 @)来监听子组件触发的事件。

父组件 App.vue 代码如下

<template>
  <div id="app">
    <Child @child-message="handleChildMessage" />
  </div>
</template>

<script>
import Child from './components/Child.vue';

export default {
  components: {
    Child
  },
  methods: {
    handleChildMessage(message) {
      console.log('接收到子组件的消息:', message);
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

在父组件模板中,@child - message="handleChildMessage" 表示监听子组件触发的 child - message 事件,当事件触发时,会调用父组件的 handleChildMessage 方法,并将子组件传递的数据作为参数传入。

事件参数的传递和处理

子组件触发事件时可以传递多个参数。例如,假设子组件需要传递一个数字和一个字符串给父组件。

子组件 Child.vue 代码更新如下

<template>
  <div>
    <button @click="sendDataToParent">点击我向父组件发送数据</button>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  },
  methods: {
    sendDataToParent() {
      const number = 42;
      const string = '这是一个字符串';
      this.$emit('child - data', number, string);
    }
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

父组件 App.vue 代码更新如下

<template>
  <div id="app">
    <Child @child - data="handleChildData" />
  </div>
</template>

<script>
import Child from './components/Child.vue';

export default {
  components: {
    Child
  },
  methods: {
    handleChildData(number, string) {
      console.log('接收到子组件的数字:', number);
      console.log('接收到子组件的字符串:', string);
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

在父组件的 handleChildData 方法中,我们可以依次接收子组件传递的多个参数,并进行相应的处理。

使用 ref 引用进行父子组件通信

ref 提供了另一种父子组件通信的方式,它允许我们直接访问子组件的实例或 DOM 元素。

获取子组件实例

在父组件模板中给子组件添加 ref 属性,然后在父组件的代码中通过 this.$refs 来访问子组件实例。

父组件 App.vue 代码如下

<template>
  <div id="app">
    <Child ref="childComponent" />
    <button @click="callChildMethod">调用子组件方法</button>
  </div>
</template>

<script>
import Child from './components/Child.vue';

export default {
  components: {
    Child
  },
  methods: {
    callChildMethod() {
      this.$refs.childComponent.childMethod();
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

这里通过 ref="childComponent" 给子组件一个引用名称,然后在 callChildMethod 方法中,通过 this.$refs.childComponent 访问到子组件实例,并调用子组件的 childMethod 方法。

子组件 Child.vue 代码如下

<template>
  <div>
    <p>这是子组件</p>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  },
  methods: {
    childMethod() {
      console.log('子组件方法被调用');
    }
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

访问子组件的数据

除了调用子组件的方法,通过 ref 还可以访问子组件的数据。例如,假设子组件有一个 count 数据属性。

子组件 Child.vue 代码更新如下

<template>
  <div>
    <p>子组件的计数: {{ count }}</p>
    <button @click="increment">增加计数</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    };
  },
  methods: {
    increment() {
      this.count++;
    }
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

父组件 App.vue 代码更新如下

<template>
  <div id="app">
    <Child ref="childComponent" />
    <button @click="getChildrenData">获取子组件计数</button>
  </div>
</template>

<script>
import Child from './components/Child.vue';

export default {
  components: {
    Child
  },
  methods: {
    getChildrenData() {
      console.log('子组件的计数:', this.$refs.childComponent.count);
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

在父组件的 getChildrenData 方法中,通过 this.$refs.childComponent.count 可以访问到子组件的 count 数据。需要注意的是,过度依赖 ref 进行父子组件通信可能会使组件之间的耦合度增加,应该谨慎使用。

使用事件总线进行父子组件通信(非父子组件也适用)

事件总线是一种在组件之间共享事件的机制,它可以用于父子组件、兄弟组件甚至跨层级组件之间的通信。在 Vue 中,我们可以通过创建一个空的 Vue 实例作为事件总线。

创建事件总线

首先,在项目中创建一个单独的文件,例如 eventBus.js

import Vue from 'vue';
export const eventBus = new Vue();

这个 eventBus 实例就像一个“消息中心”,各个组件可以在这里发布和订阅消息。

子组件发布事件

在子组件中,我们可以使用 eventBus.$emit 来发布事件。

子组件 Child.vue 代码如下

<template>
  <div>
    <button @click="sendMessageToBus">点击我向事件总线发送消息</button>
  </div>
</template>

<script>
import { eventBus } from '../eventBus.js';

export default {
  data() {
    return {};
  },
  methods: {
    sendMessageToBus() {
      const message = '这是子组件通过事件总线发送的消息';
      eventBus.$emit('child - message - on - bus', message);
    }
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

这里子组件通过 eventBus.$emit 触发了一个名为 child - message - on - bus 的事件,并传递了消息数据。

父组件订阅事件

父组件需要先引入事件总线,然后使用 eventBus.$on 来订阅事件。

父组件 App.vue 代码如下

<template>
  <div id="app">
    <Child />
  </div>
</template>

<script>
import Child from './components/Child.vue';
import { eventBus } from './eventBus.js';

export default {
  components: {
    Child
  },
  created() {
    eventBus.$on('child - message - on - bus', (message) => {
      console.log('通过事件总线接收到子组件的消息:', message);
    });
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

在父组件的 created 钩子函数中,通过 eventBus.$on 监听了 child - message - on - bus 事件,当事件触发时,会执行回调函数并处理接收到的数据。需要注意的是,使用事件总线时要确保在适当的时候取消事件订阅,例如在组件销毁时,可以使用 eventBus.$off 方法,以避免内存泄漏。

使用 Vuex 进行父子组件通信(适用于大型应用状态管理)

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。虽然 Vuex 更多用于整个应用的状态管理,但在父子组件通信场景中也非常有用。

安装和配置 Vuex

首先,通过 npm 安装 Vuex:

npm install vuex --save

然后在项目中创建一个 store 目录,并在其中创建 index.js 文件来配置 Vuex。

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    sharedData: '初始共享数据'
  },
  mutations: {
    updateSharedData(state, newData) {
      state.sharedData = newData;
    }
  },
  actions: {
    updateSharedDataAction({ commit }, newData) {
      commit('updateSharedData', newData);
    }
  }
});

export default store;

在上述代码中,state 定义了应用的状态数据,mutations 定义了修改状态的方法,actions 可以用于处理异步操作并提交 mutations

父组件通过 Vuex 传递数据

父组件可以通过修改 Vuex 中的状态来间接向子组件传递数据。

父组件 App.vue 代码如下

<template>
  <div id="app">
    <button @click="updateSharedData">更新共享数据</button>
    <Child />
  </div>
</template>

<script>
import Child from './components/Child.vue';
import { mapActions } from 'vuex';

export default {
  components: {
    Child
  },
  methods: {
  ...mapActions(['updateSharedDataAction']),
    updateSharedData() {
      this.updateSharedDataAction('这是更新后的共享数据');
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

在父组件中,通过 mapActions 辅助函数将 updateSharedDataAction 映射到组件的方法中,点击按钮时调用该方法来更新 Vuex 中的 sharedData

子组件从 Vuex 获取数据

子组件可以通过计算属性从 Vuex 的状态中获取数据。

子组件 Child.vue 代码如下

<template>
  <div>
    <p>从 Vuex 获取的共享数据: {{ sharedData }}</p>
  </div>
</template>

<script>
import { mapState } from 'vuex';

export default {
  computed: {
  ...mapState(['sharedData'])
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

通过 mapState 辅助函数,子组件将 Vuex 中的 sharedData 映射为组件的计算属性,这样当 sharedData 发生变化时,子组件会自动更新。使用 Vuex 进行父子组件通信可以使数据管理更加集中和规范,尤其适用于大型应用程序。

插槽(Slot)在父子组件通信中的应用

插槽为父子组件之间的内容分发提供了一种方式,虽然它不是传统意义上的数据通信,但在一定程度上可以实现父子组件之间的交互和内容定制。

匿名插槽

匿名插槽是最基本的插槽类型。在子组件模板中使用 <slot> 标签定义插槽位置,父组件在使用子组件时,可以在子组件标签内插入内容,这些内容会被渲染到子组件的插槽位置。

子组件 Child.vue 代码如下

<template>
  <div>
    <h3>子组件标题</h3>
    <slot>这里是插槽的默认内容</slot>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

父组件 App.vue 代码如下

<template>
  <div id="app">
    <Child>
      <p>这是父组件插入到子组件插槽的内容</p>
    </Child>
  </div>
</template>

<script>
import Child from './components/Child.vue';

export default {
  components: {
    Child
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

在父组件中,<Child> 标签内的 <p> 元素会替换子组件插槽中的默认内容,被渲染到子组件的插槽位置。

具名插槽

具名插槽允许我们在子组件中定义多个插槽,并通过名称区分。在子组件模板中使用 <slot name="slot - name"> 来定义具名插槽。

子组件 Child.vue 代码更新如下

<template>
  <div>
    <header>
      <slot name="header">默认头部内容</slot>
    </header>
    <main>
      <slot>默认主体内容</slot>
    </main>
    <footer>
      <slot name="footer">默认底部内容</slot>
    </footer>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
header {
  background - color: lightblue;
  padding: 5px;
}
main {
  background - color: lightgreen;
  padding: 5px;
}
footer {
  background - color: lightpink;
  padding: 5px;
}
</style>

父组件 App.vue 代码更新如下

<template>
  <div id="app">
    <Child>
      <template v - slot:header>
        <h2>自定义头部</h2>
      </template>
      <p>自定义主体内容</p>
      <template v - slot:footer>
        <p>自定义底部</p>
      </template>
    </Child>
  </div>
</template>

<script>
import Child from './components/Child.vue';

export default {
  components: {
    Child
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

在父组件中,通过 v - slot:slot - name(Vue 2.6+ 语法,之前版本使用 slot="slot - name")来指定内容要插入到哪个具名插槽中。这样可以更灵活地定制子组件不同部分的内容。

作用域插槽

作用域插槽允许子组件向父组件传递数据,以便父组件根据这些数据来渲染插槽内容。在子组件中,通过在 <slot> 标签上绑定属性来传递数据。

子组件 Child.vue 代码如下

<template>
  <div>
    <slot :data="childData">
      <p>默认内容,当父组件未提供自定义内容时显示</p>
    </slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      childData: '子组件的数据'
    };
  }
};
</script>

<style scoped>
div {
  border: 1px solid #ccc;
  padding: 10px;
  margin: 10px;
}
</style>

父组件 App.vue 代码如下

<template>
  <div id="app">
    <Child>
      <template v - slot:default="slotProps">
        <p>接收到子组件的数据: {{ slotProps.data }}</p>
      </template>
    </Child>
  </div>
</template>

<script>
import Child from './components/Child.vue';

export default {
  components: {
    Child
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

在父组件中,通过 v - slot:default="slotProps" 获取子组件通过插槽传递的数据 childData,并在插槽内容中使用。这里的 default 表示默认插槽,如果是具名插槽,将 default 替换为具名插槽的名称即可。作用域插槽为父子组件之间的内容定制和数据交互提供了强大的功能。