在Vue 3.0 发布以后,我们基于新的特性,来归纳一下父子组件通信的方式。并且检验一下Vue 2.0中常用的通信方式,如何在Vue 3.0中使用。本文列出了三种常用的通信方式:

  1. 子组件通过emit函数派发消息给父组件
  2. 父组件通过事件代理获取子组件内容
  3. 通过vuex实现父子组件数据共享
  4. 通过依赖注入实现父子组件数据共享

设计和实现

本文通过一个Tabs组件实例来说明父子组件的通信方式,用户可以将Tabs数据和初始默认选中的Tab项作为属性传入Tabs组件。组件中每个月份为一个Tab项,每个Tab项内包含当月的销量数据。

点击Tabs项的链接,页面显示对应月份的销量数据。具体如下图所示:
在这里插入图片描述

根据上述需求,Tabs组件包括TabItem和TabContent两个子组件。

  • TabItem 显示月份信息
  • TabContent 显示指定月份的销量数据
  • Tabs,TabItem和TabContent共享当前点击的月份信息,即哪个Tabs项被选中。
  • 数据结构请参看附录

最终实现的用法如下,data见附录:

<tabs :data="data" :initialIdx="1"  >

Tabs组件目录接构如下:

  • index.vue Tabs的最外层组件,可以接收用户传入的Tabs数据和默认选中的Tabs项
  • tab-item.vue Tabs的子组件,用于显示每个Tabs项的月份信息
  • tab-content.vue Tabs的子组件,用于显示每个Tabs项的销量数据

基于emit函数实现父子组件通信

父组件Tabs关键代码如下:

<template>
  <div>
      <div class="Tabs">
          <tab-item
            v-for = "item in data"
            :key = "item.Id"
            :text = "item.Month"
            :curIdx = "CurrentIdx"
            @click-item = "clickItem"
          >
          </tab-item>
      </div>
      <tab-content :curIdx = "CurrentIdx" :data = "data"></tab-content>
  </div>
</template>
<script>
import TabItem from "./tab-item";
import TabContent from "./tab-content";
import {onMounted, reactive, toRefs} from "vue";
export default {
    name: "Tabs",
    props:{
        data:{
            type:Array,
            default:()=>[]
        },
        initialIdx: {
            type:Number,
            default:1
        }
    },
    components:{
        TabItem,
        TabContent
    },
    setup(){
        const state = reactive({CurrentIdx:0});
        onMounted(()=>{
            state.CurrentIdx = props.initialIdx;
        });
        const clickItem = (index)=>{
            state.CurrentIdx = index;
        };
        return {
            ...toRefs(state),
            clickItem
        };
    }
}
</script>
  • CurrentIdx定义为响应式数据,子组件通过curIdx属性获取当前选中的tab项。
  • onMounted是composition API,使用方式和Vue2.0的钩子函数类似。
  • …toRefs(state)将响应式数据平铺,以便于视图中绑定CurrentIdx。

子组件TabItem关键代码如下:

<template>
  <div class="tab-item" @click = "clickItem()">
      <a href="javascript:;" 
      :class = "{active: index === curIdx}"
      >{{text}}</a>
  </div>
</template>
<script>
import {getCurrentInstance} from 'vue'
export default {
    name: "TabItem",
    props: {
        text: String,
        curIdx: Number
    },
    setup(props,ctx){
        const instance = getCurrentInstance();
        const index = instance.vnode.key;
        const clickItem = ()=>{
            ctx.emit("click-item",index);
        }
        return {index, clickItem};
    }
}
</script>
  • 父组件Tabs在渲染的每个TabItem时,已经将key值传入,作为每个Tabs项的Id。通过Composition API “getCurrentInstance”获取当前组件实例,然后即可获取当前tab项的Id,这样做的好处是我们省去了定义其他属性获取Id的步骤。
  • 当点击TabItem时候,需要将父组件Tabs的响应式数据CurrentIdx进行修改,显然该步骤显然在TabItem组件中无法完成。
  • 因此通过自定义事件click-item,将当前Tabs项的Id通过emit函数传递给父组件Tabs,在Tabs中的clickItem函数中修改CurrentIdx。
  • 调用emit函数不同于Vue2.0的this.$emit方式,通过setup的第二个参数,上下文参数对象,调用emit函数。

子组件TabContent关键代码如下:

<template>
  <div class="tab-content">
      <h1>{{item.title}}</h1>
      <p>Sales: {{ item.content}}</p>
  </div>
</template>

<script>
import {watch} from 'vue'
export default {
    name: "TabContent",
    props:{
        curIdx:Number,
        data:{
            type: Array,
            default: ()=>[]
        },
    },
    setup(props){
        const item = {};
        watch(()=>{
            return props.curIdx;
        },(val)=>{
            const {Month,Sales} = props.data.filter(item=>item.Id === val)[0];
            item.title = Month;
            item.content = Sales;
        });
        return {item};
    }
}
</script>
  • TabContent通过属性接收到curIdx,如果该值发生变化,则需要重新查找指定月份的数据
  • 因此通过watch函数对curIdx进行监听,该函数包括两个函数参数,第一个函数返回要监听的数据,第二个函数在监听数据发生变化后执行。
  • watch函数的使用不同于Vue2.0,该函数也是Composition API,通过对象解构的方式引入。

基于事件代理父子组件通信

父组件Tabs关键代码如下:

<template>
  <div>
      <div class="Tabs" @click = "clickItem($event)">
          <tab-item
            v-for = "item in data"
            :key = "item.Id"
            :text = "item.Month"
            :curIdx = "CurrentIdx"           
          >
          </tab-item>
      </div>
      <tab-content :curIdx = "CurrentIdx" :data = "data"></tab-content>
  </div>
</template>
<script>
import TabItem from "./tab-item";
import TabContent from "./tab-content";
import {onMounted, reactive, toRefs} from "vue";
export default {
    name: "Tabs",
    props:{
        data:{
            type:Array,
            default:()=>[]
        },
        initialIdx: {
            type:Number,
            default:1
        }
    },
    components:{
        TabItem,
        TabContent
    },
    setup(){
        const state = reactive({CurrentIdx:0});
        onMounted(()=>{
            state.CurrentIdx = props.initialIdx;
        });
        const clickItem = (e)=>{
            state.CurrentIdx = Number(e.target.getAttribute("date-index")) || 1;
        };
        return {
            ...toRefs(state),
            clickItem
        };
    }
}
</script>

子组件TabItem关键代码如下:

<template>
  <div class="tab-item" :data-index="index">
      <a href="javascript:;" 
      :class = "{active: index === curIdx}"
      >{{text}}</a>
  </div>
</template>

<script>
import {getCurrentInstance} from 'vue'
export default {
    name: "TabItem",
    props: {
        text: String,
        curIdx: Number
    },
    setup(props,ctx){
        const instance = getCurrentInstance();
        const index = instance.vnode.key;
        return {index};
    }
}vuex
</script>

子组件TabContent关键代码同上。

  • 通过事件代理的方式获取当前选中Tabs项Id,因此将clickItem方法绑定在父组件Tabs上
  • TabItem不再需要绑定任何click事件,只需要将当前Tabs项的Id,绑定到data-index属性上,便于父组件通过事件代理函数的参数获取。
  • 在点击TabItem组件后,利用事件冒泡原理,触发Tabs的click方法,通过方法参数,获取当前的Tabs项Id。
  • 在点击事件的方法中,将父组件Tabs中的CurrentIdx的值修改为当前点击的Tabs项Id,该Id通过事件代理参数获取。

基于vuex实现父子组件通信

父组件Tabs关键代码如下

<template>
  <div>
      <div class="Tabs" >
          <tab-item
            v-for = "item in data"
            :key = "item.Id"
            :text = "item.Month"
            :curIdx = "CurrentIdx"         
          >
          </tab-item>
      </div>
      <tab-content :curIdx = "CurrentIdx" :data = "data"></tab-content>
  </div>
</template>
<script>
import TabItem from "./tab-item";
import TabContent from "./tab-content";
import {onMounted,computed} from "vue";
import {useStore} from "vuex"
export default {
    name: "Tabs",
    props:{
        data:{
            type:Array,
            default:()=>[]
        },
        initialIdx: {
            type:Number,
            default:1
        }
    },
    components:{
        TabItem,
        TabContent
    },
    setup(props,ctx){
        const store = useStore();
        const state = store.state;
        onMounted(()=>{
            ctx.commit("setCurIdx", props.initialIdx);
        });
        const CurrentIdx = computed(()=>{
            return store.state.curIdx;
        })
        return {CurrentIdx};
    }
}
</script>

子组件TabItem关键代码如下:

<template>
  <div class="tab-item" @click ="clickItem()">
      <a href="javascript:;" 
      :class = "{active: index === curIdx}"
      >{{text}}</a>
  </div>
</template>

<script>
import {useStore} from "vuex"
import {getCurrentInstance} from 'vue'
export default {
    name: "TabItem",
    props: {
        text: String,
        curIdx: Number
    },
    setup(props,ctx){
        const store = useStore();
        const state = store.state;
        const instance = getCurrentInstance();
        const index = instance.vnode.key;
        const clickItem = ()=>{
            state.commit("setCurIdx",index);
        }
        return {index, clickItem};
    }
}
</script>

store/index.js 代码如下:

import {createStore} from "vuex"
export default createStore({
    state: {curIdx: 1,},
    mutations:{
        setCurIdx(state,curIdx){
            state.curIdx = curIdx;
        }
    }
});
  • 选中Tabs项的Id不再通过父组件进行管理,而是通过全局store对象进行管理。
  • clickItem函数放到TabItem中执行即可,子组件通过调用mutations中的setCurIdx方法,直接修改选中的TabItem的Id。
  • 父组件Tabs通过设置计算属性CurrentIdx,实现组件的联动。即无论store中的curIdx在那个组件被修改,父组件立刻就得知该变化。

基于依赖注入的父子组件通信

父组件Tabs关键代码如下:

<template>
  <div>
    <div class="Tabs">
      <tab-item v-for="item in data" :key="item.Id" :text="item.Month" />
    </div>
    <tab-content :data="data" />
  </div>
</template>

<script>
import TabItem from "./item";
import TabContent from "./tabcontent";
import { provide, reactive, toRefs } from "vue";
export default {
  name: "TabsDI",
  props: {
    data: {
      type: Array,
      default: () => []
    },
    InitialIdx: { type: Number, default: 1 }
  },
  components: {
    TabItem,
    TabContent
  },

  setup(props) {
    const state = reactive({
      currentIdx: props.InitialIdx
    });
    const clickItem = index => {
      state.currentIdx = index;
    };
    const data = toRefs(state);
    provide("currentIdx", data.currentIdx);
    provide("clickItem", clickItem);
    return {
      ...data
    };
  }
};
</script>
  • 子组件TabItem和TabContent 不再需要通过属性获取当前选中项的Id
  • 父组件TabsDI通过provide方法将当前选中项currentIdx 和修改该Id的方法clickItem传递给子组件
  • 本例子通过reactive方法创建响应式数据,请注意const data = toRefs(state);这句不可以省略。state.currentIdx只是一个数值,必须通过该方法转换为响应式数据。否则组件无法监控器变化。

子组件TabItem关键代码如下:

<template>
  <div class="tab-item" @click="clickItem(index)">
    <a href="javascript:;" :class="{ active: curIdx === index }">{{ text }}</a>
  </div>
</template>

<script>
import { getCurrentInstance, inject } from "vue";
export default {
  name: "TabItem",
  props: {
    text: String
  },
  setup() {
    const instance = getCurrentInstance();
    const index = instance.vnode.key;
    const clickItem = inject("clickItem");
    const curIdx = inject("currentIdx");

    return { curIdx, index, clickItem };
  }
};
</script>
  • 子组件通过inject方法获取当前选中项的Id
  • 子组件通过inject方法获取修改父组件选中项的方法

子组件TabContent关键代码如下:

<template>
  <div class="tab-content">
    <h1>{{ item.title }}</h1>
    <p>Sales: {{ item.content }}</p>
  </div>
</template>

<script>
import { inject, computed } from "vue";
export default {
  name: "TabContent",
  props: {
    curIdx: {
      type: Number,
      default: 0
    },
    data: {
      type: Array,
      default: () => []
    },
    text: String
  },
  setup(props) {
    const val = inject("currentIdx");
    const item = computed(() => {
      const { Month, Sales } = props.data.filter(
        item => item.Id === val.value
      )[0];
      return {
        title: Month,
        content: Sales
      };
    });
    return { item };
  }
};
</script>
  • 通过inject方法获取当前选中项Id
  • 因为本例不再使用属性传值的方式获取当前选中项Id,所以不再使用watch方法监控属性变化
  • 由于当前选中项Id值currentIdx依然是响应式数据,因此采用computed方法监控器变化。如果变化则更新销量数据

附录

数据结构

export default [
	{Id:1,Month:"January",Sales:100000},
	{Id:1,Month:"February",Sales:200000},
	{Id:1,Month:"March",Sales:300000},
	{Id:1,Month:"April",Sales:300000},
	{Id:1,Month:"May",Sales:400000},
]
Logo

前往低代码交流专区

更多推荐