4. vue组件通信

140 阅读1分钟

一、父子组件

父传子:基于props属性来完成

在父组件中调用子组件,想让子组件中具备不同的信息(每个子组件都是单独的实例)

  1. 属性传递的时候,属性名尽可能不要出现大写(因为你设置为大写,最后也是基于小写传递的),例如:sumNum => supnum
  2. 属性名设置为kabab-case模式,在子组件注册的时候用 camalCase 或 PasalCase来 注册接收,通常都是用大驼峰注册,例如:sup-num => SupNum
  3. 传递的属性值默认都是字符串,如果需要传递其他类型值,我们需要基于v-bind处理,例如::supnum="10" 这样传递过去的就是Number类型

非props的attribute:调用组件的时候如果设置的属性是 id/class/style 这类内置样式属性,也会传递给子组件,vue会默认帮我们把样式和组件的样式进行合并处理,无需我们在props中注册处理

接下来就用vote投票组件实现父传子和子传父

image.png

vote组件

// 父组件Vote.vue
<template>
  <div class="container">
    <!-- 父组件通过自定义属性传递属性,子组件通过props接收-->
    <vote-header title="支持学习vue人数?" :supnum="supnum" :oppnum="oppnum"></vote-header>
    <vote-main :supnum="supnum" :oppnum="oppnum"></vote-main>
    <!-- 
      @changeNum="handle"  调用子组件的时候,我们这样写,就相当于创建一个自定义事件xxx,并且
      向事件池中追加一个叫做handle的方法,而且是把方法放入到vote-footer的事件池中,
      类似于:footer实例.$on('xxx',handle)
     -->
    <vote-footer @changeNum="handle"></vote-footer>
  </div>
</template>

<script>
import VoteHeader from "./voteHeader.vue";
import VoteMain from "./voteMain.vue";
import VoteFooter from "./voteFooter.vue";

export default {
  name: "vote",
  data() {
    return {
      supnum: 0,
      oppnum: 0,
    };
  },
  components: {
    VoteHeader,
    VoteMain,
    VoteFooter,
  },
  methods: {
    handle(type) {
      type === "sup" ? this.supnum++ : this.oppnum++;
    },
  },
};
</script>

<style scoped>
.container {
  width: 400px;
  padding: 10px;
  margin: 20px auto;
  border: 1px solid #aaa;
}

.headerBox {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 40px;
}

.headerBox h3,
.headerBox span {
  font-size: 22px;
  margin: 0;
}
</style>

header组件

子组件中注意事项:

  1. 子组件想要用父组件传递的属性,需要注册一下(注册完成后,会把当前属性挂载到实例上 {{title}} / this.title
  2. 属性注册时候的校验规则(即使校验失败,组件还会正常的渲染,只不过控制台会有错误信息提示
  3. 基于父组件传递给子组件的属性值是不建议去直接修改操作的(项目中我们可以把获取的属性值再赋值给组件的 DATA/COMPUTED,操作都是按照DATA来操作)
  4. 这里要注意的是把props放在data中,父组件更新,子组件不会更新原因是传递过来的属性放在data中,而data在beforecreate和created中间执行,data只执行一次
  • data更新子组件一定更新,只是data只执行一次而已,放在computed中可以更新
  • v-if:控制当前组件销毁,数据更新前销毁,更新后展示(this.$nextTick(()=>>{this.flag = true})),重新走生命周期,所以会重新渲染
// 子组件header.vue

<template>
  <header class="headerBox">
    <h3>{{ title }}</h3>
    <span>{{ supnum + oppnum }}人</span>
  </header>
</template>

<script>

export default {
  name: "voteHeader",
  props: {
    // 设定传递属性的数据格式类型,大写
    // 这样设定就是属性值可以是多种类型皆可
    // title: [String, Number...]
    // required: true 必传
    // default: 指定默认值
    title: {
      type: String,
      required: true
    },
    supnum: {
      type: Number,
      default: 0
    },
    oppnum: {
      type: Number,
      default: 0
      // 自定义校验的规则 val传递的属性值 在函数中,根据自己的规则,
      // 返回true和FALSE来验证传递的值是否符合规则
      // validator(val) {
      // 	return val <= 10;
      // }
    }
  },
  data() {
    return {
      supNum: this.supnum,
      oppNum: this.oppnum
    };
  }
};
</script>

<style scoped>
.headerBox {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 40px;
}

.headerBox h3,
.headerBox span {
  font-size: 22px;
  margin: 0;
}
</style>

main组件

//main.vue

<template>
  <main class="mainBox">
    <p>支持人数:{{ supnum }}</p>
    <p>反对人数:{{ oppnum }}</p>
    <p>支持率:{{ ratio }}</p>
  </main>
</template>

<script>
export default {
  name: "voteMain",
  props: ["supnum", "oppnum"],
  data() {
    return {};
  },
  computed: {
    ratio() {
      let { supnum, oppnum } = this;
      let total = supnum + oppnum;
      if (total === 0) return "--";
      return ((supnum / total) * 100).toFixed(2) + "%";
    },
  },
};
</script>

<style scoped>
.mainBox {
  margin: 10px auto;
}

.mainBox p {
  line-height: 35px;
  margin: 0;
  font-size: 16px;
}
</style>

子传父:$emit

footer组件

//footer.vue

<template>
  <footer class="footerBox">
    <button type="button" class="btn-primary" @click="func('sup')">支持</button>
    <button type="button" class="btn-danger" @click="func('opp')">反对</button>
  </footer>
</template>

<script>
export default {
  name: "voteFooter",
  // 1.emits的数组语法
  // emits: ['changeNum'],

  // 2.emits的对象语法,验证参数(了解)
  // emits: {
  //   add: function(type){
  //     if(!type) return
  //   }
  // },
  methods: {
    func(type) {
      // 通知子组件事件池中的HANDLE自定义事件执行,在vue3中会有emits属性放通知的自定义事件 
      // emits: ['changeNum']
      this.$emit("changeNum", type);
    },
  },
};
</script>

<style scoped>
button {
  border: none;
  border: 1px solid #eee;
  padding: 5px 10px;
  border-radius: 5px;
  margin-right: 10px;
}

.footerBox .btn-primary {
  color: #fff;
  background-color: #409eff;
}

.footerBox .btn-danger {
  color: #fff;
  background-color: #f56c6c;
}
</style>

二、EventBus事件总线(相同祖先的兄弟组件通信)

Vue3从实例中移除了$on、$off和$once方法,使用全局事件总线,要通过第三方的库:mitt/tiny-emitter/hy-event-store

//创建一个全局的Eventbus => $emit  $on  $off
let Eventbus = new Vue();
//挂在到Vue.prototype
Vue.prototype.$bus = Eventbus;

用vote组件进行实现

image.png

父组件(祖先)

vote

//vote.vue

<template>
  <div class="container">
    <vote-header title="你喜欢大海吗?"></vote-header>
    <vote-main></vote-main>
    <vote-footer></vote-footer>
  </div>
</template>

<script>
import VoteHeader from "./voteHeader.vue";
import VoteMain from "./voteMain.vue";
import VoteFooter from "./voteFooter.vue";

export default {
  name: "vote",
  components: {
    VoteHeader,
    VoteMain,
    VoteFooter,
  }
};
</script>

<style scoped>
.container {
  width: 400px;
  padding: 10px;
  margin: 20px auto;
  border: 1px solid #aaa;
}

.headerBox {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 40px;
}

.headerBox h3,
.headerBox span {
  font-size: 22px;
  margin: 0;
}
</style>

子组件(兄弟组件)

当点击footer中的按钮时,传递信息给其他兄弟组件

header

// header.vue

<template>
  <header class="headerBox">
    <h3>{{ title }}</h3>
    <span>{{ count }}人</span>
  </header>
</template>

<script>

export default {
  name: "voteHeader",
  props: ["title", "eventBus"],
  data() {
    return {
      count: 0
    }
  },
  methods: {
    handleVote() {
      this.count++;
    }
  },
  created() {
    // 创建完实例,把方法加入到事件池中
    this.$bus.$on('xxx', this.handleVote);
  }
};
</script>

<style scoped>
.headerBox {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 40px;
}

.headerBox h3,
.headerBox span {
  font-size: 22px;
  margin: 0;
}
</style>

main

//main.vue

<template>
  <main class="mainBox">
    <p>支持人数:{{ supnum }}</p>
    <p>反对人数:{{ oppnum }}</p>
    <p>支持率:{{ ratio }}</p>
  </main>
</template>

<script>
export default {
  name: "voteMain",
  data() {
    return {
      supnum: 0,
      oppnum: 0
    };
  },
  computed: {
    ratio() {
      let { supnum, oppnum } = this;
      let total = supnum + oppnum;
      if (total === 0) return "--";
      return ((supnum / total) * 100).toFixed(2) + "%";
    },
  },
  created() {
    // 创建完实例,把方法加入到事件池中
    this.$bus.$on('xxx', this.handleMain);
  },
  methods: {
    handleMain(type) {
      type === "sup" ? this.supnum++ : this.oppnum++;
    }
  }
};
</script>

<style scoped>
.mainBox {
  margin: 10px auto;
}

.mainBox p {
  line-height: 35px;
  margin: 0;
  font-size: 16px;
}
</style>

footer

//footer.vue

<template>
  <footer class="footerBox">
    <button type="button" class="btn-primary" @click="func('sup')">支持</button>
    <button type="button" class="btn-danger" @click="func('opp')">反对</button>
  </footer>
</template>

<script>
export default {
  name: "voteFooter",
  props: ["eventBus"],
  methods: {
    func(type) {
      // 通知事件池中的方法执行
      this.$bus.$emit('xxx', type);
    }
  }
};
</script>

<style scoped>
button {
  border: none;
  border: 1px solid #eee;
  padding: 5px 10px;
  border-radius: 5px;
  margin-right: 10px;
}

.footerBox .btn-primary {
  color: #fff;
  background-color: #409eff;
}

.footerBox .btn-danger {
  color: #fff;
  background-color: #f56c6c;
}
</style>

三、基于ref & $children 和 $parent实现父子组件信息通信

用vote组件进行实现

image.png

父操作子

  1. 给子组件设置REF,最后存储的结果是当前子组件的实例(这样我们就可以修改其中的数据信息了) console.log(this.$refs.AAA);
  2. this.$children 存储了当前父组件中用到的所有子组件的实例($children是一个数组集合,顺序自己去记住即可) console.log(this.$children);

vote

//main.vue

<template>
  <div class="container">
    <vote-header title="你喜欢大海吗?"></vote-header>
    <vote-main></vote-main>
    <vote-footer></vote-footer>
  </div>
</template>

<script>
import VoteHeader from "./voteHeader.vue";
import VoteMain from "./voteMain.vue";
import VoteFooter from "./voteFooter.vue";

export default {
  name: "vote",
  components: {
    VoteHeader,
    VoteMain,
    VoteFooter,
  },
  data() {
    return {
      supNum: 0,
      oppNum: 0
    }
  },
  mounted() {}
};
</script>

<style scoped>
.container {
  width: 400px;
  padding: 10px;
  margin: 20px auto;
  border: 1px solid #aaa;
}

.headerBox {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 40px;
}

.headerBox h3,
.headerBox span {
  font-size: 22px;
  margin: 0;
}
</style>

子操作父-this.$parent

header

//header.vue

<template>
  <header class="headerBox">
    <h3>{{ title }}</h3>
    <span>{{ $parent.supNum + $parent.oppNum }}人</span>
  </header>
</template>

<script>

export default {
  name: "voteHeader",
  props: ["title"]
};
</script>

<style scoped>
.headerBox {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 40px;
}

.headerBox h3,
.headerBox span {
  font-size: 22px;
  margin: 0;
}
</style>

main

//main.vue

<template>
  <main class="mainBox">
    <p>支持人数:{{ $parent.supNum }}</p>
    <p>反对人数:{{ $parent.oppNum }}</p>
    <p>支持率:{{ ratio }}</p>
  </main>
</template>

<script>
export default {
  name: "voteMain",
  computed: {
    ratio() {
      let { supNum, oppNum } = this.$parent;
      let total = supNum + oppNum;
      if (total === 0) return "--";
      return ((supNum / total) * 100).toFixed(2) + "%";
    },
  }
};
</script>

<style scoped>
.mainBox {
  margin: 10px auto;
}

.mainBox p {
  line-height: 35px;
  margin: 0;
  font-size: 16px;
}
</style>

footer

//footer组件

<template>
  <footer class="footerBox">
    <button type="button" class="btn-primary" ref="supBtn" @click="handle(1)">支持</button>
    <button type="button" class="btn-danger" ref="oppBtn" @click="handle(0)">反对</button>
  </footer>
</template>

<script>
export default {
  name: "voteFooter",
  methods: {
    handle(type) {
      type == 1 ? this.$parent.supNum++ : this.$parent.oppNum++;
    }
  },
  mounted(){
     // this.$refs是一个对象,对象中存储了所有页面中设定的ref和对应的元素对象
     //(ref帮助我们获取指定的DOM元素对象)
     // { supBtn:DOM元素对象, oppBtn:DOM元素对象 ....}
     // console.log(this.$refs);

    // this.$parent获取其所在父组件的实例
    // console.log(this.$parent);
  }
};
</script>

<style scoped>
button {
  border: none;
  border: 1px solid #eee;
  padding: 5px 10px;
  border-radius: 5px;
  margin-right: 10px;
}
.footerBox .btn-primary {
  color: #fff;
  background-color: #409eff;
}
.footerBox .btn-danger {
  color: #fff;
  background-color: #f56c6c;
}
</style>

四、基于祖先后代(或父子)传递-provide & inject

父组件把需要用到的方法和状态全部编写好,子组件只需要注册使用即可

用vote组件进行实现

image.png

祖先(父)

vote

//vote.vue

<template>
  <div class="container">
    <vote-header title="你喜欢大海吗?"></vote-header>
    <vote-main></vote-main>
    <vote-footer></vote-footer>
  </div>
</template>

<script>
import VoteHeader from "./voteHeader.vue";
import VoteMain from "./voteMain.vue";
import VoteFooter from "./voteFooter.vue";

export default {
  name: "vote",
  components: {
    VoteHeader,
    VoteMain,
    VoteFooter,
  },
  // =>我们会创建响应式状态信息先存储公共信息,让PROVIDE中存储的是状态信息:以后只要我们把状态信息修改了,存储在祖先PROVIDE中的信息也会跟着修改;而且比较变态的地方是,我们需要保证PROVIDE中存储的数据是可被监控的,这样DATA中存储的数据需要 以对象 的方式存储,这样才能保证对象中的每个数据也是被监控的
  // => 如果data中不是对象,在vue3中使用computed函数包裹也可以实现 message:'hello world' , 在provide函数中message: computed(()=> this.message),然后注入的时候页面使用 message.value
  data() {
    return {
      obj: {
        supNum: 0,
        oppNum: 0
      }
    }
  },
  // 此方法只有第一次加载组件的时候执行一次,这些数据存储在this._provided:可以是对象也可以是闭包的方式,闭包方式的好处是“他会在实例上的信息都挂在完成后再处理”
  provide() {
    return {
      obj: this.obj,
      handle: this.handle
    };
  },
  methods: {
    handle(type) {
      type === "SUP" ? this.obj.supNum++ : this.obj.oppNum++;
    }
  }
};
</script>

<style scoped>
.container {
  width: 400px;
  padding: 10px;
  margin: 20px auto;
  border: 1px solid #aaa;
}

.headerBox {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 40px;
}

.headerBox h3,
.headerBox span {
  font-size: 22px;
  margin: 0;
}
</style>

后代(子)

header

//header.vue

<template>
  <header class="headerBox">
    <h3>{{ title }}</h3>
    <span>{{obj.supNum + obj.oppNum }}人</span>
  </header>
</template>

<script>


export default {
  name: "voteHeader",
  props: ["title"],
  inject: ['obj']
};
</script>

<style scoped>
.headerBox {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 40px;
}

.headerBox h3,
.headerBox span {
  font-size: 22px;
  margin: 0;
}
</style>

main

//main.vue

<template>
  <main class="mainBox">
    <p>支持人数:{{ obj.supNum }}</p>
    <p>反对人数:{{ obj.oppNum }}</p>
    <p>支持率:{{ ratio }}</p>
  </main>
</template>

<script>
export default {
  name: "voteMain",
  inject: ['obj'],
  computed: {
    ratio() {
      let { supNum, oppNum } = this.obj;
      let total = supNum + oppNum;
      if (total === 0) return "--";
      return ((supNum / total) * 100).toFixed(2) + "%";
    },
  }
};
</script>

<style scoped>
.mainBox {
  margin: 10px auto;
}

.mainBox p {
  line-height: 35px;
  margin: 0;
  font-size: 16px;
}
</style>

footer

//footer.vue

<template>
  <footer class="footerBox">
    <button type="button" class="btn-primary" ref="supBtn" @click="handle('SUP')">支持</button>
    <button type="button" class="btn-danger" ref="oppBtn" @click="handle('OPP')">反对</button>
  </footer>
</template>

<script>
export default {
  name: "voteFooter",
  inject: ['handle']
};
</script>

<style scoped>
button {
  border: none;
  border: 1px solid #eee;
  padding: 5px 10px;
  border-radius: 5px;
  margin-right: 10px;
}
.footerBox .btn-primary {
  color: #fff;
  background-color: #409eff;
}
.footerBox .btn-danger {
  color: #fff;
  background-color: #f56c6c;
}
</style>

五、基于Vuex实现组件通信

image.png

Vuex

// store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
import createLogger from 'vuex/dist/logger';

//=>Vuex是vue中的一个插件
Vue.use(Vuex);//=>基于mixin混入的方式给vue提供一个store
const ENV = process.env.NODE_ENV;
//=>创建store容器并且导出
const store = new Vuex.Store({
  state: {
    supNum: 20,
    oppNum: 15
  },
  //=>存储的方式等价于computed计算属性:监听当前容器中state中的计算属性
  getters: {
    ratio(state) {
      let { supNum, oppNum } = state,
        total = supNum + oppNum;
      return total === 0 ? '--' : ((supNum / total) * 100).toFixed(2) + '%';
    }
  },
  //=>mutations存储sync function,这些方法修改state的状态信息
  mutations: {
    change(state, type) {
      type === 'sup' ? state.supNum++ : state.oppNum++;
    }
  },
  //=>actions存储async function:这些方法首先异步获取需要的数据,再基于commit触发mutations中的方法,从而改变state
  actions: {
    //=>this.$store.dispatch('xx',xx);触发actions执行
    // aaa({commit},payload){
    //     //context.commit('xx',xx);
    // }
  },

  //=>使用logger中间件插件:能够详细输出每次操作之前,当中操作的信息
  plugins: ENV === 'production' ? [] : [createLogger()]
});
export default store;

vote组件

vote

//vote.vue

<template>
  <div class="container">
    <vote-header title="你喜欢大海吗?"></vote-header>
    <vote-main></vote-main>
    <vote-footer></vote-footer>
  </div>
</template>

<script>
import VoteHeader from "./voteHeader.vue";
import VoteMain from "./voteMain.vue";
import VoteFooter from "./voteFooter.vue";

export default {
  name: "vote",
  components: {
    VoteHeader,
    VoteMain,
    VoteFooter,
  }
};
</script>

<style scoped>
.container {
  width: 400px;
  padding: 10px;
  margin: 20px auto;
  border: 1px solid #aaa;
}

.headerBox {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 40px;
}

.headerBox h3,
.headerBox span {
  font-size: 22px;
  margin: 0;
}
</style>

voteHeader

//voteHeader.vue

<template>
  <header class="headerBox">
    <h3>{{ title }}</h3>
    <span>{{ supNum + oppNum }}人</span>
  </header>
</template>

<script>
import { mapState } from 'vuex'
export default {
  name: "voteHeader",
  props: ["title"],
  // 我们把公共的状态和计算属性都赋值给computed,以此保证公共状态改变,视图重新渲染重新更新走beforeUpdate和updated,不重新走生命周期,放在data中组件不会重新渲染,数据不更改
  computed: {
    ...mapState({
      supNum: (state) => state.supNum,
      oppNum: (state) => state.oppNum
    })
    /*
    遍历store容器中的state,获取到的结果是一个新的对象
      mapState(["supNum", "oppNum"]) => {supNum:xxx,oppNum:xxx}
      
      mapState({
        //=>state:this.$store.state
        supNum: state => state.supNum,
        oppNum: state => state.oppNum
      })=> {supNum:f(),oppNum:f()} 里面每一个都是函数,computed要求势函数,所以展开运算符即可
      
      // 可以拿到多级
      ...mapState({
        //=>state:this.$store.state
        supNum: state => state.supNum,
        oppNum: state => state.oppNum
      })
      // 只能拿到一级
      ...mapState(["supNum", "oppNum"]),
      ...mapState("A", ["x", "y"]), 获取指定模块中的公共状态信息
    */
  }
  /* computed: {
    supNum() {
      return this.$store.state.supNum;
    },
    oppNum() {
      return this.$store.state.oppNum;
    },
  }, */
};
</script>

<style scoped>
.headerBox {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 40px;
}

.headerBox h3,
.headerBox span {
  font-size: 22px;
  margin: 0;
}
</style>

voteMain

//voteMain.vue

<template>
  <main class="mainBox">
    <p>支持人数:{{ supNum }}</p>
    <p>反对人数:{{ oppNum }}</p>
    <p>支持率:{{ ratio }}</p>
  </main>
</template>

<script>
import { mapState, mapGetters } from "vuex";
export default {
  name: "voteMain",
  computed: {
    ...mapState(["supNum", "oppNum"]),
    ...mapGetters(["ratio"]),
  },
};
</script>

<style scoped>
.mainBox {
  margin: 10px auto;
}

.mainBox p {
  line-height: 35px;
  margin: 0;
  font-size: 16px;
}
</style>

voteFooter

//voteFooter.vue

<template>
  <footer class="footerBox">
    <button type="button" class="btn-primary" @click="change('sup')">支持</button>
    <button type="button" class="btn-danger" @click="change('opp')">反对</button>
  </footer>
</template>

<script>
import { mapMutations } from "vuex";
export default {
  name: "voteFooter",
  // 我们会把mutations/actions中的方法赋值给组件的methods
  methods: {
    ...mapMutations(["change"]),
    // ...mapMutations({
    //   handle: "change",
    // }),
    /* handle(type) {
      this.$store.commit("change", type);
    }, */
  },
};
</script>

<style scoped>
button {
  border: none;
  border: 1px solid #eee;
  padding: 5px 10px;
  border-radius: 5px;
  margin-right: 10px;
}
.footerBox .btn-primary {
  color: #fff;
  background-color: #409eff;
}
.footerBox .btn-danger {
  color: #fff;
  background-color: #f56c6c;
}
</style>