一、关于组件通信
组件是 vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。
组件可以有以下几种关系:父子关系
, 兄弟关系
,隔代关系(可能隔多代)
。
本文总结了vue组件间通信的几种方式,如props
、$emit/$on
、vuex
、ref/$parent / $children
、$attrs/$listeners
和provide/inject
。
图示如下:
二、组件通信的方式:
方法一、props/$emit
父组件A通过props
的方式向子组件B传递,B to A 通过在 B 组件中$emit
, A 组件中 v-on 的方式实现。
1.父组件向子组件传值(子:props,父:v-bind)
//App.vue父组件
<template>
<div id="app">
<users v-bind:users="users"></users>//前者自定义名称便于子组件调用,后者要传递数据名
</div>
</template>
<script>
import Users from "./components/Users"
export default {
name: 'App',
data(){
return{
users:["Henry","Bucky","Emily"]
}
},
components:{
"users":Users
}
}
</script>
//users子组件
<template>
<div class="hello">
<ul>
<li v-for="user in users">{
{user}}</li>//遍历传递过来的值,然后呈现到页面
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props:{
users:{
//这个就是父组件中子标签自定义名字
type:Array,
required:true, // 如果为true则代表必须传值
default: [], // 没有传值则使用默认值,和required二选一
}
}
}
</script>
可以把props写成一个对象。没有通过则提前报错。
总结:父组件通过props向下传递数据给子组件。
注:组件中的数据共有三种形式:data、props、computed
2.子组件向父组件传值(子:this.$emit,父:v-on)
当我们点击“Vue.js Demo”后,子组件向父组件传递值,文字由原来的“传递的是一个值”变成“子向父组件传值”,实现子组件向父组件值的传递。
// 子组件
<template>
<header>
<h1 @click="changeTitle">{
{title}}</h1>//绑定一个点击事件
</header>
</template>
<script>
export default {
name: 'app-header',
data() {
return {
title:"Vue.js Demo"
}
},
methods:{
changeTitle() {
this.$emit("titleChanged","子向父组件传值");//自定义事件 传递值“子向父组件传值”
}
}
}
</script>
// 父组件
<template>
<div id="app">
<app-header v-on:titleChanged="updateTitle" ></app-header>//与子组件titleChanged自定义事件保持一致
// updateTitle($event)接受传递过来的文字
<h2>{
{title}}</h2>
</div>
</template>
<script>
import Header from "./components/Header"
export default {
name: 'App',
data(){
return{
title:"传递的是一个值"
}
},
methods:{
updateTitle(e){
//声明这个函数
this.title = e;
}
},
components:{
"app-header":Header,
}
}
</script>
总结:子组件通过this.$emit(arg1,arg2)(arg1:方法名字,arg2:要传出的值)
将想要传递的值通过函数的形式传出,在父组件接收
方法二、$emit/$on
(事件总线)
这种方法通过一个空的Vue实例作为中央事件总线(事件中心)
,用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案vuex。
在vue文件main.js中,我们利用Vue.prototype.bus=new Vue()
来定义,此时我们就有了一个中间量。
例:
//第一个子组件
<template>
<div>
<div id="title">我是第一个子组件</div>
我要给第二个兄弟发信息,内容是:
<input type="text" v-model="to" />
</div>
<button @click="toBrother">点我给兄弟传值</button>
</div>
</template>
<script>
export default {
data() {
return {
to: "哈喽老二"
};
},
methods: {
toBrother() {
this.bus.$emit("toBrother", this.to);
}
}
};
</script>
在button上绑定了一个方法,在方法内部使用中间变量bus中的$emit来传递值,参数同子传父的参数一致。
//第二个子组件
<template>
<div>
<div id="title">我是第二个子组件</div>
<div>我得到的兄弟组件信息是:{
{get}}</div>
</div>
</template>
<script>
export default {
data() {
return {
get: ""
};
}
beforeCreate() {
this.bus.$on("toBrother", msg => {
this.get = msg;
});
}
};
</script>
在第二个子组件里面通过一般会在 mounted 或 created 钩子中来监听传过来的值,这时候需要用this.bus.$on来接收
,第一个参数是this.bus.$emit定义的第一个方法名
,第二个参数是一个方法
,此方法带一个返回参数。
方法三、Vuex
Vuex的使用详见:Vuex的基本使用与概念解析
方法四、$parent / $children与 ref
- ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
- $parent / $children:访问父 / 子实例
这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。
$parent
为当前组件树的根实例
,如果当前实例没有父实例,此实例将会是其自己。
$children
为当前组件的直接子组件,是一个无序的数组
。
如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件
,并且使用 Array 作为真正的来源。
使用$children来调用子组件的方法:
this.$children.forEach(child => {
child.todo()
})
用 ref来访问组件的例子:
// component-a 子组件
export default {
data () {
return {
title: 'Vue.js'
}
},
methods: {
sayHello () {
window.alert('Hello');
}
}
}
// 父组件
<template>
<component-a ref="comA"></component-a>
</template>
<script>
export default {
mounted () {
const comA = this.$refs.comA;
console.log(comA.title); // Vue.js
comA.sayHello(); // 弹窗
}
}
</script>
这两种方法的弊端是无法在跨级或兄弟间通信。
// parent.vue
<component-a></component-a>
<component-b></component-b>
<component-b></component-b>
想在 component-a 中,访问到引用它的页面中(这里就是 parent.vue)的两个 component-b 组件,那这种情况下,就得配置额外的插件或工具了,比如 Vuex 和 Bus 的解决方案。
方法五、$attrs/$listeners
多级组件嵌套需要传递数据时,通常使用的方法是通过vuex。但如果仅仅是传递数据,而不做中间处理,使用 Vuex 处理,未免有点大材小用。为此Vue2.4 版本提供了另一种方法——$attrs/$listeners
。
$attrs
:包含了父作用域中不被 prop 所识别(子组件中props里未调用父组件传进的参数)的特性绑定
(class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件
。通常配合 interitAttrs 选项一起使用。$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。
A组件与C组件通信时
一般用B来做中转站,当A组件需要把信息传给C组件时,B接受A组件的信息,然后利用属性传给C组件,这是一种解决方案,但是如果嵌套的组件过多,会导致代码繁琐,代码维护比较困难;如果C中状态的改变需要传递给A, 使用事件系统一级级往上传递 。
默认情况下父作用域的不被认作props(子组件中props里未调用父组件传进的参数)的属性
,将会“回退”且作为普通的HTML特性应用在子组件的根元素上。
1.$attrs
在Vue2.4中新增选项inheritAttrs
inheritAttrs的默认值为true, 将inheritAttrs的值设为false, 这些默认的行为会禁止掉。但是通过实例属性$attrs
,可以将这些特性生效,且可以通过v-bind 绑定到子组件的非根元素上
。
接下来我们看个跨级通信的例子:
// index.vue
<template>
<div>
<h2>浪里行舟</h2>
<child-com1
:foo="foo"
:boo="boo"
:coo="coo"
:doo="doo"
title="前端工匠"
></child-com1>
</div>
</template>
<script>
const childCom1 = () => import("./childCom1.vue");
export default {
components: {
childCom1 },
data() {
return {
foo: "Javascript",
boo: "Html",
coo: "CSS",
doo: "Vue"
};
}
};
</script>
// childCom1.vue
<template class="border">
<div>
<p>foo: {
{ foo }}</p>
<p>childCom1的$attrs: {
{ $attrs }}</p>
<child-com2 v-bind="$attrs"></child-com2>
</div>
</template>
<script>
const childCom2 = () => import("./childCom2.vue");
export default {
components: {
childCom2
},
inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性
props: {
foo: String // foo作为props属性绑定
},
created() {
console.log(this.$attrs); // { "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠"
//此时输出元素中没有props里的foo
}
}
};
</script>
// childCom2.vue
<template>
<div class="border">
<p>boo: {
{ boo }}</p>
<p>childCom2: {
{ $attrs }}</p>
<child-com3 v-bind="$attrs"></child-com3>
</div>
</template>
<script>
const childCom3 = () => import("./childCom3.vue");
export default {
components: {
childCom3
},
inheritAttrs: false,
props: {
boo: String
},
created() {
console.log(this.$attrs); // {"coo": "CSS", "doo": "Vue", "title": "前端工匠" }
//此时输出元素中没有props里的foo和boo
}
};
</script>
// childCom3.vue
<template>
<div class="border">
<p>childCom3: {
{ $attrs }}</p>
//此时$attrs应该是{"doo": "Vue"}
</div>
</template>
<script>
export default {
props: {
coo: String,
title: String
}
};
</script>
2.$listeners
同时vue2.4版本新增了$listeners
属性,$listeners
是组件的内置属性,它的值是父组件(不含.native修饰器的) v-on事件监听器
。
组件可以通 过在自己的子组件上使用v-on=”$listeners”
,进一步把值传给自己的子组件
。如果子组件已经绑定$listener中同名的监听器,则两个监听器函数会以冒泡的方式先后执行。
例:
父组件:
<template>
<div class="test">
<child v-bind="{name, sex, age}" v-on="{changeName,changeAge}"></child>
</div>
</template>
<script>
import child from './child'
export default {
data() {
return {
name: '张三',
sex: '男',
age: 11,
}
},
components: {
child
},
methods: {
changeName(name) {
this.name = name
},
changeAge(age) {
this.age = age
}
}
}
</script>
子组件child.vue
<template>
<div class="child">
child组件的$attrs {
{$attrs}}
<child-child v-bind="$attrs" v-on="$listeners" @showAttrs="showAttrs"></child-child>
</div>
</template>
<script>
import childChild from './child-child'
export default {
name: "child",
props: ['name'],
inheritAttrs: false,
created() {
console.log('child', this.$listeners)
},
components: {
childChild
},
methods: {
showAttrs() {
console.log(this.$attrs)
}
}
}
</script>
孙子组件:child-child.vue
<template>
<div class="child-child">
child-child组件的$attrs {
{$attrs}}
</div>
</template>
<script>
export default {
name: "child-child",
inheritAttrs: false,
created() {
console.log('child-child',this.$listeners)
}
}
</script>
打印结果:
子组件会打印出父组件的全部方法,而孙子组件会打印出父组件+子组件的全部方法。它的值是父组件(不含.native修饰器的) v-on事件监听器
。
注:孙子组件会有爷爷组件的方法是因为在子组件v-on绑定了$listeners
,而$listeners的值恰好是父组件(不含.native修饰器的) v-on事件监听器
方法六、provide/inject
祖先组件
中通过provider来提供变量
,然后在子孙组件
中通过inject来注入变量
。
provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系
。
假设有两个组件: A.vue 和 B.vue,B 是 A 的子组件:
// A.vue
export default {
provide: {
name: '29Kun'
}
}
// B.vue
export default {
inject: ['name'],
mounted () {
console.log(this.name); // 29Kun
}
}
可以看到,在 A.vue 里,我们设置了一个 provide: name
,值为 29Kun,它的作用就是将 name 这个变量提供给它的所有子组件。而在 B.vue 中,通过inject
注入了从 A 组件中提供的 name 变量,那么在组件 B 中,就可以直接通过 this.name 访问这个变量了
,它的值也是 29Kun。这就是 provide / inject API 最核心的用法。
注意的是:provide 和 inject 绑定并不是可响应的
。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。所以,上面 A.vue 的 name 如果改变了,B.vue 的 this.name 是不会改变的,仍然是 29Kun。
provide与inject 怎么实现数据响应式
(日后再说):
一般来说,有两种办法:
- provide祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的东西比如props,methods
- 使用2.6最新API Vue.observable 优化响应式 provide(推荐)
参考:https://www.jianshu.com/p/85b27a2981d8
https://segmentfault.com/a/1190000019208626
https://blog.csdn.net/songxiugongwang/article/details/84001967
https://blog.csdn.net/liangrongliu1991/article/details/100555091