Vue Component v-model 筆記

Vue本身一個很好用的特性是可以利用Vue的元件(Component)去拼成一個頁面。將頁面拆成元件的好處是這些元件可以被重覆的使用在其他頁面上。 一個父元件裡面可以有許多子元件,而子元件裡面當然可以有更多的子元件。要做到一個父/子之間的互動,傳遞參數必然少不了。單純的垂直參數傳遞可以透過propsemit事件來達成。 父元件透過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

Leave a Reply

avatar