Vue本身一個很好用的特性是可以利用Vue的元件(Component)去拼成一個頁面。將頁面拆成元件的好處是這些元件可以被重覆的使用在其他頁面上。 一個父元件裡面可以有許多子元件,而子元件裡面當然可以有更多的子元件。要做到一個父/子之間的互動,傳遞參數必然少不了。單純的垂直參數傳遞可以透過props和emit事件來達成。 父元件透過props傳遞參數給子元件,子元件則靠emit事件回給父元件監聽。看似單純,但其實有許多有趣的眉角在裏頭。
<!-- MyInput.vue --> <template> <div> <input type="text" :value="childData" @input="update" /> <div>childData: {{childData}}</div> </div> </template> <script> export default { props: ['childData'], methods: { update(e) { this.$emit('input', e.target.value); } } } </script>
<!-- Parent.vue --> <template> <div> <my-input :childData="parentData" @input="updateParentData"></my-input> <div>parentData: {{parentData}}</div> <button @click="setParentData"> Change Parent Data</button> </div> </template> <script> import MyInput from '@src/component/MyInput' export default { components: { MyInput }, data(){ return { parentData : 'hello' } }, methods: { setParentData(){ this.parentData = "world" + new Date().getTime(); }, updateParentData(newVal) { this.parentData = newVal; } } } </script>
以上是一個MyInput元件,裡面有一個簡單的text input,並且父和子元件都有各自的data。透過父元件裡的按鈕去變更parentData會連動到子元件裡的childData,反之如果在子元件裡input輸入文字也會反應回parentData。
如果希望MyInput元件可以支援v-model的方式,我們做了以下修改:
<!-- Parent.vue --> <template> <div> <my-input v-model="parentData"></my-input> ... </div> </template> <script> ... export default { ... methods: { setParentData(){ this.parentData = "world" + new Date().getTime(); } } } </script>
<!-- MyInput.vue --> <template> <div> <input type="text" :value="value" @input="update" /> ... </div> </template> <script> export default { props: ['value'], ... } </script>
其實v-model在vue裡面等於 :value=”” @input=””。也就是在MyInput.vue裡面要有一個名為”value”的props跟emit一個”input”的事件。基本上用v-model寫法看起來也簡潔許多。
當然,在MyInput裡面的text input本身也支援v-model。如果此時直接在<input>裡面直接寫v-model=”value”,其實是會報錯的。原因是value是props,而在vue裡面props是不建議被子元件修改的。這時候我們可以新建一個變數childData,並在一開始的時候將value指派給他。修改如下:
<!-- MyInput.vue --> <template> <div> <input type="text" v-model="childData" /> <div>childData: {{childData}}</div> </div> </template> <script> export default { props: ['value'], data() { return { childData: this.value } }, watch: { childData(newVal){ this.$emit('input', newVal); } } } </script>
到目前為止把吃v-bind+v-on的方式都改用v-model的方式改寫,但在這邊會發現其實在input輸入文字會反應回parentData,但 setParentData 的按鈕卻失效了!! 這是因為父元件的parentData傳遞給子元件prop value,並在初始化的時候就指派給childData。此時parentData後續即使有任何變動,其實只有prop的value知道,而childData不會知道。所以要達到上述變動,我們必須要監聽value的變動,並且重新更新childData。新的程式碼如下:
<template> <div> ... </div> </template> <script> export default { ... watch: { childData(newVal){ this.$emit('input', newVal); }, value(newVal){ this.childData = newVal } } } </script>
這樣一來不管是parentData還是childData被更改,父/子元件都可以 同時反應出變動。這樣也不用擔心parentData資料是非同步被更動了。
看似短短的v-model語法,有時候沒設定好有可能會發生許多問題,例如遞迴更新、參數只改動到一邊、或是View沒被更新等等。當然還有其他有趣的解法 :
- 子元件除了提供v-model支援外,也可以吃其他的props去做判斷
- 直接在元件上加key=”parentData”,每次變數的異動都會強迫重新更新
- 使用v-if,用額外的變數去控制render這件事
曾經試過上面三種解法,個人感想是的確能解決問題,但程式碼可能變得沒那麼乾淨,甚至在某些情境下會有副作用 (例如forceUpdate,讓元件每次re-render造成在user-interaction上反應變遲鈍)。最後,在玩component傳遞參數時如果還有遇到一些奇怪的問題,下面提供許多不錯的文章可以參考參考。
Reference
- https://michaelnthiessen.com/26-time-saving-tips/
- https://vuejsdevelopers.com/2018/06/18/vue-components-play-nicely/
- https://alligator.io/vuejs/add-v-model-support/
- https://michaelnthiessen.com/how-to-watch-nested-data-vue/
- https://michaelnthiessen.com/difference-between-computed-property-and-watcher/
- https://medium.com/@sky790312/about-vue-2-parent-to-child-props-af3b5bb59829
- https://michaelnthiessen.com/force-re-render/