# 2-2 元件之間的溝通傳遞
前面一個小節我們快速介紹了 Vue.js 元件系統的特性,以及元件內部的基本結構。 那麼在這個小節中,我們繼續對元件與元件之間各種傳遞資料的方式來做說明。
# Props
前面我們提到,Vue.js 每個元件的實體狀態、模板等作用範圍都應該要是獨立的, 這意味著我們不能(也不應該)在子元件的模組「直接」去修改父元件,甚至是另一個元件的資料,
這樣除了元件因為耦合程度過高維護不易,也可能產生難以追蹤的錯誤。
但是當我們切分元件的時候,就是希望能夠重複利用這個元件,我們希望這個元件可以根據「外部」傳入的資料來反映出不同的結果。 那麼,既然不能直接取用,那麼上下層元件之間,若需要從外部引進資料時,就需要透過 props
屬性來引用外部的狀態。
使用方式很簡單,我們只要在自訂的子元件上使用上一章介紹過的 v-bind
指令:
<div id="app">
<!-- 這是外層元件的 msg -->
<h3>{{ msg }}</h3>
<!-- 這裡的 v-bind:parent-msg 可以簡寫為 :parent-msg -->
<my-component v-bind:parent-msg="msg"></my-component>
</div>
2
3
4
5
6
7
const app = Vue.createApp({
data () {
return {
msg: '這是外層元件的 msg'
}
}
});
app.component('my-component', {
template: `
<div class="component">
<div> 從 props 來的 parentMsg ==> {{ parentMsg }} </div>
<div> 自己的 msg ==> {{ msg }} </div>
</div>`,
props: ["parentMsg"],
data () {
return {
msg: '這是子元件的 msg'
}
}
});
app.mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
像這樣,我們可以在內層元件內透過 props
屬性宣告要從「外部」引用進來的屬性名稱,
並且在外層模板使用內層元件標籤時,以 v-bind
指令來將資料傳遞進來。
另外,這裡要特別注意的是, props
與子元件命名的情況一樣,若我們是以 HTML 作為模板的時候,因為 HTML 不分大小寫的關係,像 parentMsg
這樣的駝峰式寫法,在模板裡要轉換成連字號 (kebab-case) parent-msg
來使用。
在內層元件 (或稱子元件) 宣告 props
屬性,最簡單的方式就是透過「陣列」的型態,
app.component('my-component', {
props: ['props1', 'props2', 'props3', ...],
// 下略...
});
2
3
4
這樣我們就可以透過 HTML 標籤內的屬性將外層的狀態引入至對應的 props
:
<my-component
:props1="..."
:props2="..."
:props3="..."></my-component>
2
3
4
小提醒: 傳入 props 時一定要加 v-bind (`:`) 嗎?
先說結論,答案是不一定,或者更準確一點來說,看情況決定加或不加。
在前面的範例中,我們在外層模板要將資料傳進子元件時,都會透過 v-bind:XXX="..."
或 :XXX="..."
的方式來進行資料的傳遞,但如果我們在傳遞資料的時候,忘了加上 v-bind:
指令時,內層元件仍然會收到資料。
稍微修改一下前面的範例,像這樣:
<!-- 注意這裡沒有 v-bind 或 : -->
<my-component parent-msg="msg"></my-component>
2
此時,子元件接收到的會是 "msg"
的「純文字字串」,而不是來自外層元件的 msg
狀態內容。
實務上,除了忘記加上 v-bind
指令的情況外,通常使用在希望由後端直接渲染輸出網頁內容的時候,預先將傳入子元件的內容印在 HTML 的標籤上,這樣可以節省掉一次 request (意思是無需呼叫 API 取得內容)。
也就是後端不想出 API 的時候會用到的實用技巧
但要注意的是,像這樣沒有使用 v-bind
傳入的 props
,會一律以「純文字」的形式在子元件被接收,即便你所傳遞進來的內容是數字的資料。
# props
資料類型的驗證
如果說元件與網站的應用是由不同團隊所開發的時候 (如第三方套件),針對從外部傳入的 props
型別檢查與驗證就是很實用的功能。
Vue.js 內建能夠檢查的 type
屬性有下面幾種類型:
String
Number
Boolean
Array
Object
Date
Function
Symbol
# 替 props
指定資料格式
如何指定 props 的資料格式來做驗證呢? 使用方式很簡單,我們稍微改寫一下 props
屬性:
props: {
'props-number': {
// 注意:這裡的 Number 無需用引號包成字串,而且首字要大寫
type: Number
}
}
2
3
4
5
6
像這樣,我們就可以指定傳入的 props-number
為一個 Number
的格式。
<!-- 正確,有使用 v-bind, Vue.js 會將其轉為數字 -->
<my-component :props-number="123"></my-component>
<!-- 錯誤,傳入的會是 "123" 的字串 -->
<my-component props-number="123"></my-component>
2
3
4
5
若是我們嘗試傳遞一個 "123"
的字串給 props-number
,則會在 console 主控台看到這樣的錯誤:
[Vue warn]: Invalid prop: type check failed for prop "propsNumber". Expected Number with value 123, got String with value "123".
。
這段警告的意思是 propsNumber
這個 prop 狀態, Vue.js 預期它應該是個 Number 型別的資料,但傳入的卻是字串。
當然,若是我們希望允許多種不同格式的 prop
,則可以透過陣列的形式來指定:
props: {
// 同時允許 String 與 Number 型別的資料傳入
something: {
type: [String, Number]
}
}
2
3
4
5
6
如果希望指定這個 props
為必要的屬性,則可以加上 required
屬性,並指定為 true
:
props: {
something: {
required: true
}
}
2
3
4
5
像這樣,如果沒有傳入指定的 props
則會在 console 主控台看到 Missing required prop: "something"
的錯誤。
# 替 props
指定預設值
當然要為某個 props
指定預設值也是沒問題,只要加上 default
屬性即可:
props: {
something: {
type: [String, Number],
default: 'Hello'
}
}
2
3
4
5
6
這樣即使沒有傳入 something
這個 props
,在子元件的實體中,也會自動給定 'Hello'
的字串做為預設值。
另外,像是陣列、物件的預設內容也是可以的:
something: {
type: Array,
default: [1, 2, 3]
}
2
3
4
something: {
type: Object,
default: {
msg: 'Hello Vue 3.0!'
}
}
2
3
4
5
6
透過 default
來指定預設內容,可以避免許多因 props
忘記傳遞帶來的問題。
# 自訂 props
驗證規則
如果 Vue.js 內建的幾種型別檢查還沒辦法滿足你的話,沒關係,我們可以加上 validator
屬性來自定驗證規則:
props: {
something: {
type: Number,
// 注意,在 validator function 內不可存取 data / computed 屬性!
// 驗證傳入的 something 是否大於 0
validator: value => value > 0
}
}
2
3
4
5
6
7
8
像這樣,我們在 something
這個 props
加上了 validator
檢查,
當傳入的數值大於 0
的時候表示正確,否則 console 主控台將會出現錯誤訊息。
小提醒
注意 props
在元件初始化時的順序會更優先於 data
、 computed
等屬性,所以像是在 default
或 validator
是無法取得實體內的這些狀態 (意思是無法在裡面取得 this.xxx
的實體內容) 。
# 以物件作為 props
傳遞
由於 JavaScript 的物件是以「參考」的方式來傳遞的 (pass by reference) ,所以若是要由外層元件傳遞物件至內層子元件時,則需要特別小心。
假設外層元件的 data
有個叫 books
的陣列:
data () {
return {
books: [
{
id: 'a00001',
name: '0 陷阱!0 誤解!8 天重新認識 JavaScript!',
author: 'Kuro Hsu',
publishedAt: '2019/09'
},
{
id: 'a00002',
name: '重新認識 Vue.js',
author: 'Kuro Hsu',
publishedAt: '2021/02'
},
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
接著我們透過 v-for
將書籍資訊列出來:
<ul v-for="book in books" class="book">
<li>{{ book.name }}</li>
<li>{{ book.author }}</li>
<li>{{ book.publishedAt }}</li>
</ul>
2
3
4
5
然後加入一個子元件,同樣用 v-for
來渲染 books
,並將 book
當作 prop
傳入:
<!--
注意,若直接使用 HTML 作為模板的情況下,因 HTML 不分大小寫的特性,
v-bind 的屬性不可使用駝峰式 (:bookInfo) ,需要使用連字號 (:book-info) 才能正確解析。
寫在 JavaScript 實體物件或是 .vue 的單一元件檔的 template 模板則不受此限制。
-->
<my-component v-for="book in books" :key="book.id" :book-info="book" />
2
3
4
5
6
7
傳給子元件的 bookInfo
prop 屬性,我們將其設定為物件的資料,並在模板中使用 v-model
指令:
app.component('my-component', {
props: {
bookInfo: {
type: Object
}
},
template: `
<div class="child-app">
<div>書名: <input type="text" v-model="bookInfo.name"></div>
<div>作者: <input type="text" v-model="bookInfo.author"></div>
<div>出版日: <input type="text" v-model="bookInfo.publishedAt"></div>
</div>`,
});
2
3
4
5
6
7
8
9
10
11
12
13
乍看之下感覺沒什麼問題,但是此時若我們嘗試在子元件對 input
進行修改,就會發現外層的資料也被變動了!
很遺憾地我必須要跟各位說,這肯定不是 feature,這是 bug ,而且是我們產生的 bug,絕對禁止!
這裡要與各位讀者強調一個觀念,在 Vue 的每個實體 (或者元件) 它們的狀態都應該要是彼此獨立的,
如果說今天子元件可以透過 props
自由地修改外層元件的狀態,那麼要是有「兩個以上」的元件同時引用同一個狀態作為 prpos
呢?
這時就可能由於某個子元件的修改,卻造成另一個子元件的 props
狀態污染,產生難以追蹤且不可預期的錯誤了。
所以,想要傳遞物件類型的 props
屬性時,應該先將物件屬性解構成原始型別 (Primitive) 後再將資料傳遞出去:
<my-component
v-for="book in books"
:name="book.name"
:author="book.author"
:published-at="book.published-at"></my-component>
2
3
4
5
app.component('my-component', {
template: `
<div class="child-app">
<div>書名: <input type="text" v-model="name"></div>
<div>作者: <input type="text" v-model="author"></div>
<div>出版日: <input type="text" v-model="publishedAt"></div>
</div>`,
props: ['name', 'author', 'published-at'],
});
2
3
4
5
6
7
8
9
像這樣,將傳入的 props
解構成純值的作法,更新時就不會改寫到外層的資料了。
如果覺得把所有屬性一個一個打散來寫太囉唆的話,也可以透過 v-bind
指令,改寫成 v-bind="book"
,這樣在傳入 props
至 my-component
元件時,會自動將 book
物件解構。
<!-- v-bind="book" 會將物件自動解構 -->
<my-component
v-for="book in books"
v-bind="book"
></my-component>
<!-- 分開寫的結果跟上面的寫法一模一樣 -->
<my-component
v-for="book in books"
:name="book.name"
:author="book.author"
:published-at="book.published-at"
></my-component>
2
3
4
5
6
7
8
9
10
11
12
13
# 非 prop 的屬性傳遞
前面有說到,我們可以透過網頁模版標籤上的屬性與 props
來做到子元件的資料傳遞,那麼假如我們在子元件忘了加上 props
,又會發生什麼事呢?
<div id="app">
<!-- 這裏透過 v-bind 傳入 className -->
<my-component :class="className"></my-component>
</div>
2
3
4
const app = Vue.createApp({
data() {
return {
className: 'block'
}
}
});
// 注意,子元件並未含有 props 屬性
app.component('my-component', {
template: `<div class="child-app"></div>`,
});
app.mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
有趣的是,這個時候不但不會出現錯誤,而且子元件 <my-component>
的 HTML 渲染結果會是:
<!-- 直接將外層 class 內容交給實際的 DOM 身上 -->
<div class="child-app block"></div>
2
而除了 props
之外,事件也有一樣的特性:
<div id="app">
<!-- 透過 v-on 訂閱 DOM 原生 click 事件 -->
<my-component :class="className" @click="greeting"></my-component>
</div>
2
3
4
5
const app = Vue.createApp({
data() {
return {
className: 'block'
}
},
methods: {
greeting() {
alert('Hello Vue!');
}
}
});
// 注意,子元件身上並未有 $emit 觸發事件的行為
app.component('my-component', {
template: `<div class="child-app"></div>`,
});
app.mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
小提醒
屬性的傳遞與繼承只有在子元件是「唯一根節點」時有效,若子元件擁有多個根節點時,Vue.js 不知道該將屬性交給哪一個 DOM 節點,就會出現警告訊息。
// 多個根節點,若嘗試指定非 prop 的屬性傳遞,會出現警告訊息
app.component('custom-layout', {
template: `
<header>...</header>
<main>...</main>
<footer>...</footer>
`
});
2
3
4
5
6
7
8
但如果我們在某個指定的標籤上加入了 v-bind="$attrs"
後,便可正常執行。
// 加入 v-bind="$attrs" 至指定的節點 (不一定要是根節點) 後可正常執行
app.component('custom-layout', {
template: `
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
`
});
2
3
4
5
6
7
8
另外,若不希望屬性被子元件所繼承,則可在子元件加入 inheritAttrs: false
app.component('custom-layout', {
inheritAttrs: false,
// 略
template: `...`
});
2
3
4
5
這樣非 Props 的屬性就不會被傳入子元件了。
# 雙向綁定? 單向資料流?
到這裏,我們已經可以將父元件的 data
與傳遞給子元件的 props
順利解耦了。
但是,這時如果我們試著修改子元件的任何一個透過 v-model
綁定 props
資料的輸入框內容時,你應該會發現 console 主控台跳出警告:
[Vue warn]: Attempting to mutate prop "XXXXX". Props are readonly.
(XXXXX 是你修改的欄位名稱)
的錯誤訊息,這是為什麼呢?
在解釋原因前,我們先談談 Vue.js 資料的「雙向綁定」 與 「單向資料流」。
在前一章介紹指令的部分,我們提到了 v-model
會針對 Vue 實體內的狀態 (data
) 與畫面上表單元素 (如 input
等) 進行綁定,
當表單元素的 value
被更新的時候,Vue.js 就會直接反映至實體對應的狀態。 這樣的作用,我們通常稱它叫資料的「雙向綁定」 (Two-way Data Binding)。
然而你在 Vue.js 的官方文件或是某些文章當中,可能會看到 Vue.js 其實是採用「單向資料流」 (One-way Data Flow) 的方式來管理狀態的。
那麼 Vue.js 究竟是雙向綁定或是單向資料流呢? 其實兩者都是對的,要看你用什麼角度解釋它。
假設我們從狀態 (Data) 到畫面 (View) 的角度來看,那麼 Vue.js 確實能做到 UI 的雙向綁定。
但若是以「元件對元件」的狀態管理來看,每一個元件都應該有屬於自己的狀態,自己的狀態自己改,
所以當我們嘗試將 props
傳入的屬性透過 v-model
來更新狀態時, Vue.js 就會跳出錯誤訊息提醒。
所以,如果我們希望能排除錯誤,則可以將 props
傳入的狀態,在元件實體內使用 data
來承接:
app.component('my-component', {
props: ['name', 'author', 'publishedAt'],
data () {
return {
bookName: this.name,
bookAuthor: this.author,
bookPublishedAt: this.publishedAt,
}
},
// v-model 綁定的是 data 回傳的資料,而不是 props
template: `
<div class="child-app">
<div>書名: <input type="text" v-model="bookName"></div>
<div>作者: <input type="text" v-model="bookAuthor"></div>
<div>出版日: <input type="text" v-model="bookPublishedAt"></div>
</div>`,
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
這樣就可以將 props
傳入的狀態複製一份由子元件來管理了。
# Props
與遞迴元件
前面說過,在 Vue.js 的元件系統當中,元件裡頭可以再包覆另一個元件作為子元件。 但是你知道嗎,元件也可以將「自己」當成「子元件」,而這類元件通常我們稱它叫「遞迴元件」 (Recursive Component)。
而使用「遞迴元件」的限制只有一個,就是它必須要有 name
屬性。
讓我們以實務上很常見的樹狀選單為例,假設我們今天有個像這樣的階層式選單資訊:
const menuData = {
name: '好書推薦',
childNodes: [{
name: 'Git',
childNodes: [{
name: '為你自己學 Git',
url: 'https://www.tenlong.com.tw/products/9789864342662'
}]
},
{
name: '前端開發',
childNodes: [{
name: '金魚都能懂的 CSS 選取器',
url: 'https://www.tenlong.com.tw/products/9789864344994'
},
{
name: '0 陷阱!0 誤解!8 天重新認識 JavaScript!',
url: 'https://www.tenlong.com.tw/products/9789864344130'
},
{
name: '讓 TypeScript 成為你全端開發的 ACE!',
url: 'https://www.tenlong.com.tw/products/9789864344895'
},
]
},
{
name: 'IoT',
childNodes: [{
name: 'IoT沒那麼難!新手用 JavaScript 入門做自己的玩具!',
url: 'https://www.tenlong.com.tw/products/9789864345328'
}]
},
{
name: 'Chatbot',
childNodes: [{
name: '人人可作卡米狗:從零打造自己的 LINE 聊天機器人',
url: 'https://www.tenlong.com.tw/products/9789864342938'
}]
}
]
};
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
40
41
我們需要依照資料的階層來渲染樹狀選單,這時可以怎麼做呢? 很簡單,只需要一個子元件即可完成:
<div id="app">
<!-- Magic! -->
<menu-component
:title="menuData.name"
:child="menuData.childNodes"></menu-component>
</div>
2
3
4
5
6
7
雖然看起來很神奇,但這並不是施展了什麼魔法,而是我們將 <menu-component>
當作子元件來利用:
<!-- <menu-component> 的模板結構 -->
<ul>
<li>
<template v-if="child.length > 0">
<h2 class="has-child"
:class="{ 'is-open': isOpen }"
@click="isOpen = !isOpen">{{ title }}</h2>
<!-- 把自己當成子元件利用,並把下層資料透過 Props 傳遞進去 -->
<menu-component
v-show="isOpen"
v-for="c in child"
:key="c.name"
:title="c.name"
:child="c.childNodes"
:url="c.url"></menu-component>
</template>
<!-- 下層已經沒有 childNodes 了,表示是最後一層,直接渲染連結 -->
<a v-else :href="url" target="_blank">{{ title }}</a>
</li>
</ul>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 注意,「遞迴元件」必須要有 `name` 屬性,這樣在 template 內才會認得
app.component('menu-component', {
name: `menu-component`,
props: {
title: String,
url: String,
child: {
type: Array,
default: []
}
},
data() {
return {
isOpen: false
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
像這樣,我們就可以透過列表元件 <menu-component>
進行包裝,樹狀選單就可以利用階層式物件搭配 Props
進行渲染了。
# 元件與自訂事件
然而從父元件傳遞 props
給子元件之後,有時可能會需要將處理過的狀態送回給外層的父元件,
但我們又不能直接修改外層的父元件的狀態,這時該怎麼處理呢?
在 Vue.js 裡面,父子元件之間的溝通方式有個流傳已久的口訣:「Props in, Event out」。
父層資料透過 props
傳入子層,而子層透過 event
來觸發父層狀態的更新。
props
傳入的部分我們前面看過了,現在我們來看看事件的部分。
在元件內部處理事件與 DOM 監聽事件一樣,我們可以透過 v-on
指令來處理:
<!-- 直接將 v-for 的 book 物件作為 props 傳遞 -->
<!-- 並監聽自訂的 update 事件 -->
<my-component
v-for="(book, idx) in books"
:key="idx"
v-bind="book"
@update="updateInfo"
></my-component>
2
3
4
5
6
7
8
然後外層元件加上對應的 methods
作為事件處理器:
const app = Vue.createApp({
data() {
return {
books: [{
id: '0001',
name: '0 陷阱!0 誤解!8 天重新認識 JavaScript!',
author: 'Kuro Hsu',
publishedAt: '2019/09'
},
{
id: '0002',
name: '重新認識 Vue.js',
author: 'Kuro Hsu',
publishedAt: '2021/02'
},
]
}
},
methods: {
updateInfo(val) {
// 註:如果是 Vue 2.x 要透過 this.$set 來更新
// 如: this.$set(this.books, idx, val);
// Vue 3.x 則無此限制
const idx = this.books.findIndex(d => d.id === val.id);
this.books[idx] = val;
}
}
});
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
那麼子元件的 <my-component>
則是將 props
接收的狀態,在 data
複製一份後回傳一個新物件 bookInfo
,並且加上 watch
屬性來偵測更新:
app.component('my-component', {
template: `
<div class="child-app">
<div>書名: <input type="text" v-model="bookInfo.name"></div>
<div>作者: <input type="text" v-model="bookInfo.author"></div>
<div>出版日: <input type="text" v-model="bookInfo.publishedAt"></div>
</div>`,
props: ['id', 'name', 'author', 'publishedAt'],
data() {
return {
bookInfo: {
id: this.id,
name: this.name,
author: this.author,
publishedAt: this.publishedAt
}
};
},
watch: {
bookInfo: {
// 注意! 由於 bookInfo 物件必須加上 deep: true
// 才能做物件的深層更新偵測
deep: true,
handler(val) {
this.$emit('update', val);
},
},
}
});
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
當資料被更新時,我們就可以透過 this.$emit('事件名', 參數)
的方式來觸發事件。
如 bookInfo
被更新時,透過 this.$emit
觸發在 @update="updateInfo"
訂閱的自訂 update
事件,通知外層父元件的 updateInfo
來更新外層的狀態,而不是由子元件來直接更新。
像這樣,自己的資料自己改,才是 Vue.js 元件維護資料最安全的方式!
小提醒: 元件的父與子
雖然說不建議在父/子元件修改彼此的狀態,但實務上可能會因為需求的關係,
需要在子元件取得父層元件的內容,可以透過 this.$parent
來存取它的父層元件。
而父層元件則可以透過 this.$refs
來取得子元件,
在使用前子元件必須先加上 ref
屬性作為別名:
<my-component ref="child" />
<my-component ref="child2" />
2
這樣就可以在父層透過 this.$refs.child
或 this.$refs.child2
來存取對應的子元件了。
注意
Vue.js 自從 3.0 版本起,已經取消了 $on
、$off
,以及 $once
的用法,事件只能由 v-on
所指定,請讀者使用時多加留意小心。
# v-model 與元件的雙向綁定 (Vue 3.x 新增)
雖說 Vue.js 規定父子元件狀態的管理,是遵循 Props in, Event out 的方式來管理傳遞,
但從 Vue 3.0 起,它允許我們在自訂的子元件加上 v-model
指令來做到「雙向綁定」的效果。
快速複習一下,上一章介紹的 v-model
指令,我們通常會拿來使元件內 data
與網頁「表單元素」進行雙向綁定:
data () {
return {
msg: 'Hello Vue!'
}
}
2
3
4
5
<input v-model="msg">
這個時候, Vue 會將 msg
的值,也就是 "Hello Vue!"
放在 <input>
的 value
屬性中。
當使用這進行修改的時候,會將更新後的 <input>
內容回存至 data
的 msg
。
換句話說,上面的程式碼意義等同於:
<input :value="msg" @input="msg = $event.target.value" />
那麼,本小節介紹的 v-model
又是怎麼應用在子元件上呢?
<div id="app">
<h1>{{ message }}</h1>
<!-- 透過 v-model 來做到父子元件間的「雙向綁定」 -->
<custom-input v-model="message"></custom-input>
</div>
2
3
4
5
6
7
const app = Vue.createApp({
data () {
return {
message: "Hello Vue!"
};
}
});
// 子元件 <custom-input>
app.component("custom-input", {
props: ["modelValue"],
template: `<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)">`
});
app.mount("#app");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
看起來很神奇對吧!
不過這只是一種語法糖,像這樣將 v-model
直接應用在元件的做法:
<custom-input v-model="message"></custom-input>
<!-- 或者 -->
<custom-input v-model:message="message"></custom-input>
2
3
4
實際上是 Vue.js 會在背後將它解開成 v-bind
與 v-on
的組合:
<custom-input :message="message" @uptade:message="message = $event" /></custom-input>
依然是透過觸發事件來通知父層元件更新狀態。
另外,若我們要綁定兩個以上的 v-model
到元件上,可以像這樣把變數傳遞到元件裡:
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName" />
2
3
在元件內同樣透過 $emit('update:lastName', lastName)
的方式發送事件通知上層更新即可。
注意
像這種父子元件間「雙向綁定」的方式,在 Vue 2.x 是透過 .sync
修飾子來處理,此修飾子在 Vue 3.x 已不適用。
# 跨越層級的傳遞方式
前面我們介紹了父子層級的元件資料是由 props
與 event
來做溝通傳遞,那麼如果遇到了跨層級的狀態溝通該怎麼處理呢?
對於這類型的需求, Vue.js 提供的常見解決方案有這些:
# provide
與 inject
前面說過,父層的元件資料通常會透過 Props
來傳遞給子層元件,那麼假設我們有更深一層的資料要進行傳遞,例如根元件傳給最底部的元件:
又該怎麼處理呢? 這時候就要透過 Vue.js 提供的 provide
與 inject
機制了。
假設我們的元件結構如下:
app
├─── list-component
│ ├── list-item 1
│ ├── list-item 2
│ └── 下略...
│
├── XXX-component
└── 下略...
2
3
4
5
6
7
8
這時候,若我們希望從 app
傳遞資料給 <list-item>
時,用傳統的 props
一層一層傳遞,肯定是很麻煩的一件事,而且還會增加元件之間的耦合程度。
provide
與 inject
機制的使用方式非常簡單。
首先,我們在根元件也就是 app
的層級,把要傳遞出去的資料定義在 provide
中:
const app = Vue.createApp({
data () {
return {
msg: 'Hello App!'
}
},
provide () {
// 將 this.msg 透過 provide 傳遞出去
return {
provideMsg: this.msg
};
}
});
2
3
4
5
6
7
8
9
10
11
12
13
再來,在子或孫元件中 (總之不管隔幾層都可以) 需要取得頂層元件 provide
狀態的元件 (這裡以 list-item
為例) ,
加上 inject
屬性:
app.component('list-component', {
template: `
<ul>
<li v-for="i in 3">
{{ i }}
<list-item />
</li>
</ul>`,
components: {
'list-item': {
// 在子、孫元件中,加上 inject 屬性即可取得 provideMsg
inject: ['provideMsg'],
template: `<div>{{ provideMsg }}!</div>`
}
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
另外要注意的是,透過 provide
輸出的資料並不會隨著父層資料的更新而有所改變,
如果希望子層 inject
取回來的資料能與上層資料連動,則需要透過 Vue.computed()
進行包裝:
// 將 provide 透過 Vue.computed 包裝
provide() {
return {
provideMsg: this.msg,
provideMsg2: Vue.computed(() => this.msg)
};
}
2
3
4
5
6
7
包裝後的物件,在子層元件的 inject
使用時,需要加上 .value
方可正常運作:
'list-item': {
// 由於傳入的是透過 Vue.computed 包裝後的物件,所以要加上 .value
// 有關 .value 的用法在本書最後一章 Composition API 會有更詳細的說明
inject: ['provideMsg', 'provideMsg2'],
template: `
<div>provideMsg: {{ provideMsg }}!</div>
<div>provideMsg2: {{ provideMsg2.value }}!</div>
`
}
2
3
4
5
6
7
8
9
像這樣,若遇到跨層級的 Props
資料,就無需一層一層引入,可以直接透過指定的 provide
輸出,再由 inject
取回來了。
# EventBus (Vue 3.x 起已不建議使用)
假設我們遇到了跨元件的事件傳遞,
由於自 Vue 3.0 開始移除了 $on
, $off
的用法,
所以若想使用 EventBus 來當作元件間的橋樑,需要改用 mitt ( https://github.com/developit/mitt ) 來代替原本的 Vue 2.x 以前的 EventBus 物件實體。
Mitt 版 EventBus 的使用很簡單,如果是使用 npm 管理的朋友,透過 npm 安裝 mitt:
$ npm install --save mitt
或是透過 CDN 的方式將它引入至網頁裡:
<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>
首先,新增一個新的 mitt() 實體,並將其指定到 bus
變數:
const bus = mitt();
接著我們在外層的實體新增兩組 <button-counter>
元件,並在自訂事件 add-sum
觸發時執行 plus
這個方法。
<div id="app">
<h1>Total: {{ sum }}</h1>
<button-counter @add-sum="plus"></button-counter>
<button-counter @add-sum="plus"></button-counter>
<button-reset></button-reset>
</div>
2
3
4
5
6
7
同時,我們在外層元件的 created
階段,針對一開始宣告的 EventBus
綁定一個 reset
的自訂事件:
// 根元件
const app = Vue.createApp({
data () {
return {
sum: 0
}
},
methods: {
plus () {
this.sum++;
},
reset () {
this.sum = 0;
}
},
created () {
// 實體建立時,在 bus 身上註冊 reset 事件
// 觸發事件時呼叫 this.reset 方法
bus.on('reset', this.reset);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
另外, 在 <button-counter>
這個元件也同樣在 created
階段加上 reset
事件與對應方法 :
// 元件 <button-counter>
app.component('button-counter', {
template: `<button @click="plus">You clicked me {{ count }} times.</button>`,
data () {
return {
count: 0
};
},
methods: {
plus () {
// 自己的 count 加一
this.count++;
// 觸發在 v-on 註冊的 add-sum 事件
this.$emit('add-sum');
},
reset () {
this.count = 0;
},
},
created () {
// 訂閱 bus 的 reset 事件
// 觸發事件時呼叫 this.reset 方法歸零
bus.on('reset', this.reset);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
最後, <button-reset>
元件就單純多了,在點擊內部的 button
之後會執行 reset
方法,
裡面就只對 EventBus
觸發 reset
事件:
// 元件 <button-reset>
app.component('button-reset', {
template: `<button @click="reset">reset</button>`,
methods: {
reset () {
// 觸發 bus 的 reset 事件
bus.emit('reset');
},
},
});
2
3
4
5
6
7
8
9
10
像這樣,當 EventBus
的 reset
事件被觸發後,那些曾經向 EventBus
訂閱 reset
事件元件們就會執行對應的方法。
不過 EventBus 並非萬靈丹,由於我們將各種事件都往 EventBus 上註冊, 那些原本 Vue.js 會在元件銷毀時自動解除事件的動作就必須由開發者自行來處理, 甚至還要當心訂閱事件名稱重複所引發的各種問題,這些都是在使用 EventBus 需要特別注意的地方。
而且除了 EventBus 還有其他更好的做法,這個後續會在相關章節為讀者們解說。
# Vuex
前面說的 EventBus 到 Vue.js 3.x 已經不建議使用後,現今最主流的跨元件狀態維護就是透過 Vuex 來管理了
過去我們想要存放多個狀態的時候,常常一言不合就往全域物件丟,但是丟 window
一時爽,一直丟 window
...時間一長專案長大後,恐怕維護起來就不太爽了。
而 Vuex 的核心就是 store
,我們可以將 Vuex 的 store
想像成一個「受規範限制」的全域物件,
每個元件都可以向這個中央倉庫去存取狀態,但又必須要遵守 Vuex 的規定,不致於無法控管資料的流向。
像是 store
只能透過 Mutations
進行存取,而且只能執行「同步」的操作,而非同步的動作需要透過 Actions
來進行,這樣才能確保資料更新的動向得以追蹤。
當我們的應用程式成長到一定規模後, Vuex 就是一個很有效且安全的狀態共享管理機制。 關於 Vuex 的詳細內容,本書的後續還有一整個專門章節來為各位做說明。
# Vue Composition API
除了 Vuex 之外,Vue 3.0 起新增的 Vue Composition API 也可以用來處理跨元件的資料與程式邏輯共享。
這裏我們將上一個範例用 Composition API 來改寫,首先抽取出共用的邏輯與方法:
// 共用邏輯
const sum = ref(0);
const plus = () => sum.value++;
const reset = () => sum.value = 0;
2
3
4
再來,定義父子元件內容,新增的 setup
函式是用來建立與啟動我們的元件,並將模板 <template>
會用到的東西 return
出去:
// 父層、根元件
const app = createApp({
setup() {
// 將模板用到的 sum, plus 回傳出去
return {
sum,
plus
};
}
});
// <button-counter>
app.component('button-counter', {
template: `<button @click="plus">You clicked me {{ count }} times.</button>`,
setup(props, {emit}) {
const count = ref(0);
// 透過 emit 傳遞自定義事件
const plus = () => {
count.value++
emit('add-sum');
};
// 觀察 sum 的變化,若 sum 為 0 代表要 reset count 的內容
watch(sum, v => count.value = v === 0 ? 0 : count.value);
// 將模板用到的 count, plus 回傳出去
return {
count,
plus
}
}
});
// <button-reset>
app.component('button-reset', {
template: `<button @click="reset">reset</button>`,
setup() {
// 將模板用到的 reset 回傳出去
return {
reset
}
}
});
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
40
41
42
43
44
45
46
47
改寫後的完整範例如下:
像這樣,雖然實際執行的結果沒什麼不同,但由於我們將相同的狀態、邏輯都抽取出來,透過 Composition API 改寫後的程式,看起來會比過去的寫法變得更加簡潔。 有關 Composition API 的詳細內容,在本書的最後一章會有整個章節來為讀者詳細解說,這裡就先簡單劇透一下。