Appearance
2-2 元件之間的溝通傳遞
前面一個小節我們快速介紹了 Vue.js 元件系統的特性,以及元件內部的基本結構。 那麼在這個小節中,我們繼續對元件與元件之間各種傳遞資料的方式來做說明。
Props
前面我們提到,Vue.js 每個元件的實體狀態、模板等作用範圍都應該要是獨立的, 這意味著我們不能(也不應該)在子元件的模組「直接」去修改父元件,甚至是另一個元件的資料,
這樣除了元件因為耦合程度過高維護不易,也可能產生難以追蹤的錯誤。
但是當我們切分元件的時候,就是希望能夠重複利用這個元件,我們希望這個元件可以根據「外部」傳入的資料來反映出不同的結果。 那麼,既然不能直接取用,那麼上下層元件之間,若需要從外部引進資料時,就需要透過 props 屬性來引用外部的狀態。
使用方式很簡單,我們只要在自訂的子元件上使用上一章介紹過的 v-bind 指令:
html
<div id="app">
<!-- 這是外層元件的 msg -->
<h3>{{ msg }}</h3>
<!-- 這裡的 v-bind:parent-msg 可以簡寫為 :parent-msg -->
<my-component v-bind:parent-msg="msg"></my-component>
</div>
js
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');像這樣,我們可以在內層元件內透過 props 屬性宣告要從「外部」引用進來的屬性名稱, 並且在外層模板使用內層元件標籤時,以 v-bind 指令來將資料傳遞進來。
另外,這裡要特別注意的是, props 與子元件命名的情況一樣,若我們是以 HTML 作為模板的時候,因為 HTML 不分大小寫的關係,像 parentMsg 這樣的駝峰式寫法,在模板裡要轉換成連字號 (kebab-case) parent-msg 來使用。
在內層元件 (或稱子元件) 宣告 props 屬性,最簡單的方式就是透過「陣列」的型態,
js
app.component('my-component', {
props: ['props1', 'props2', 'props3', ...],
// 下略...
});這樣我們就可以透過 HTML 標籤內的屬性將外層的狀態引入至對應的 props :
html
<my-component
:props1="..."
:props2="..."
:props3="..."></my-component>小提醒: 傳入 props 時一定要加 v-bind (:) 嗎?
先說結論,答案是不一定,或者更準確一點來說,看情況決定加或不加。
在前面的範例中,我們在外層模板要將資料傳進子元件時,都會透過 v-bind:XXX="..." 或 :XXX="..." 的方式來進行資料的傳遞,但如果我們在傳遞資料的時候,忘了加上 v-bind: 指令時,內層元件仍然會收到資料。
稍微修改一下前面的範例,像這樣:
html
<!-- 注意這裡沒有 v-bind 或 : -->
<my-component parent-msg="msg"></my-component>此時,子元件接收到的會是 "msg" 的「純文字字串」,而不是來自外層元件的 msg 狀態內容。
實務上,除了忘記加上 v-bind 指令的情況外,通常使用在希望由後端直接渲染輸出網頁內容的時候,預先將傳入子元件的內容印在 HTML 的標籤上,這樣可以節省掉一次 request (意思是無需呼叫 API 取得內容)。
也就是後端不想出 API 的時候會用到的實用技巧
但要注意的是,像這樣沒有使用 v-bind 傳入的 props ,會一律以「純文字」的形式在子元件被接收,即便你所傳遞進來的內容是數字的資料。
props 資料類型的驗證
如果說元件與網站的應用是由不同團隊所開發的時候 (如第三方套件),針對從外部傳入的 props 型別檢查與驗證就是很實用的功能。
Vue.js 內建能夠檢查的 type 屬性有下面幾種類型:
StringNumberBooleanArrayObjectDateFunctionSymbol
替 props 指定資料格式
如何指定 props 的資料格式來做驗證呢? 使用方式很簡單,我們稍微改寫一下 props 屬性:
js
props: {
'props-number': {
// 注意:這裡的 Number 無需用引號包成字串,而且首字要大寫
type: Number
}
}像這樣,我們就可以指定傳入的 props-number 為一個 Number 的格式。
html
<!-- 正確,有使用 v-bind, Vue.js 會將其轉為數字 -->
<my-component :props-number="123"></my-component>
<!-- 錯誤,傳入的會是 "123" 的字串 -->
<my-component props-number="123"></my-component>若是我們嘗試傳遞一個 "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 ,則可以透過陣列的形式來指定:
js
props: {
// 同時允許 String 與 Number 型別的資料傳入
something: {
type: [String, Number]
}
}如果希望指定這個 props 為必要的屬性,則可以加上 required 屬性,並指定為 true :
js
props: {
something: {
required: true
}
}像這樣,如果沒有傳入指定的 props 則會在 console 主控台看到 Missing required prop: "something" 的錯誤。
替 props 指定預設值
當然要為某個 props 指定預設值也是沒問題,只要加上 default 屬性即可:
js
props: {
something: {
type: [String, Number],
default: 'Hello'
}
}這樣即使沒有傳入 something 這個 props,在子元件的實體中,也會自動給定 'Hello' 的字串做為預設值。
另外,像是陣列、物件的預設內容也是可以的:
js
something: {
type: Array,
default: [1, 2, 3]
}js
something: {
type: Object,
default: {
msg: 'Hello Vue 3.0!'
}
}透過 default 來指定預設內容,可以避免許多因 props 忘記傳遞帶來的問題。
自訂 props 驗證規則
如果 Vue.js 內建的幾種型別檢查還沒辦法滿足你的話,沒關係,我們可以加上 validator 屬性來自定驗證規則:
js
props: {
something: {
type: Number,
// 注意,在 validator function 內不可存取 data / computed 屬性!
// 驗證傳入的 something 是否大於 0
validator: value => value > 0
}
}像這樣,我們在 something 這個 props 加上了 validator 檢查, 當傳入的數值大於 0 的時候表示正確,否則 console 主控台將會出現錯誤訊息。
小提醒
注意 props 在元件初始化時的順序會更優先於 data 、 computed 等屬性,所以像是在 default 或 validator 是無法取得實體內的這些狀態 (意思是無法在裡面取得 this.xxx 的實體內容) 。
以物件作為 props 傳遞
由於 JavaScript 的物件是以「參考」的方式來傳遞的 (pass by reference) ,所以若是要由外層元件傳遞物件至內層子元件時,則需要特別小心。
假設外層元件的 data 有個叫 books 的陣列:
js
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'
},
]
}
}接著我們透過 v-for 將書籍資訊列出來:
html
<ul v-for="book in books" class="book">
<li>{{ book.name }}</li>
<li>{{ book.author }}</li>
<li>{{ book.publishedAt }}</li>
</ul>然後加入一個子元件,同樣用 v-for 來渲染 books,並將 book 當作 prop 傳入:
html
<!--
注意,若直接使用 HTML 作為模板的情況下,因 HTML 不分大小寫的特性,
v-bind 的屬性不可使用駝峰式 (:bookInfo) ,需要使用連字號 (:book-info) 才能正確解析。
寫在 JavaScript 實體物件或是 .vue 的單一元件檔的 template 模板則不受此限制。
-->
<my-component v-for="book in books" :key="book.id" :book-info="book" />傳給子元件的 bookInfo prop 屬性,我們將其設定為物件的資料,並在模板中使用 v-model 指令:
js
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>`,
});乍看之下感覺沒什麼問題,但是此時若我們嘗試在子元件對 input 進行修改,就會發現外層的資料也被變動了!

很遺憾地我必須要跟各位說,這肯定不是 feature,這是 bug ,而且是我們產生的 bug,絕對禁止!
這裡要與各位讀者強調一個觀念,在 Vue 的每個實體 (或者元件) 它們的狀態都應該要是彼此獨立的, 如果說今天子元件可以透過 props 自由地修改外層元件的狀態,那麼要是有「兩個以上」的元件同時引用同一個狀態作為 prpos 呢?
這時就可能由於某個子元件的修改,卻造成另一個子元件的 props 狀態污染,產生難以追蹤且不可預期的錯誤了。
所以,想要傳遞物件類型的 props 屬性時,應該先將物件屬性解構成原始型別 (Primitive) 後再將資料傳遞出去:
html
<my-component
v-for="book in books"
:name="book.name"
:author="book.author"
:published-at="book.published-at"></my-component>js
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'],
});像這樣,將傳入的 props 解構成純值的作法,更新時就不會改寫到外層的資料了。
如果覺得把所有屬性一個一個打散來寫太囉唆的話,也可以透過 v-bind 指令,改寫成 v-bind="book",這樣在傳入 props 至 my-component 元件時,會自動將 book 物件解構。
html
<!-- 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>非 prop 的屬性傳遞
前面有說到,我們可以透過網頁模版標籤上的屬性與 props 來做到子元件的資料傳遞,那麼假如我們在子元件忘了加上 props,又會發生什麼事呢?
html
<div id="app">
<!-- 這裏透過 v-bind 傳入 className -->
<my-component :class="className"></my-component>
</div>js
const app = Vue.createApp({
data() {
return {
className: 'block'
}
}
});
// 注意,子元件並未含有 props 屬性
app.component('my-component', {
template: `<div class="child-app"></div>`,
});
app.mount('#app');有趣的是,這個時候不但不會出現錯誤,而且子元件 <my-component> 的 HTML 渲染結果會是:
html
<!-- 直接將外層 class 內容交給實際的 DOM 身上 -->
<div class="child-app block"></div>而除了 props 之外,事件也有一樣的特性:
html
<div id="app">
<!-- 透過 v-on 訂閱 DOM 原生 click 事件 -->
<my-component :class="className" @click="greeting"></my-component>
</div>js
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');小提醒
屬性的傳遞與繼承只有在子元件是「唯一根節點」時有效,若子元件擁有多個根節點時,Vue.js 不知道該將屬性交給哪一個 DOM 節點,就會出現警告訊息。
js
// 多個根節點,若嘗試指定非 prop 的屬性傳遞,會出現警告訊息
app.component('custom-layout', {
template: `
<header>...</header>
<main>...</main>
<footer>...</footer>
`
});但如果我們在某個指定的標籤上加入了 v-bind="$attrs" 後,便可正常執行。
js
// 加入 v-bind="$attrs" 至指定的節點 (不一定要是根節點) 後可正常執行
app.component('custom-layout', {
template: `
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
`
});另外,若不希望屬性被子元件所繼承,則可在子元件加入 inheritAttrs: false
js
app.component('custom-layout', {
inheritAttrs: false,
// 略
template: `...`
});這樣非 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 來承接:
js
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>`,
});這樣就可以將 props 傳入的狀態複製一份由子元件來管理了。
Props 與遞迴元件
前面說過,在 Vue.js 的元件系統當中,元件裡頭可以再包覆另一個元件作為子元件。 但是你知道嗎,元件也可以將「自己」當成「子元件」,而這類元件通常我們稱它叫「遞迴元件」 (Recursive Component)。
而使用「遞迴元件」的限制只有一個,就是它必須要有 name 屬性。
讓我們以實務上很常見的樹狀選單為例,假設我們今天有個像這樣的階層式選單資訊:
js
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'
}]
}
]
};我們需要依照資料的階層來渲染樹狀選單,這時可以怎麼做呢? 很簡單,只需要一個子元件即可完成:
html
<div id="app">
<!-- Magic! -->
<menu-component
:title="menuData.name"
:child="menuData.childNodes"></menu-component>
</div>雖然看起來很神奇,但這並不是施展了什麼魔法,而是我們將 <menu-component> 當作子元件來利用:
html
<!-- <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>js
// 注意,「遞迴元件」必須要有 `name` 屬性,這樣在 template 內才會認得
app.component('menu-component', {
name: `menu-component`,
props: {
title: String,
url: String,
child: {
type: Array,
default: []
}
},
data() {
return {
isOpen: false
}
}
};像這樣,我們就可以透過列表元件 <menu-component> 進行包裝,樹狀選單就可以利用階層式物件搭配 Props 進行渲染了。
元件與自訂事件
然而從父元件傳遞 props 給子元件之後,有時可能會需要將處理過的狀態送回給外層的父元件, 但我們又不能直接修改外層的父元件的狀態,這時該怎麼處理呢?
在 Vue.js 裡面,父子元件之間的溝通方式有個流傳已久的口訣:「Props in, Event out」。

父層資料透過 props 傳入子層,而子層透過 event 來觸發父層狀態的更新。
props 傳入的部分我們前面看過了,現在我們來看看事件的部分。
在元件內部處理事件與 DOM 監聽事件一樣,我們可以透過 v-on 指令來處理:
html
<!-- 直接將 v-for 的 book 物件作為 props 傳遞 -->
<!-- 並監聽自訂的 update 事件 -->
<my-component
v-for="(book, idx) in books"
:key="idx"
v-bind="book"
@update="updateInfo"
></my-component>然後外層元件加上對應的 methods 作為事件處理器:
js
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;
}
}
});那麼子元件的 <my-component> 則是將 props 接收的狀態,在 data 複製一份後回傳一個新物件 bookInfo ,並且加上 watch 屬性來偵測更新:
js
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);
},
},
}
});當資料被更新時,我們就可以透過 this.$emit('事件名', 參數) 的方式來觸發事件。
如 bookInfo 被更新時,透過 this.$emit 觸發在 @update="updateInfo" 訂閱的自訂 update 事件,通知外層父元件的 updateInfo 來更新外層的狀態,而不是由子元件來直接更新。

像這樣,自己的資料自己改,才是 Vue.js 元件維護資料最安全的方式!
小提醒: 元件的父與子
雖然說不建議在父/子元件修改彼此的狀態,但實務上可能會因為需求的關係, 需要在子元件取得父層元件的內容,可以透過 this.$parent 來存取它的父層元件。
而父層元件則可以透過 this.$refs 來取得子元件, 在使用前子元件必須先加上 ref 屬性作為別名:
html
<my-component ref="child" />
<my-component ref="child2" />這樣就可以在父層透過 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 與網頁「表單元素」進行雙向綁定:
js
data () {
return {
msg: 'Hello Vue!'
}
}html
<input v-model="msg">這個時候, Vue 會將 msg 的值,也就是 "Hello Vue!" 放在 <input> 的 value 屬性中。 當使用這進行修改的時候,會將更新後的 <input> 內容回存至 data 的 msg 。
換句話說,上面的程式碼意義等同於:
html
<input :value="msg" @input="msg = $event.target.value" />那麼,本小節介紹的 v-model 又是怎麼應用在子元件上呢?
html
<div id="app">
<h1>{{ message }}</h1>
<!-- 透過 v-model 來做到父子元件間的「雙向綁定」 -->
<custom-input v-model="message"></custom-input>
</div>js
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");看起來很神奇對吧!
不過這只是一種語法糖,像這樣將 v-model 直接應用在元件的做法:
html
<custom-input v-model="message"></custom-input>
<!-- 或者 -->
<custom-input v-model:message="message"></custom-input>實際上是 Vue.js 會在背後將它解開成 v-bind 與 v-on 的組合:
html
<custom-input :message="message" @uptade:message="message = $event" /></custom-input>依然是透過觸發事件來通知父層元件更新狀態。
另外,若我們要綁定兩個以上的 v-model 到元件上,可以像這樣把變數傳遞到元件裡:
html
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName" />在元件內同樣透過 $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
└── 下略...這時候,若我們希望從 app 傳遞資料給 <list-item> 時,用傳統的 props 一層一層傳遞,肯定是很麻煩的一件事,而且還會增加元件之間的耦合程度。
provide 與 inject 機制的使用方式非常簡單。
首先,我們在根元件也就是 app 的層級,把要傳遞出去的資料定義在 provide 中:
js
const app = Vue.createApp({
data () {
return {
msg: 'Hello App!'
}
},
provide () {
// 將 this.msg 透過 provide 傳遞出去
return {
provideMsg: this.msg
};
}
});再來,在子或孫元件中 (總之不管隔幾層都可以) 需要取得頂層元件 provide 狀態的元件 (這裡以 list-item 為例) , 加上 inject 屬性:
js
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>`
}
}
});另外要注意的是,透過 provide 輸出的資料並不會隨著父層資料的更新而有所改變, 如果希望子層 inject 取回來的資料能與上層資料連動,則需要透過 Vue.computed() 進行包裝:
js
// 將 provide 透過 Vue.computed 包裝
provide() {
return {
provideMsg: this.msg,
provideMsg2: Vue.computed(() => this.msg)
};
}包裝後的物件,在子層元件的 inject 使用時,需要加上 .value 方可正常運作:
js
'list-item': {
// 由於傳入的是透過 Vue.computed 包裝後的物件,所以要加上 .value
// 有關 .value 的用法在本書最後一章 Composition API 會有更詳細的說明
inject: ['provideMsg', 'provideMsg2'],
template: `
<div>provideMsg: {{ provideMsg }}!</div>
<div>provideMsg2: {{ provideMsg2.value }}!</div>
`
}像這樣,若遇到跨層級的 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:
shell
$ npm install --save mitt或是透過 CDN 的方式將它引入至網頁裡:
html
<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>首先,新增一個新的 mitt() 實體,並將其指定到 bus 變數:
js
const bus = mitt();接著我們在外層的實體新增兩組 <button-counter> 元件,並在自訂事件 add-sum 觸發時執行 plus 這個方法。
html
<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>同時,我們在外層元件的 created 階段,針對一開始宣告的 EventBus 綁定一個 reset 的自訂事件:
js
// 根元件
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);
}
});另外, 在 <button-counter> 這個元件也同樣在 created 階段加上 reset 事件與對應方法 :
js
// 元件 <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);
}
});最後, <button-reset> 元件就單純多了,在點擊內部的 button 之後會執行 reset 方法, 裡面就只對 EventBus 觸發 reset 事件:
js
// 元件 <button-reset>
app.component('button-reset', {
template: `<button @click="reset">reset</button>`,
methods: {
reset () {
// 觸發 bus 的 reset 事件
bus.emit('reset');
},
},
});
像這樣,當 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 來改寫,首先抽取出共用的邏輯與方法:
js
// 共用邏輯
const sum = ref(0);
const plus = () => sum.value++;
const reset = () => sum.value = 0;再來,定義父子元件內容,新增的 setup 函式是用來建立與啟動我們的元件,並將模板 <template> 會用到的東西 return 出去:
js
// 父層、根元件
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
}
}
});改寫後的完整範例如下:
像這樣,雖然實際執行的結果沒什麼不同,但由於我們將相同的狀態、邏輯都抽取出來,透過 Composition API 改寫後的程式,看起來會比過去的寫法變得更加簡潔。 有關 Composition API 的詳細內容,在本書的最後一章會有整個章節來為讀者詳細解說,這裡就先簡單劇透一下。