Component 若只有一層,那問題不大,若牽涉到 Component 包含 Component,就會牽涉到一個基本問題:該如何將 Model 由外層 Component 傳給內層 Component ? 此時就要使用 Prop。
Version
macOS Catalina 10.15.6
WebStorm 2020.2
Vue 2.6.11
Static Prop
由 component 顯示 Hello World
。
App.vue
<template>
<hello-world greeting="Hello" user-name="World"></hello-world>
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'app',
components: {
HelloWorld
}
}
</script>
一樣是老梗 Hello World,但這次資料並不是在 HTML template 內定義,而是由外層傳進來。
第 2 行
<hello-world greeting="Hello" user-name="World"></hello-world>
一樣使用 HelloWorld
component,但這次透過自訂的 greeting
與 userName
props 將資料由 component 外層傳進 component。
Prop 在 HTML template 會使用 kebab-case
HelloWorld.vue
<template>
<div>{{ greeting }} {{ userName }}</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: [
'greeting',
'userName'
],
}
</script>
第 8 行
props: [
'greeting',
'userName',
],
若要由外層 component 的傳資料進來,則要在 component 使用 props
定義。
在 JavaScript 會使用 camelCase 宣告 prop
第 2 行
<div>{{ greeting }} {{ userName }}</div>
在 props
宣告過 greeting
與 userName
prop 之後,就可在 HTML template 如同使用 model 一樣。
- Prop 在 HTML template 會使用 kebab-case
- Prop 在 JavaScript 會使用 camelCase
Dynamic Prop
外層 component 將 model 動態傳入 prop。
App.vue
<template>
<div>
<div v-for="(userName, index) in userNames" :key="index">
<hello-world greeting="Hello" :user-name="userName"></hello-world>
</div>
</div>
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'app',
components: {
HelloWorld
},
data: () => ({
userNames: [
'Sam',
'Kevin',
'John'
]
})
}
</script>
之前傳進 prop 的值是寫死的,能夠動態將 model 與 prop 綁定嗎 ?
第 3 行
<div v-for="(userName, index) in userNames" :key="index">
<hello-world greeting="Hello" :user-name="userName"></hello-world>
</div>
想將 userName
綁定到 user-name
prop,必須使用 :
attribute binding。
而 userName
則由 v-for
來自於 userNames
model。
17 行
data: () => ({
userNames: [
'Sam',
'Kevin',
'John'
]
}),
userNames
model 在 data
內定義。
HelloWorld.vue
<template>
<div>{{ greeting }} {{ userName }}</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: [
'greeting',
'userName'
],
}
</script>
與 static prop 的 HelloWorld.vue
完全一樣。
透過 dynamic prop,我們就能將外層 component 的 model 透過 prop 傳進 內層 component,但必須使用 attribute binding
Unidirectional Dataflow
若在內層 component 內直接修改 prop,Vue 會出現警告訊息。
App.vue
<template>
<div>
{{ counter }}
<button @click="onClick">+</button>
<my-counter :inner-counter="counter"></my-counter>
</div>
</template>
<script>
import myCounter from '@/components/MyCounter.vue'
let onClick = function() {
this.counter++
}
export default {
components: {
myCounter
},
data: () => ({
counter: 0
}),
methods: {
onClick
}
}
</script>
Vue prop 是 unidirectional dataflow,也就是 model 只能從外層 component 傳進內層 component,而無法由內層傳到外層。
若要將 model 從內層 component 傳到外層 component,則要使用 event
第 3 行
{{ counter }}
<button @click="onClick">+</button>
外層有自己 onClick()
計算 counter。
第 5 行
<my-counter :inner-counter="counter"></my-counter>
想要將外層 component 的 counter
model 透過 inner-counter
prop 傳進內層 MyCounter
component。
12 行
let onClick = function() {
this.counter++
}
外層 component 自己計算 counter
model。
MyCounter.vue
<template>
<div>
<h2>{{ innerCounter }}</h2>
<button @click="onClick">+</button>
</div>
</template>
<script>
let onClick = function() {
this.innerCounter++
}
export default {
name: 'MyCounter',
props: [
'innerCounter'
],
methods: {
onClick
}
}
</script>
15 行
props: [
'innerCounter'
],
宣告 innerCounter
prop。
第 9 行
let onClick = function() {
this.innerCounter++
}
直接對 innterCounter
prop 做累加。
這種寫法雖然可執行,但有個淺在問題:由於 Vue 的 unidirectional dataflow,每次 外層 component 的 model 改變,都會同時透過 prop 傳入內層 component,因而會 覆蓋
掉原本 component 內部累加。
Vue 在 console 也提出警告,建議不要對 prop 直接修改,因為會隨時會被外層 component 的 model 蓋掉。
由於不建議直接改 prop,Vue 官網建議改用以下兩種 pattern:
- Model
- Computed
Model
改用 model 之後,就不再有警告訊息。
App.vue
<template>
<div>
{{ counter }}
<button @click="onClick">+</button>
<my-counter :start-counter="counter"></my-counter>
</div>
</template>
<script>
import myCounter from '@/components/MyCounter.vue'
let onClick = function() {
this.counter++
}
export default {
components: {
myCounter
},
data: () => ({
counter: 0
}),
methods: {
onClick
}
}
</script>
第 5 行
<my-counter :start-counter="counter"></my-counter>
由 inner-counter
prop 改成 start-counter
prop。
inner-counter
要留給內層 component 的 model 所用
MyCounter.vue
<template>
<div>
<h2>{{ innerCounter }}</h2>
<button @click="onClick">+</button>
</div>
</template>
<script>
let onClick = function() {
this.innerCounter++;
}
export default {
name: 'my-counter',
props: [
'startCounter'
],
data: function() {
return {
innerCounter: this.startCounter
}
},
methods: {
onClick
}
}
</script>
15 行
props: [
'startCounter'
],
改成 startCounter
prop。
18 行
data: function() {
return {
innerCounter: this.startCounter
};
},
由於 prop 不適合直接修改,因此我們另外宣告了 innerCounter
model。
startCounter
prop 的用途只在於初始化 innerCounter
model,之後 startCounter
有任何變動,都無法改變 innerCounter
。
data: () => ({
innerCounter: this.startCounter
}),
注意這裡只使能使用 function expression,不能使用 arrow function,因為使用了 this
context 存取 prop,arrow function 將無法被改變 this
指向 component。
這是少數 model 一定要使用 function expression 之處,因為要使用
this
讀取startCounter
prop
第 9 行
let onClick = function() {
this.innerCounter++;
}
直接對 innerCounter
model 做累加。
Computed
使用 model 的優點是內層 component 的資料不會受外層影響,但缺點是內層 component 的資料不會與外層連動。
若要內外層 model 能夠連動,則建議改用 computed。
MyCounter.vue
<template>
<div>
<h2>{{ innerCounter }}</h2>
</div>
</template>
<script>
let innerCounter = function() {
return this.startCounter + 1
}
export default {
name: 'my-counter',
props: [
'startCounter'
],
computed: {
innerCounter
}
}
</script>
17 行
computed: {
innerCounter
}
innerData
由 model 改成 computed。
第 8 行
let innerCounter = function() {
return this.startCounter + 1
}
innerCounter
computed 直接使用 startCounter
prop,因此與外層 component 連動。
Model 與 computed 兩種寫法都各有適用場景,視需求決定要使用哪種方式
Conclusion
- 透過 prop,model 也能由外層傳進 component
- Dynamic props 能將 MVVM 的 model 綁定 prop,不再只是寫死的資料
- Prop 只支援 unidirectional dataflow,也就是外層的 model 改變,會影響到內層 component,但內層 component 的 model 改變,不會影響到外層 component
- Model 與 computed 都能改善 prop 的 unidirectional dataflow,但也不能完全取代,要視需求而使用