一、父子组件
父传子:基于props属性来完成
在父组件中调用子组件,想让子组件中具备不同的信息(每个子组件都是单独的实例)
- 属性传递的时候,属性名尽可能不要出现大写(因为你设置为大写,最后也是基于小写传递的),例如:sumNum => supnum
- 属性名设置为kabab-case模式,在子组件注册的时候用 camalCase 或 PasalCase来 注册接收,通常都是用大驼峰注册,例如:sup-num => SupNum
- 传递的属性值默认都是字符串,如果需要传递其他类型值,我们需要基于v-bind处理,例如::supnum="10" 这样传递过去的就是Number类型
非props的attribute:调用组件的时候如果设置的属性是id/class/style这类内置样式属性,也会传递给子组件,vue会默认帮我们把样式和组件的样式进行合并处理,无需我们在props中注册处理
接下来就用vote投票组件实现父传子和子传父
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组件
子组件中注意事项:
子组件想要用父组件传递的属性,需要注册一下(注册完成后,会把当前属性挂载到实例上 {{title}} / this.title- 属性注册时候的校验规则(
即使校验失败,组件还会正常的渲染,只不过控制台会有错误信息提示) 基于父组件传递给子组件的属性值是不建议去直接修改操作的(项目中我们可以把获取的属性值再赋值给组件的 DATA/COMPUTED,操作都是按照DATA来操作)- 这里要注意的是把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组件进行实现
父组件(祖先)
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组件进行实现
父操作子
- 给子组件设置REF,最后存储的结果是当前子组件的实例(这样我们就可以修改其中的数据信息了)
console.log(this.$refs.AAA); 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组件进行实现
祖先(父)
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实现组件通信
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>