likes
comments
collection
share

面试总结-你真的了解vue组件之间的通信吗?

作者站长头像
站长
· 阅读数 4

前言

在本人最近的面试中,关于vue之间的组件通信的方式是经常被面试官提起,随着面试场次的增多,本人对于组件通信方式的理解 从只会 ‘props’、‘emit’和vuex 之外学到了更多的方式。

为什么要有组件通信

我们都知道,在vue中当我们需要将当前页面组件的信息传递给另一个组件的时候,就需要实现vue的组件通信功能。因为Vue是一个组件化的框架,应用程序通常被拆分成许多小组件,每个组件负责一个特定的功能。组件通信允许这些组件相互交流和协作,使得构建复杂的应用程序变得更加容易和灵活。

因此组件通信在Vue中是必需的,因为它使得构建复杂的应用程序变得更加容易和灵活,具体原因包括:

  1. 组件化开发:Vue是一个组件化的框架,应用程序通常被拆分成许多小组件,每个组件负责一个特定的功能。组件通信使得这些组件能够相互交流和协作,从而构建出功能丰富的应用程序。
  2. 解耦和复用:通过组件通信,可以将应用程序拆分成独立的组件,每个组件只关注自己的功能,不需要关心其他组件的实现细节。这样可以降低组件之间的耦合度,提高组件的复用性和可维护性。
  3. 状态管理:组件通信使得状态管理变得更加简单和灵活。通过向子组件传递props和监听子组件的事件,可以实现父子组件之间的数据传递和状态管理。而通过全局事件总线、Vuex等工具,可以实现任意组件之间的状态共享和管理。
  4. 用户交互:在用户交互方面,组件通信可以实现组件之间的消息传递和事件触发。例如,一个按钮组件可以触发一个点击事件,通知其他组件执行相应的操作。
  5. 跨组件通信:有时候需要在不相邻的组件之间进行通信,例如兄弟组件之间或者跨级组件之间。通过提供/注入、全局事件总线、Vuex等方式,可以实现跨组件的数据传递和状态管理。

组件通信的方式

那么都有哪些方式可以实现组件通信呢?本人在最近的学习中共学习到了以下的实现方式:

  1. 父子组件通信:父组件通过props向子组件传递数据,子组件通过defineProps接收数据
  2. 父子组件通信:父组件provide,子组件inject
  3. 子父组件通信:子组件拿到父组件的数据并修改后emit出来,父组件通过v-model实现双向绑定
  4. 子父组件通信:子组件defineExpose暴露方法,父组件通过ref读取整个子组件对象来获取值
  5. 子父组件通信:子组件通过emit触发事件,父组件通过v-on(@) 监听事件
  6. EventBus 事件总线 vue3中不推荐使用,但是可以通过 mitt 插件实现
  7. vuex

下面本人将为大家讲解一下如何用以上方式来实现组件通信

父子组件通信

首先就是父子组件通信方式的实现了。

props

如以上代码

// 父组件
<template>
  <div class="hd">
    <input type="text" v-model="msg">
    <button @click="add">添加</button>
  </div>


  <Child :list="list"/>

</template>

<script setup>
import { ref } from 'vue'
import Child from './components/Child_1.vue'

const list = ref(['html', 'css'])

const msg = ref('')
const add = () => {
  list.value.push(msg.value)
  msg.value = ''
}

</script>

<style lang="css" scoped>

.hd input{
  width: 200px;
  padding: 10px 0;
  font-size: 10px;
}
.hd button{
  padding: 10px 20px;
  font-size: 20px;
}

</style>
//子组件
<template>
  <div class="child">
    <div class="bd">
      <ul>
        <li v-for="item in list" :key="item">{{ item }}</li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { defineProps } from 'vue'

defineProps({
  list: {
    type: Array,
    default: () => []
  }
})
</script>

<style scoped>
.child {
  margin-top: 20px;
}
.bd {
  background-color: #f0f0f0;
  padding: 20px;
  border-radius: 8px;
}
</style>

以上代码中,在父组件中,我们通过<Child :list="list"/>语法,将父组件的list数据传递给子组件。并且使用 ref 创建了名为 listmsgadd 的响应式数据。list 用于存储列表项,msg 用于存储输入框的值,add 函数用于向 list 数组中添加新的列表项。

子组件中,通过defineProps定义了一个list属性,其类型为数组,默认值为空数组。然后在模板中使用了v-for指令遍历list数组,将数组中的每个元素渲染成一个列表项。

在父组件中,通过将子组件Childlist属性绑定到父组件中的msg变量,实现了父组件向子组件传递数据的功能。当父组件中的msg变量发生变化时,子组件中的list属性也会相应地更新。

provide和inject

在vue中,我们可以通过导入provideinject,开实现父子组件之间的通信。 特点

  1. 灵活性: provideinject 不受组件层次结构的限制,可以在任何深度的组件中使用,从而使得数据传递更加灵活。
  2. 跨层级通信: 与 props/downward data flow 不同,provideinject 可以实现跨层级的组件通信,无需一层层地传递 props,从而简化了组件之间的通信。
  3. 依赖注入的思想: 类似于依赖注入的思想,使得组件之间的依赖关系更加清晰,降低了组件之间的耦合度。
<template>
  <div class="hd">
    <input type="text" v-model="msg">
    <button @click="add">添加</button>
  </div>


  <!-- <Child :list="list"/> -->
  <Child />

</template>

<script setup>
import { ref,provide } from 'vue'
import Child from './components/Child_5.vue'


const msg = ref('')
const list = ref(['html', 'css'])

provide('list', list.value);

const add = () => {
  list.value.push(msg.value)
  msg.value = ''
}

</script>

<style lang="css" scoped>
同上
</style>
<template>
  <div class="bd">
    <ul>
      <li v-for="item in list">{{item}}</li>
    </ul>
  </div>
</template>

<script setup>
import { ref,inject } from 'vue'

const list = inject('list')

</script>

<style lang="css" scoped>

</style>

在以上代码中,我们通过父组件 provide 提供了 list 数据,而子组件则通过 inject 获取了这个数据,从而实现了组件之间的通信。这种方式可以在组件层次结构中的任何深度进行通信,而不需要一层层地传递 props,因此更加灵活方便。

但是需要注意的是它只能由祖先组件向子孙组件发送数据。并且不太推荐使用这种方式来实现父子组件之间的通信,因为它与我们所认知的单向数据流的思想相悖。

子父组件通信

emit

<template>
  <Child @add="handle"/>

  <div class="bd">
    <ul>
      <li v-for="item in list">{{item}}</li>
    </ul>
  </div>

</template>

<script setup>
import { ref } from 'vue';
import Child from './components/Child_2.vue'

const list = ref(['html', 'css'])

const handle = (e) => {
  list.value.push(e)
}
</script>
<template>
  <div class="hd">
    <input type="text" name="" id="" v-model="msg">
    <button @click="add">添加</button>
  </div>

</template>

<script setup>
import { ref } from 'vue';

const msg = ref('')
const emits = defineEmits(['add']) //创建一个add事件

const add = () => {
  emits('add', msg.value);
  console.log(msg.value);
  msg.value = '';
}

</script>

在以上代码中,通过 defineEmits 函数来定义了一个名为 add 的自定义事件。这个函数的作用是创建一个函数,用于触发组件的自定义事件。

具体来说,在子组件中,我们首先使用 defineEmits 函数来创建了一个名为 emits 的对象,该对象包含了定义的自定义事件。然后,在子组件的 add 方法中,我们通过 emits 对象来触发了 add 事件,并且传递了 msg.value 作为参数。

这样,在父组件中,我们通过监听子组件触发的 add 事件,并在事件处理函数中接收到传递过来的参数,从而实现了子组件向父组件的通信。

v-model

我们都知道v-model 在 Vue.js 中通常用于实现表单元素和组件之间的双向数据绑定,但它也可以用于自定义组件之间的通信。这是因为 v-model 实际上是一个语法糖,它背后的原理是通过将 value 属性和 input 事件结合起来实现数据的双向绑定。

当你在一个自定义组件上使用 v-model 时,Vue.js 实际上会做两件事情:

  1. 将 value 属性传递给组件作为 props。
  2. 监听组件内部触发的 input 事件,并根据该事件更新外部数据。

因此,通过在自定义组件内部正确地处理 value 属性和触发 input 事件,我们可以使自定义组件支持 v-model 语法,并实现与父组件之间的双向数据绑定。

<template>
  <Child v-model:list="list"/>

  <div class="bd">
    <ul>
      <li v-for="item in list">{{item}}</li>
    </ul>
  </div>

</template>

<script setup>
import { ref } from 'vue';
import Child from './components/Child_3.vue'

const list = ref(['html', 'css'])

</script>
<template>
  <div class="hd">
    <input type="text" name="" id="" v-model="msg">
    <button @click="add">添加</button>
  </div>

</template>

<script setup>
import { ref } from 'vue';

const msg = ref('')
const props = defineProps({
  list: {
    type: Array,
    default: () => []
  }
})

const emits = defineEmits(['update:list']) // 事件名必须是 update:xxx

const add = () => {
  const arr = props.list
  arr.push(msg.value)
  // 不要写这种代码:props.list.push(msg.value)

  emits('update:list', arr)
  msg.value = ''
}

</script>

以上代码中,我们在父组件中,通过 <Child v-model:list="list"/> 这样的语法,将 list 数据作为 v-model 的值传递给了子组件 Child。这样做的效果是,父组件的 list 数据会被传递给子组件,并且在子组件内部使用 v-model 时,list 数据将作为 value 属性的值。

在子组件 Child 中,使用了 v-model="msg",这样做的效果是,msg 数据将与输入框进行双向绑定。也就是说,当输入框中的值发生变化时,msg 数据也会跟着变化;反之亦然。

在子组件中,通过点击按钮触发 add 方法,将输入框中的值添加到 list 数组中。同时,通过 emits('update:list', arr) 将更新后的 list 数组发送给父组件。

在父组件中,由于使用了 v-model,因此子组件发送的 list 数据更新会立即反映在父组件中,从而实现了双向数据绑定和组件通信的效果。

defineExpose

实际上defineExpose并不是专门用于实现组件通信的功能,而是 Vue 3 中的一个用于暴露组件内部数据或方法给父组件的 API。在 Vue 3 中,组件的内部数据和方法默认是私有的,只能在组件内部访问,而无法直接通过组件实例访问。但是,有时候我们希望将某些数据或方法暴露给父组件,以便父组件可以直接访问或操作。

defineExpose 的作用就是将组件内部的数据或方法暴露给父组件。当组件使用 defineExpose 声明后,父组件就可以通过组件实例直接访问这些暴露的数据或方法,从而实现了组件之间的通信。

<template>
  <Child ref="childRef"/>

  <div class="bd">
    <ul>
      <li v-for="item in childRef?.list">{{item}}</li>
    </ul>
  </div>

</template>

<script setup>
import { ref } from 'vue';
import Child from './components/Child_4.vue'
const childRef = ref(null)
</script>
<template>
  <div class="hd">
    <input type="text" name="" id="" v-model="msg">
    <button @click="add">添加</button>
  </div>

</template>

<script setup>
import { ref } from 'vue';

const list = ref(['html', 'css'])
const msg = ref('')

const add = () => {
  list.value.push(msg.value)
  msg.value = ''
}

defineExpose({list}) //子组件暴露出去的数据

</script>

以上代码中,我们在子组件中通过 defineExpose 来暴露子组件的 list 数据,这样父组件就可以直接通过 childRef.list 访问子组件的 list 数据了

兄弟组件通信

EventBus 事件总线

事件总线是一种常见的 Vue 应用程序中组件通信的方式,它通过一个中央事件总线来实现组件之间的通信,允许不同组件之间进行解耦合的通信。

//App.vue
<template>
  <div>
    <ChildA />
    <ChildB />
  </div>
</template>

<script setup>
import ChildA from './components/child6/A.vue'
import ChildB from './components/child6/B.vue'

</script>
//A.vue
<template>
  <div>
    <input type="text" v-model="msg" />
    <button @click="add">添加</button>
  </div>
</template>

<script setup>
import emitter from './emit.js'
import { onMounted, ref } from "vue";

const list = ref(["html", "css"]);

onMounted(() => {
  emitter.emit("list", list.value);
});

const msg = ref("");
const add = () => {
  list.value.push(msg.value);
  msg.value = "";
};

//B.vue
<template>
  <div>
    <ul>
      <li v-for="item in list ">{{ item }}</li>
    </ul>
  </div>
</template>

<script setup>
import emitter from './emit';
import { ref } from 'vue';

const list = ref([])
emitter.on('list', (e) => {
  console.log(e);
  list.value = e;
})

</script>
// emit.js
import mitt from 'mitt';

const emitter = mitt();

export default emitter;

首先,因为在vue3中不支持直接使用Eventbus,我们需要通过import mitt from 'mitt' 来创建事件总线。需要注意的是我们需要去下载这个mitt插件。

ChildA 组件中,当组件挂载完成时,我们通过事件总线向外发送了一个名为 list 的事件,并携带了自己的 list 数据。这里的 emitter.emit("list", list.value) 表示发送一个名为 list 的事件,并将当前 list 数据作为参数传递给这个事件。

ChildB 组件中,它监听了事件总线上的 list 事件,并在事件被触发时执行相应的回调函数,更新自己的数据。这里的 emitter.on('list', callback) 表示监听名为 list 的事件,并在事件被触发时执行 callback 回调函数。在回调函数中,它将收到的数据更新到 ChildB 组件的 list 中。

的来说,通过事件总线,ChildA 组件发送事件并携带数据,而 ChildB 组件监听这个事件,并在事件被触发时接收数据,从而实现了组件之间的通信。

vuex

在vue中我们可以通过vuex来实现组件之间的通信。因为Vuex 是 Vue.js 官方提供的状态管理库,它可以帮助我们集中管理应用中的状态,并提供了一种在组件之间共享状态的机制。

  1. 安装 Vuex: 首先,我们需要去安装 Vuex。

    npm install vuex
    # 或者
    yarn add vuex
    
  2. 创建 Vuex Store: 在文件夹中,我们需要创建一个 store 文件来管理应用的状态。在 Vuex store 中,我们可以定义状态、mutations、actions 等。

    // store/index.js
    import { createStore } from 'vuex';
    
    const store = createStore({
      state: {
        list: ['html', 'css'],
      },
      mutations: {
        updateList(state, newList) {
          state.list = newList;
        },
      },
      actions: {
        updateList(context, newList) {
          context.commit('updateList', newList);
        },
      },
      getters: {
        getList(state) {
          return state.list;
        },
      },
    });
    
    export default store;
    
  3. 使用 Vuex: 将创建的 Vuex store 导入到 main.js 中,以便所有组件都能够访问到 Vuex 中的状态和方法。

    // main.js
    import { createApp } from 'vue';
    import App from './App.vue';
    import store from './store';
    
    const app = createApp(App);
    app.use(store);
    app.mount('#app');
    
  4. 在兄弟组件中使用 Vuex

    • ChildA 组件:在需要更新状态的地方,通过调用 Vuex store 中的 action 来更新状态。

      // ChildA.vue
      <template>
        <button @click="updateList">Update List</button>
      </template>
      
      <script>
      import { mapActions } from 'vuex';
      
      export default {
        methods: {
          ...mapActions(['updateList']),
        },
      };
      </script>
      
    • ChildB 组件:通过计算属性(computed property)或者直接访问 Vuex store 的 getters 来获取状态。

      // ChildB.vue
      <template>
        <div>
          <ul>
            <li v-for="item in list" :key="item">{{ item }}</li>
          </ul>
        </div>
      </template>
      
      <script>
      import { mapGetters } from 'vuex';
      
      export default {
        computed: {
          ...mapGetters(['getList']),
          list() {
            return this.getList;
          },
        },
      };
      </script>
      

通过以上步骤,我们就可以在兄弟组件之间实现通信了。当 ChildA 组件触发更新时,通过 Vuex store 中的 action 更新状态,然后 ChildB 组件通过计算属性或者直接访问 getters 来获取更新后的状态,从而实现了兄弟组件之间的通信。

总结

我所了解的组件之间的通信方式就是以上的所有内容了,如有不足之处,欢迎大家加以补充。