前言
組件是 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