# 組件之間的溝通傳遞
提醒
注意 prop 在初始化時的順序會優於 data 、 computed 等屬性,所以像 props 中的 default 、 validator 是無法取得實體內的這些狀態。
# props
主要的核心概念:
多數用於 一次性 的設定值,沒有一定要雙向綁定的需求
這些組件都已經是末端組件,換句話說,子組件下不太會有其他子組件存在
會搭配 Slot 來做到組件變化,而不是再寫一個子組件
當你必須要使用其他子組件時,請改用事件傳遞所需要的資料
props所承接的資料,絕大多數不會過於複雜以分頁為範例,應該將各分頁的資料分開來寫,不要全部傳入子組件。
子組件
// bad - 傳入整個物件 export default { name: 'Pagination', props: { pager: { type: Object, required: true } } }; // good - 屬性分開傳入 export default { name: 'Pagination', props: { totlaItems: { type: Number, required: true }, limit: { type: Number, required: true }, first: { type: Number, required: true }, current: { type: Number, required: true } } };1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32父組件
<!-- bad --> <pagination :pager="pager"></pagination> <!-- good --> <ul> <li v-for="page in pager"> <pagination v-bind="page"></pagination> </li> </ul>1
2
3
4
5
6
7
8
9
# 傳入 props 時沒有加上 v-bind
一律以「純文字」的形式在子組件被接收,而不是外層組件的狀態。
實務上,除了忘記加上 v-bind 指令的情況外,通常會使用在希望後端直接輸出網頁內容時,
預先將傳入子組件的內容印在 HTML 的標籤上,這樣可以節省掉一次 request。
# 以物件作為 props 傳遞
由於 JavaScript 的物件是以「參考」的方式來傳遞的 (pass by reference) ,所以若是要由外層組件傳遞物件至內層子組件時,則需要特別小心。
<edit-component
v-for="comic in comics"
:key="comic.id"
:comic="comic"
></edit-component>
2
3
4
5
- 鬼滅之刃
- 2020/07/09
- 我的英雄學院
- 2020/07/10
乍看之下感覺沒什麼問題,但是此時若我們嘗試在子組件對 input 進行修改,
就會發現外層的資料也被變動了!
這時就可能由於某個子組件的修改,卻造成另一個子組件的 props 狀態污染,產生難以追蹤且不可預期的錯誤了。
所以,想要傳遞物件類型的 props 屬性時,先將物件屬性解構後再傳遞出去:
<edit-component
v-for="comic in comics"
:key="comic.id"
:id="comic.id"
:name="comic.name"
:time="comic.time"
></edit-component>
2
3
4
5
6
7
如果覺得把所有的屬性打散,可以透過 v-bind 指令,達到自動將物件解構效果:
<edit-component
v-for="comic in comics"
:key="comic.id"
v-bind="comic"
></edit-component>
2
3
4
5
- 鬼滅之刃
- 2020/07/09
- 我的英雄學院
- 2020/07/10
# 父組件存取子組件的 DOM 元素
如需要在子組件取得父層組件的內容,可以透過 this.$parent 來存取他的父層組件,反過來則是可以在外層透過 this.$children 來取得子組件的內容。
另外,由於 this.$children 是以「陣列」的形式產生,
而這個陣列有可能因為 v-if 或其他操作造成 this.$children 索引值的更動,
此時我們可以在子組件上加上 ref 屬性作為別名:
<div class="parent">
<child-component ref="child"></child-component>
</div>
2
3
這樣就可以在父層透過 this.$refs.child 就可以來存取指定的子組件了。
# 不是 Props 的 DOM 屬性
如果在組件上設定屬性並沒有在組件中的 props 被定義,這個屬性會直接套用在組件的 根元素 上。
範例如下方。
# 合併父組件及子組件屬性
class 及 style 在合併父組件及子組件時會將父子組件所有的屬性合併。
子組件
加上 class="thick":
Vue.component('kebab-case-converter', {
...
template: '<div class="thick">kabeb-case : {{kababCase}}</div>'
})
2
3
4
父組件
加上 class="italic":
<kebab-case-converter
class="italic"
data-test="test data"
:camel-case="camelCase"
></kebab-case-converter>
2
3
4
5
結果
父組件的 class 屬性會跟子組件的 class 做合併。
<div class="thick italic" data-test="test data">kabeb-case : hello</div>
# 由父組件替代子組件的屬性
除了 class 及 style 之外的其他屬性都會是由父組件蓋掉子組件屬性值
子組件
<div class="thick" data-test="child">kabeb-case : {{kababCase}}</div>
父組件
<kebab-case-converter
class="italic"
data-test="parent"
:camel-case="camelCase"
></kebab-case-converter>
2
3
4
5
結果
<div data-test="parent" class="thick italic">kabeb-case :</div>
# 避免替代屬性
父組件的屬性蓋掉子組件的值有時會產生非預期的結果,為了避免這樣的問題, Vue 提供了 inheritAttrs 這個參數,它可以將組件設為不要帶入父組件的屬性值(只有那些沒有設定於子組件 porps 中的屬性值不會被帶入)。
inheritAttrs 常常搭配 $attrs 這個物件設定, $attrs 是 父組件的屬性集合(沒有包含 props 中的屬性值),有時我們不想將屬性值設於根元素中,可以使用 inheritAttrs 取消綁定屬性到根元素的行為,並且使用 $attrs 將屬性值綁定要我們期望的元素上。
注意
class 跟 style 不會受 inheritAttrs 效果影響,也不會包在 $attrs 物件上。
子組件
Vue.component('base-checkbox', {
inheritAttrs: false,
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<div>
<input
type="checkbox"
v-bind="$attrs"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
</div>
`
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
父組件
<base-checkbox
v-model="lovingVue"
class="thick"
data-test="parent"
></base-checkbox>
2
3
4
5
結果
<div class="thick">
<input type="checkbox" data-test="parent" />
</div>
2
3
class因inheritAttrs及$attrs對class無效,所以還是在 根元素 上。data-test因inheritAttrs的影響,所以 不會 出現在根元素上,但因$attrs綁定,所以會在input上。
# 客製組件的 v-model
v-model 是 v-bind:value 及 v-on:input 的語法糖,它的作用是做 雙向綁定,但有些像是 radio 及 checkbox 需要 value 去綁定各別選項的值,而真正勾選的值是由像是 checked 之類的屬性綁定,為了避免 v-model 產生衝突,可以將 model 改為 v-bind:checked 及 v-on:change 來讓 v-model 運作正常。
checkbox 的 v-model:
Vue.component('base-checkbox', {
model: {
// 預設為 value
prop: 'checked',
// 預設為 input
event: 'change'
},
// 跟 value 一樣, v-model 的 prop : checked 要設定在 props 中
props: ['checked', 'label'],
template: `
<label>
<input
type="checkbox"
:checked="checked"
@change="$emit('change', $event.target.checked)"
>
{{label}}
</label>
`
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# model 物件
這個物件有兩個屬性 prop 及 event :
prop: 目標屬性。event: 監聽的事件。
# 綁定原生事件
替整個組件加上事件(click,focus),需使用 .native 修飾詞。
<base-input @focus.native="onFocus"></base-input>
但如果當 base-input 為:
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
/>
</label>
2
3
4
5
6
7
8
因為根元素是 <label> 而非 <input> ,所以 @focus.native 會綁定到 <label> 導致 focus 事件不會被觸發,為了解決這個問題, Vue 提供了 $listeners ,這個物件包含 所有 的父組件事件(排除有 native 修飾符的事件),如此一來我們就可以使用 $listeners 綁定想要的元素。
修改子組件
<label>
{{label}}
<input v-bind="$attrs" v-bind:value="value" v-on="$listeners" />
</label>
2
3
4
將父組件中的 focus 事件 拿掉 native 修飾符。
<base-input-with-label @focus="onFocus"></base-input-with-label>
# 切分 $listeners 的多個事件
子組件
Vue.component('base-input-with-label', {
props: ['value'],
computed: {
// 透過 DevTool 可以看到,只有 input 事件是可以觸發的
// 其它的因為未定義,而是 invoke
inputListeners() {
const vm = this;
// 使用 Object.assign 合併物件
return Object.assign(
{},
// 將 $listeners 的事件當作預設值
this.$listeners,
{
// 覆蓋 $listeners 中的 input 事件
input(event) {
// 這時候的 this 為原生 window 原生事件
vm.$emit('input', event.target.value);
}
}
);
},
focusListeners() {
const vm = this;
return Object.assign({}, this.$listeners, {
// 覆蓋 $listeners 中的 focus 事件
focus(event) {
console.log('this is from child focus event');
}
});
}
},
template: `
<label>
{{value}}
<!--只會觸發 focus 事件-->
<input v-on="focusListeners">
</label>
`
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
父組件
<base-input-with-label @focus="onFocus" v-model="label"></base-input-with-label>