雙向綁定(two-way data bindings)是指把畫面上的 DOM 與資料透過 Vue 實體來綁定。當其中一方有更動時,另一方都會隨即更新。
單向資料流是 Vue 官方提倡的一種管理元件資料狀態模式。重點在於,當子層元件透過 props 來接收父層元件傳來的資料時,子層不應該直接修改 props,不然開發時就難以追蹤資料狀態的變化。建議做法是透過 emit,觸發事件來修改在父層的資料。也就是常見口訣:「props in, event out」的意思。
以下會再詳細解說雙向綁定、單向資料流、以及使用 props 時須注意的事。
雙向綁定的重點就是當畫面或資料有更新,對方也會隨之更新。最明顯的例子就是 v-model
,我們很常在 input 欄位上 v-model
來綁定畫面中輸入欄目前的內容,以及在 data 裏的資料。一旦用戶輸入內容,我們的 Vue 裏的 data 資料也會同步擁有相同的資料,反之亦然。
單向資料流的概念是針對父子層元件的資料傳遞時的模式,它跟雙向綁定的概念並沒有衝突。當要把資料從父元件傳遞到子元件時,子元件會使用 props 來接收,但子元件不應該直接修改 props 來修改父元件的資料 ,而是使用 emit。
假如你是傳入基本型別的資料,例如數字、字串等,你在子元件不會修改到父元件的資料,因為當父元件再被渲染時,它依舊要指向父元件所定義的資料,像以下例子:
HelloWorld.vue(子層)
<input v-model="childMsg" />
export default {
props: {
value: String,
childMsg: {
type: String,
default: "default msg"
}
}
};
App.vue (父層)
<template>
<div>
{{ parentMsg }}
<HelloWorld :childMsg="parentMsg" />
</div>
</template>
export default {
name: "Home",
components: {
HelloWorld
},
data() {
return {
parentMsg: "Hello vue!"
};
}
};
警告:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.
父層的 parentMsg
並沒有被修改,依舊為 "Hello vue!"。但 Vue 會把你以上的行為視作嘗試修改父層資料,因此跳警告提示此舉是無效。
如果是資料是物件呢?會否跳警告?
在父元件加入物件資料:
App.vue
data() {
return {
parentMsg: "Hello vue!",
parent: {
a: 123
}
};
}
綁定 props:
<template>
<div>
<p>{{ parentMsg }}</p>
<p>{{ parent }}</p>
<HelloWorld :childMsg="parentMsg" :child="parent" />
</div>
</template>
HelloWorld.vue(子元件):
<template>
<div>
<input v-model="childMsg" />
<input v-model="child.a" />
</div>
</template>
<script>
export default {
props: {
value: String,
childMsg: {
type: String,
default: "default msg"
},
child: {
type: Object,
default: {}
}
}
};
</script>
結果會直接改掉父層資料,而且 console 沒有跳警告:
官方文件有解釋,這是因為物件和陣列是傳址(pass by reference),因此會直接修改到父元件的資料。官方不建議這做法。
在 Vue 3,如果你的 Vue CLI 專案有加入 ESLint,當你的子元件的 input ,使用 v-model
直接綁定 props時,ESLint 就都會報錯:
Unexpected mutation of "child" prop
但如果沒有加入 ESLint,不管是修改物件還是基本型別資料,也不會報錯,而且也能把父元子的資料改掉。
為了保持單向資料流,就不能在子元件綁定 props。
如果 props 是基本型別資料,那就在 data 裏建立屬性來拷貝 props 的值即可,或者使用 computed 來處理資料再掉回畫面使用,這些都是官方文件有提到的做法,因此就不作解釋了。
但如果資料是物件或陣列,考慮到傳址的問題,可以用以下方式解決:
v-bind
,自動解構賦值如果物件資料的屬性不多的話,可以用 v-bind
來處理。示範如下:
假設在父元件有一筆物件資料,並傳送到子元件的 input 裏:
App.vue 資料(父元件):
data() {
return {
user: {
name: "Tom",
age: 20,
},
};
},
傳入子元件(Child1):
<Child1 v-bind="user" />
在子元件裏,設定 name
和 age
這兩個 props,解構 user 物件並賦值到這兩個 props 裏,最後再在 data
拷貝這兩個 props:
<template>
<div>
<input type="text" v-model="userName">
</div>
<div>
<input type="text" v-model="userAge">
</div>
</template>
<script>
export default {
props: ["name", "age"],
data() {
return {
userName: this.name,
userAge: this.age,
}
}
};
</script>
但如果該物件資料的屬性很多,就需要寫很多個 props,因此此方法就不適合。
JSON.parse
處理遇上物件屬性很多的情況,我們可以直接把整個物件傳進去子元件,並在 data 裏使用 JSON.parse()
和 JSON.stringify()
深拷貝這個物件,最後在 input 綁定拷貝的結果:
Child2(子元件):
<template>
<div>
<input type="text" v-model="user.name" />
</div>
<div>
<input type="text" v-model="user.age" />
</div>
</template>
<script>
export default {
props: ["userInfo"],
data() {
return {
user: JSON.parse(JSON.stringify(this.userInfo)),
// 用展開語法也可以,但注意不要用在處理多層物件,但因為只能做淺拷貝
// user: {...this.userInfo}
};
},
};
</script>
v-bind
才能傳入數字當我們要以 props 傳入數字,必須要使用 v-bind
才可以。沒有使用 v-bind
的話,一律當作傳入純字串。
字串:
<HelloWorld num="3" />
數字
<HelloWorld :num="3" />
注意,:
是 v-bind
的縮寫。
官方在 style guide 提及,不建議以下的寫法:
props: ['status']
Vue 建議用物件方式,加入型別檢查、預設值等等:
props: {
status: {
type: String,
required: true,
validator: function (value) {
return [
'syncing',
'synced',
'version-conflict',
'error'
].indexOf(value) !== -1
}
}
}
這是為了更嚴謹傳入 props,減少錯誤。因為當錯誤傳入 props時,Vue 會跳警告提醒。在例子中,如果 validator
回傳 false
,同樣會跳黃字警告。
另外,有些情況我亦會使用 default
屬性來設定預設值。例如在呼叫 API 時,需要等一筆陣列資料回傳過來,再放入 props 裏傳給子元件,再在子元件使 v-for
顯示陣列裏每一筆的資料。如果在子元件的 props 先建立預設值,那麼使用 v-for
來綁定 props 時就不會報錯。
v-bind
自動解構賦值,或者是深拷貝方法 JSON.parse()
和 JSON.stringify
來處理。props
時,建議用物件方式來建立,而非陣列。從而更嚴謹和仔細處理 props 的資料。