點燈坊

失くすものさえない今が強くなるチャンスよ

Vue 之 Prop

Sam Xiao's Avatar 2020-08-03

Component 若只有一層,那問題不大,若牽涉到 Component 包含 Component,就會牽涉到一個基本問題:該如何將 Model 由外層 Component 傳給內層 Component ? 此時就要使用 Prop。

Version

macOS Catalina 10.15.6
WebStorm 2020.2
Vue 2.6.11

Static Prop

prop001

由 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,但這次透過自訂的 greetinguserName 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 宣告過 greetinguserName prop 之後,就可在 HTML template 如同使用 model 一樣。

  • Prop 在 HTML template 會使用 kebab-case
  • Prop 在 JavaScript 會使用 camelCase

Dynamic Prop

prop002

外層 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

prop000

若在內層 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

prop003

改用 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

prop004

使用 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,但也不能完全取代,要視需求而使用

Reference

Vue, Passing Data to Child Components with Props
Vue, Props