前言
组件是 vue.js
最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。一般来说,组件可以有以下几种关系:
如上图所示,grandfather
和 parent
、parent
和 childA
、parent
和 childB
都是父子关系,childA
和 childB
是兄弟关系,grandfather
和 childA
、childB
是隔代关系(可能隔多代)。
所以组件通讯是 vue.js
的核心之一,接下来结合代码,来了解各个组件的是怎么通讯的。
一、props
和 $emit
子组件(Child.vue
)的 props
属性能够接收来自父组件(Parent.vue
)数据。没错,仅仅只能接收,props
是单向绑定的,即只能父组件向子组件传递,不能反向。
// 父组件(Parent.vue)
<template>
<div id="parent">
<Child :msg="msg" />
</div>
</template>
<script>
import Child from './Child'
export default {
name: 'parent',
data() {
return {
msg: '这是来自父组件来的数据~~'
}
},
components: {
Child
}
}
</script>
// 子组件(Child.vue)
<template>
<div id="child">
<div>{{ msg }}</div>
</div>
</template>
<script>
export default {
name: 'child',
data() {
return {
}
},
props: {
msg: {
type: String
}
},
methods: {
}
}
</script>
$emit
实现子组件向父组件传值(通过事件形式),子组件通过 $emit
事件向父组件发送消息,将自己的数据传递给父组件。
// 父组件
<template>
<div id="parent">
<div>{{ msg }}</div>
<Child2 @changeMsg="parentMsg" />
</div>
</template>
<script>
import Child2 from './Child2'
export default {
name: 'parent',
data() {
return {
msg: ''
}
},
methods: {
parentMsg( msg ) {
this.msg = msg;
}
},
components: {
Child2
}
}
</script>
// 子组件
<template>
<div id="child">
<button @click="childMsg">传递数据给父组件</button>
</div>
</template>
<script>
export default {
name: 'child',
data() {
return {
}
},
methods: {
childMsg() {
this.$emit( 'changeMsg', '传递数据给粑粑组件' );
}
}
}
</script>
总结:开发组件常用的数据传输方式,父子间传递。
二、$emit
和 $on
实现方式是通过创建一个空的 vue
实例,当做 $emit
事件的处理中心(事件总线),通过它来触发以及监听事件,来实现任意组件间的通信,包含父子,兄弟,隔代组件。
// 父组件
<template>
<div id="parent">
<Child1 :Event="Event" />
<Child2 :Event="Event" />
<Child3 :Event="Event" />
</div>
</template>
<script>
import Vue from 'Vue';
import Child1 from './Child1';
import Child2 from './Child2';
import Child3 from './Child3';
// 公共的实例
const Event = new Vue();
export default {
name: 'parent',
data() {
return {
Event
}
},
components: {
Child1,
Child2,
Child3
}
}
</script>
// 子组件1
<template>
<div id="child1">
1、她的名字叫:{{ name }}
<button @click="send">传递数据给Child3</button>
</div>
</template>
<script>
export default {
name: 'child1',
data() {
return {
name: '柯基慧'
}
},
props: {
Event: Object
},
methods: {
send() {
this.Event.$emit( 'msgA', this.name );
}
}
}
</script>
// 子组件2
<template>
<div id="child2">
1、她的身高:{{ height }}
<button @click="send">传递数据给Child3</button>
</div>
</template>
<script>
export default {
name: 'child2',
data() {
return {
height: '149.9cm'
}
},
props: {
Event: Object
},
methods: {
send() {
this.Event.$emit( 'msgB', this.height );
}
}
}
</script>
// 子组件3
<template>
<div id="child3">
<h3>她的名字叫:{{ name }},身高{{ height }}。</h3>
</div>
</template>
<script>
export default {
name: 'child3',
data() {
return {
name: '',
height: ''
}
},
props: {
Event: Object
},
mounted() {
this.Event.$on( 'msgA', name => {
this.name = name;
} );
this.Event.$on( 'msgB', height => {
this.height = height;
} );
}
}
</script>
总结:在父子,兄弟,隔代组件中都可以互相数据通信,重要的是 $emit
和 $on
事件必须是在一个公共的实例上才能触发。
三、$attrs
和 $listeners
Vue
组件间传输数据在 Vue2.4
版本后增加了新方法 $attrs
和 $listeners
。
$attrs
$attrs
- 包含了父作用域中不作为 props
被识别 (且获取) 的特性绑定 ( class
和 style
除外)。当一个组件没有声明任何 props
时,这里会包含所有父作用域的绑定 ( class
和style
除外),并且可以通过 v-bind="$attrs"
传入内部组件 - 在创建高级别的组件时非常有用。 简单点讲就是包含了所以父组件在子组件上设置的属性(除了 props
传递的属性、class
和 style
)。
想象一下,你打算封装一个自定义input组件 - MyInput
,需要从父组件传入 type
,placeholder
,title
等多个html元素的原生属性。此时你的 MyInput
组件 props
如下:
props:['type', 'placeholder', 'title', ...]
如果它的属性越多,那子组件就要定义更多的属性,会很影响阅读,所以,$attrs
专门为了解决这种问题而诞生,这个属性允许你在使用自定义组件时更像是使用原生 html
元素。比如:
// 父组件
<template>
<div id="parentAttrs">
<MyInput placeholder="请输入你的姓名" type="text" title="姓名" v-model="name" />
</div>
</template>
<script>
import MyInput from './MyInput';
export default {
name: 'parent',
data() {
return {
name: ''
}
},
components: {
MyInput
}
}
</script>
// 子组件
<template>
<div>
<label>姓名:</label>
<input v-bind="$attrsAll" @input="$emit( 'input', $event.target.value )" />
</div>
</template>
<script>
export default {
name: 'myinput',
data() {
return {}
},
inheritAttrs: false,
computed: {
$attrsAll() {
return {
value: this.$vnode.data.model.value,
...this.$attrs
}
}
}
}
</script>
$listener
$listeners
- 包含了父作用域中的 (不含 .native
修饰器的) v-on
事件监听器。它可以通过 v-on="$listeners"
传入内部组件 - 在创建更高层次的组件时非常有用。 简单点讲它是一个对象,里面包含了作用在这个组件上所有的监听器(监听事件),可以通过 v-on="$listeners"
将事件监听指向这个组件内的子元素(包括内部的子组件)。
同上面 $attrs
属性一样,这个属性也是为了在自定义组件中使用原生事件而产生的。比如要让前面的 MyInput
组件实现 focus
事件,直接这么写是没用的。
<template>
<div id="parentListener">
<MyInput @focus="focus" placeholder="请输入你的姓名" type="text" title="姓名" v-model="name" />
</div>
</template>
<script>
import MyInput from './MyInput';
export default {
name: 'parent',
data() {
return {
name: ''
}
},
methods: {
focus() {
console.log( 'test' );
}
},
components: {
MyInput
}
}
</script>
必须要让 focus
事件作用于 MyInput
组件的 input
元素上。
<template>
<div>
<label>姓名:</label>
<input v-bind="$attrsAll" v-on="$listenserAll" />
<button @click="handlerF">操作test</button>
</div>
</template>
<script>
export default {
name: 'myinput',
data() {
return {}
},
inheritAttrs: false,
props: ['value'],
methods: {
handlerF() {
this.$emit( 'focus' );
}
},
computed:{
$attrsAll() {
return {
value: this.value,
...this.$attrs
}
},
$listenserAll() {
return Object.assign(
{},
this.$listeners,
{input: event => this.$emit( 'input', event.target.value )})
}
}
}
</script>
$attrs
里存放的是父组件中绑定的非 props
属性,$listeners
里面存放的是父组件中绑定的非原生事件。
组件可以通过在自己的子组件上使用 v-on=”$listeners”
,进一步把值传给自己的子组件。如果子组件已经绑定 $listener
中同名的监听器,则两个监听器函数会以冒泡的方式先后执行。
总结:用在父组件传递数据给子组件或者孙组件。
四、provide
和 inject
Vue2.2
版本以后新增了这两个 API, 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
使用方法:provide
在父组件中返回要传给下级的数据;inject
在需要使用这个数据的子辈组件或者孙辈等下级组件中注入数据。
使用场景:由于 vue
有 $parent
属性可以让子组件访问父组件。但孙组件想要访问祖先组件就比较困难。通过 provide/inject
可以轻松实现跨级访问父组件的数据。
注意:provide
和 inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
// 父组件
<template>
<div class="parentProvide">
<button @click="changeSth">我要干嘛好呢~</button>
<p>要干嘛:{{ sth }}</p>
<ChildA />
</div>
</template>
<script>
import ChildA from './ChildA';
export default {
name: 'parent-pro',
data() {
return {
sth: '吃饭~'
}
},
// 在父组件传入变量
provide() {
return {
obj: this
}
},
methods: {
changeSth() {
this.sth = '睡觉~';
}
},
components: {
ChildA
}
}
</script>
// 子组件A
<template>
<div>
<div class="childA">
<p>子组件A该干嘛呢:{{ this.obj.sth }}</p>
</div>
<ChildB />
</div>
</template>
<script>
import ChildB from "./ChildB";
export default {
name: "child-a",
data() {
return {};
},
props: {},
// 在子组件拿到变量
inject: {
obj: {
default: () => {
return {}
}
}
},
components: {
ChildB
}
}
</script>
// 子组件B
<template>
<div>
<div class="childB">
<p>子组件B该干嘛呢:{{ this.obj.sth }}</p>
</div>
</div>
</template>
<script>
export default {
name: "child-b",
data() {
return {};
},
props: {},
// 在子组件拿到变量
inject: {
obj: {
default: () => {
return {}
}
}
}
}
</script>
总结:传输数据父级一次注入,子孙组件一起共享的方式。
五、$parent
和 $children & $refs
$parent
和 $children
:指定已创建的实例之父实例,在两者之间建立父子关系。子实例可以用 this.$parent
访问父实例,子实例被推入父实例的 $children
数组中。
$refs
:一个对象,持有注册过 ref
特性的所有 DOM 元素和组件实例。ref
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件。
// 父组件
<template>
<div class="parentPC">
<p>我的名字:{{ name }}</p>
<p>我的标题:{{ title }}</p>
<ChildA ref="comp1" />
<ChildB ref="comp2" />
</div>
</template>
<script>
import ChildA from "./ChildA.vue";
import ChildB from "./ChildB.vue";
export default {
name: 'parent-pc',
data() {
return {
name: '',
title: '',
contentToA: 'parent-pc-to-A',
contentToB: 'parent-pc-to-B'
}
},
mounted() {
const comp1 = this.$refs.comp1;
this.title = comp1.title;
comp1.sayHi();
this.name = this.$children[1].title;
},
components: {
ChildA,
ChildB
}
}
</script>
// 子组件A - ref方式
<template>
<div>
<p>(ChildA)我的父组件是谁:{{ content }}</p>
</div>
</template>
<script>
export default {
name: 'child-a',
data() {
return {
title: '我是子组件child-a',
content: ''
}
},
methods: {
sayHi() {
console.log( 'Hi, girl~' );
}
},
mounted() {
this.content = this.$parent.contentToA;
}
}
</script>
// 子组件B - children方式
<template>
<div>
<p>(ChildB)我的父组件是谁:{{ content }}</p>
</div>
</template>
<script>
export default {
name: 'child-b',
data() {
return {
title: '我是子组件child-b',
content: ''
}
},
mounted() {
this.content = this.$parent.contentToB;
}
}
</script>
从上面例子可以看到这两种方式都可以父子间通信,而缺点就是都不能跨级以及兄弟间通信。
总结:父子组件间共享数据以及方法的便捷实践之一。
六、Vuex
Vuex
是一个专为 Vue.js
应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex
实现了一个单项数据流,通过创建一个全局的 State
数据,组件想要修改 State
数据只能通过 Mutation
来进行,例如页面上的操作想要修改 State
数据时,需要通过 Dispatch
(触发 Action
),而 Action
也不能直接操作数据,还需要通过 Mutation
来修改 State
中数据,最后根据 State
中数据的变化,来渲染页面。
1、State (index.js)
State
用来存状态。在根实例中注册了 store
后,用 this.$store.state
来访问。
Vue.use(Vuex);
const state = {
userInfo: {}, // 用户信息
};
export default new Vuex.Store({
state,
getters,
mutations,
actions
});
2、Getters
Getters
从 State
上派生出来的状态。可以理解为基于 State
的计算属性。很多时候,不需要 Getters
,直接用 State
即可。
export default {
/**
@description 获取用户信息
*/
getUserInfo( states ) {
return states.userInfo;
}
}
3、Mutation
更改 Vuex
的 store
中的状态的唯一方法是提交 Mutation
。
Mutation
用来改变状态。需要注意的是,Mutation
里的修改状态的操作必须是同步的。在根实例中注册了 store
后, 可以用 this.$store.commit('xxx', data)
来通知 Mutation
来改状态。
export const UPDATE_USERINFO = "UPDATE_USERINFO";
export default {
[type.UPDATE_USERINFO]( states, obj ) {
states.userInfo = obj;
}
}
4、Action
-
Action
提交的是Mutation
,而不是直接变更状态。 -
Action
可以包含任意异步操作。
在根实例中注册了 store
后, 可以用 this.$store.dispatch('xxx', data)
来存触发 Action
。
export default {
update_userinfo({
commit
}, param) {
commit( "UPDATE_USERINFO", param );
}
}
乍一眼看上去感觉多此一举,我们直接分发 Mutation
岂不更方便?实际上并非如此,还记得 Mutation
必须同步执行这个限制么?Action
就不受约束!我们可以在 Action
内部执行异步操作:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
总结:对 Vue
应用中多个组件的共享状态进行集中式的管理(读/写),统一的维护了一份共同的 State 数据,方便组件间共同调用。
七、slot-scope
和 v-slot
从 vue@2.6.x
开始,Vue
为具名和范围插槽引入了一个全新的语法,v-slot
指令。
一个假设的 <base-layout>
组件的模板如下:
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<script>
export default {
name: "base-layout",
data() {
return {}
}
}
</script>
在向具名插槽提供内容的时候,我们可以在一个父组件的 <template>
元素上使用 v-slot
特性:
// 父组件
<template>
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
</template>
<script>
import BaseLayout from "./BaseLayout";
export default {
name: "parent-slot",
data() {
return {
}
},
components: {
BaseLayout
}
}
</script>
插槽的名字现在通过 v-slot:slotName
这种形式来使用,没有名字的 <slot>
隐含有一个 "default"
名称:
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
八、scopedSlots
属性
scopedSlots
是编程式语法,在 render()
函数中使用 scopedSlots
。
// baseLayout.vue
<script>
export default {
data() {
return {
headerText: "child header text",
defaultText: "child default text",
footerText: "child footer text"
}
},
render( h ) {
return h("div", { class: "child-node" }, [
this.$scopedSlots.header({ text: this.headerText }),
this.$scopedSlots.default(this.defaultText),
this.$scopedSlots.footer({ text: this.footerText })
]);
}
}
</script>
<script>
import BaseLayout from "./baseLayout";
export default {
name: "ScopedSlots",
components: {
BaseLayout
},
render(h) {
return h("div", { class: "parent-node" }, [
this.$slots.default,
h("base-layout", {
scopedSlots: {
header: props => {
return h("p", { style: { color: "red" } }, [
props.text
]);
},
default: props => {
return h("p", { style: { color: "deeppink" } }, [
props
]);
},
footer: props => {
return h("p", { style: { color: "orange" } }, [
props.text
]);
}
}
})
]);
}
}
</script>
总结一下
组件间不同的使用场景可以分为 3 类,对应的通信方式如下:
父子通信:props
和 $emit
,$emit
和 $on
,Vuex
,$attrs
和 $listeners
,provide
和 inject
,$parent
和 $children
&$refs
兄弟通信:$emit
和 $on
,Vuex
隔代(跨级)通信:$emit
和 $on
,Vuex
,provide
和 inject
,$attrs
和 $listeners
网友评论