1. 组件通信的场景
- 父->子组件间的数据传递
- 子->父组件间的数据传递
- 兄弟组件间的数据传递
- 组件深层嵌套,祖先组件与子组件间的数据传递
2. 组件通信的方式
1. 组件通信一:父子通信
父组件通过自定义属性向子组件传值
子组件通过props接收父组件的值
1.1 父->子组件传递数据的案例
html模板
<div id="app">
<h1>父组件的内容----{{num}}</h1>
<p><button @click="change">改变父组件自己的值:{{msg}}</button></p>
<hr>
<!-- 父组件向子组件传值 -->
<child :msg="msg" :num="num" :things-list="thingsList"></child>
</div>
js代码
<script src="./js/vue.js"></script>
<script>
let Child = {
props: ['msg', 'num', 'thingsList'],
template: `
<div class="box">
<p><button>子组件</button></p>
<p><em>{{msg}}----{{num}}</em></p>
<p>{{thingsList}}</p>
</div>
`
}
//根组件,父组件
var vm = new Vue({
el: '#app',
data: {
msg: 'Happy New Year',
num: [23, 45],
thingsList: ['a']
},
methods: {
change() {
this.msg = '父组件改变的值'
}
},
components: {
Child
}
});
</script>
1.2 父->子组件传递数据的注意事项
- 每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值
- 不应该在一个子组件内部改变 prop。如果这样做了,Vue 会在浏览器的控制台中发出警告。
- 可以先把该值赋给子组件自己的变量,然后去更改复制后的变量
- 如果你传进来的是个对象,同时你又需要在子组件中操作传进来的这个数据,那么在父组件中的这个数据也会改变,因为你传递的只是个引用
- 你只能对对象做深拷贝创建一个副本才能继续操作
1.3. Props
1.3.1 Prop的大小写
使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:
<child :msg="msg" :num="num" :things-list="thingsList"></child>
在子组件内部用驼峰的方式接收
props: ['thingsList'],
1.3.2 Prop的类型
通常希望每个 prop 都有指定的值类型。
<script>
Vue.component("Child", {
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
},
template: ``
})
</script>
1.3.3 Prop的验证
Vue.component('my-component', {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
2. 组件通信二:子父通信
2.1 子父通信
子组件传递数据给父组件是通过$emit触发自定义事件来做到的
父组件通过监听子组件发送的自定义事件来接收数据
推荐始终使用 kebab-case (短横线分隔命名)的事件名
<div id="app">
<h1>父组件</h1>
<p>子组件传过来的值:{{info}}</p>
<hr>
<!-- send即为自定义事件 -->
<child @send-data="handleSend"></child>
</div>
<script src="./js/vue.js"></script>
<script>
//子组件
let Child = {
template: `
<div class="box">
<p><button @click="handleClick">向父组件传值</button></p>
</div>
`,
data() {
return {
msg: '子组件的数据'
}
},
methods: {
handleClick() {
//$emit向父组件发送一个自定义事件
this.$emit("send-data", this.msg)
}
}
}
var vm = new Vue({
el: '#app',
data: {
info: ''
},
methods: {
handleSend(param) {
console.log(param);
this.info = param
}
},
components: {
Child
}
});
</script>
2.2 组件中特殊的事件绑定
2.2.1 将原生事件绑定到组件
在一个组件的根元素上直接监听一个原生事件,需要使用native修饰符
<div id="app">
<!-- 直接绑定原生事件无效 -->
<!-- <child @click="show"></child> -->
<!-- 添加native修饰符 -->
<child @click.native="show"></child>
</div>
<script src="./js/vue.js"></script>
<script>
// 子组件
let Child = {
template: `
<div class="box">
<button>按钮1</button>
<br>
<button>按钮2</button>
</div>
`
}
// 根组件
var vm = new Vue({
el: '#app',
data: {},
methods: {
show() {
alert(1);
}
},
components: {
Child
}
});
</script>
2.2.2 .sync修饰符
真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件都没有明显的变更来源。
可以使用sync修饰符进行父子组件双向绑定
<div id="app">
<!-- 监听更新属性的事件 -->
<!-- <child @update:msg="getData"></child> -->
<!--
update:msg的语法糖
动态绑定父组件属性,属性后添加sync修饰符
-->
<!--:msg.sync为扩展事件的监听方式,表示当监听到该事件时,将同步的把父组件msg的值修改为事件中携带的参数-->
<child :msg.sync="msg"></child>
<br> {{msg}}
</div>
<script src="./js/vue.js"></script>
<script>
let Child = {
template: `
<button @click="send">改变</button>
`,
methods: {
send() {
// 因为子组件要修改父组件,要在属性前加update
// update:msg为扩展的事件,目的是配合sync使用
this.$emit("update:msg", 'child')
}
}
}
var vm = new Vue({
el: '#app',
data: {
msg: 'parent'
},
methods: {
getData(newVal) {
this.msg = newVal
}
},
components: {
Child
}
});
</script>
3. 组件通信三: 兄弟组件通信
思路: 把兄弟组件共享的数据定义在父组件,A组件通过子传父的方式,向父组件传值,父组件再通过父向子的方式,向B组件传值
4. 组件通信四:Bus总线传值
-
适用场景 : 非父子组件传值
-
中央事件总线(EventBus 非父子组件间通信)
新建一个Vue事件bus对象,然后通过bus. e m i t 触 发 事 件 , b u s . emit触发事件,bus. emit触发事件,bus.on监听触发的事件
-
缺点
EventBus通信方式是无法进行有效的组件化开发的,假设一个场景,一个页面上有多个公共组件,我们只要向其中的一个传递数据,但是每个公共组件都绑定了数据接收的方法。
css样式
<style>
.brother1 {
border: 1px solid #000;
margin: 50px;
}
.brother2 {
border: 1px solid #000;
margin: 50px;
}
</style>
html代码
<div id="app">
<brother1></brother1>
<brother2></brother2>
</div>
<script src="./js/vue.js"></script>
<script>
Vue.component('brother1', {
data() {
return {
mymessage: 'hello brother1'
}
},
template: `
<div class="brother1">
<p>this is brother1 component!</p>
<input type="text" v-model="mymessage" @input="passData(mymessage)">
</div>
`,
methods: {
passData(val) {
//触发全局事件globalEvent
bus.$emit('globalEvent', val)
}
}
})
Vue.component('brother2', {
template: `
<div class="brother2">
<p>this is brother2 component!</p>
<p>brother1传递过来的数据:{{brothermessage}}</p>
</div>
`,
data() {
return {
mymessage: 'hello brother2',
brothermessage: ''
}
},
mounted() {
//绑定全局事件globalEvent
bus.$on('globalEvent', (val) => {
this.brothermessage = val;
})
}
})
//中央事件总线
var bus = new Vue();
var app = new Vue({
el: '#app'
})
</script>
5. 组件通信五: Vuex
vuex算是vue中处理复杂组件通信的最佳方案,毕竟vue和vuex一个娘胎里出来的。而且vuex底层也是用vue实现的。后续会有专门的文章来讲解Vuex
6. 组件通信六: a t t r 和 attr和 attr和listener
attrs和attrs和listeners 主要用于孙组件获取父组件的属性和方法
6.1 $attrs
$attrs
是在vue的2.40版本以上添加的- 项目中有多层组件传参可以使用
$attrs
- 使用普通的父子组件传参
prop
和$emit
,$on
会很繁琐 - 使用
vuex
会大材小用,只是在这几个组件中使用,没必要使用vuex
- 使用事件总线
eventBus
,使用不恰当的话,有可能会出现事件多次执行
6.2 $attrs 实现步骤
步骤1:祖先组件自定义属性
<div id="app">
<!-- 使用子组件 -->
<child :msg="msg" :num="num"></child>
</div>
<script src="./js/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
msg: '祖先的数据',
num: 10
},
components: {
// 注册子组件
Child
},
methods: {}
});
</script>
父组件(Father.vue),给子组件关联数据,子组件如果不用props接收,那么这些数据就作为普通的HTML特性应用在子组件的根元素上
步骤2: 中间层组件添加v-bind="$attrs"
// 子组件
let Child = {
// 注册孙组件
components: {
Grandson
},
// 使用孙组件
template: `
<grandson v-bind="$attrs"></grandson>
`,
inheritAttrs: true,
}
inheritAttrs: false
的含义是不希望本组件的根元素继承父组件的attribute,同时父组件传过来的属性(没有被子组件的props接收的属性),也不会显示在子组件的dom元素上,
步骤3: 后代组件用$attrs接受数据
// 孙组件
let Grandson = {
template: `
<h1></h1>
`,
created() {
console.log(this.$attrs);
}
}
6.3 $listener
l i s t e n e r s − − 属 性 , 它 是 一 个 对 象 , 里 面 包 含 了 作 用 在 这 个 组 件 上 的 所 有 监 听 器 , 你 就 可 以 配 合 ‘ v − o n = " listeners--属性,它是一个对象,里面包含了作用在这个组件上的所有监听器,你就可以配合 `v-on=" listeners−−属性,它是一个对象,里面包含了作用在这个组件上的所有监听器,你就可以配合‘v−on="listeners"` 将所有的事件监听器指向这个组件的某个特定的子元素。
<div id="app">
根组件A <br>
<com-b :msg-a1="msgA1" :msg-a2="msgA2" @test1="onTest1" @test2="onTest2"></com-b>
</div>
<script src="./js/vue.js"></script>
<script>
let ComC = {
template: `
<div class="c"> 孙组件ComC </div>
`,
mounted() {
// 孙组件直接获取A组件的属性值
console.log('c', this.$attrs);
// A组件可以直接监听到C组件触发的事件
// console.log('c', this.$listeners);
this.$emit("test1", 'C的数据')
}
}
let ComB = {
// C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性
// 通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)
template: `
<div class="b">
子组件ComB
<br>
<com-c v-bind="$attrs" v-on="$listeners"></com-c>
</div>
`,
components: {
ComC
}
}
// 根组件A
var vm = new Vue({
name: "A",
el: '#app',
data: {
// 根组件A的数据
msgA1: 'msgA1',
msgA2: 'msgA2'
},
methods: {
// 根组件A的事件处理程序
onTest1(data) {
console.log('onTest1', data);
},
onTest2() {
console.log('onTest2');
}
},
components: {
ComB
}
});
</script>
7.组件通信七: provide和inject
- provide 和 inject 方式通常用于祖孙组件之间的通信,主要用于祖先组件向子组件传值
父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。
不论子组件有多深,只要调用了inject那么就可以注入provider中的数据。
<div id="app">
<parent></parent>
</div>
<script src="./js/vue.js"></script>
<script>
Vue.component('child', {
inject: ['for'], //得到父组件传递过来的数据
data() {
return {
mymessage: this.for
}
},
template: `
<div>
<input type="tetx" v-model="mymessage">
</div>
`
})
Vue.component('parent', {
template: `
<div>
<p> this is parent compoent! </p>
<child></child>
</div>
`,
// 向后代传递数据
provide: {
for: '父组件的test'
},
data() {
return {
message: 'hello'
}
}
})
var vm = new Vue({
el: '#app',
})
</script>
8. 组件通信八: r e f s 、 refs、 refs、parent和$children
parent和children 用于有直接父子关系 的 组件 之间的通信
-
$refs 获取子组件的实例
-
$parent 用于 获取组件的父组件实例
-
$children 用于 获取组件的子组件实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h3>父组件----{{msg}}---------------</h3>
<com-a ref="coma"></com-a>
<com-b ref="comb"></com-b>
</div>
<script src="./js/vue.js"></script>
<script>
// 子组件
const ComA = {
template: '<p>子组件A--{{val}}--<button @click="callParentFun">调用父组件的方法</button></p>',
data(){
return {
childNum: 10
}
},
computed: {
val(){
return this.$parent.msg
}
},
methods: {
childFun(){
console.log('子组件的方法');
},
callParentFun(){
this.$parent.parentFun()
}
}
}
// 子组件
const ComB = {
template: `<p>子组件B</p>`
}
// 根组件
const vm = new Vue({
el: '#app', //绑定到DOM上
data: {
msg: 'old'
},
components: {
ComA,
ComB
},
methods: {
parentFun(){
console.log('父组件方法');
}
},
mounted(){
// 通过this.$ref或this.$children访问子组件的数据或方法
// console.log(this.$refs.coma.childNum);
// this.$refs.coma.childFun();
// console.log(this.$children[0].childNum);
// this.$children[0].childFun()
}
})
</script>
</body>
</html>