【Vue.JS】Render 實現留言板實例及 Avoid mutating a prop directly 錯誤處理

聲明:文中代碼整體思路來源於 樑灝 編著的 【Vue.JS 實戰】一書,學習過程中發現一處問題。以做記錄

效果圖

img

代碼

  • index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>BBS</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
</head>

<body>
    <div id="demo" v-cloak style="width:500px;margin:0 auto">
        <div class="message">
            <v-input v-model="username"></v-input>
            <v-textarea v-model="message" ref="message"></v-textarea>
            <button @click="handleSend">發送</button>
        </div>
        <list :list="list" @reply="handleReply"></list>
    </div>
    <script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
    <script src="input.js"></script>
    <script src="list.js"></script>
    <script src="main.js"></script>
</body>

</html>
  • main.js
var demo = new Vue({
    el: '#demo',
    data: {
        username: '',
        message: '',
        list: []
    },
    methods: {
        handleSend() {
            if (this.username === '' || this.message === '') {
                alert('不能爲空');
                return;
            }
            this.list.push({
                username: this.username,
                message: this.message,
            });
            this.message = '';
        },
        handleReply(index) {
            var name = this.list[index].username;
            this.message = "回覆@" + name + ':';
            this.$refs.message.focus();
        }
    },
})
  • input.js
Vue.component('vInput', {

    props: {
        value: {
            type: [String, Number],
            default: ''
        }
    },

    render: function (createElement) {
        var _this = this;
        return createElement('div', [
            createElement('span', '暱稱:'),
            createElement('input', {
                attrs: {
                    type: 'text'
                },
                domProps: {
                    value: this.value,
                },
                on: {
                    input: function (event) {
                        _this.value = event.target.value;
                        _this.$emit('input', event.target.value);
                    }
                }
            })
        ]);
    }
});

Vue.component('vTextarea', {
    props: {
        value: {
            type: [String, Object],
            default: '',
        }
    },
    render: function (createElement) {
        var _this = this;
        return createElement('div', [
            createElement('span', '留言內容:'),
            createElement('textarea', {
                attrs: {
                    placeholder: '請輸入內容',
                },
                domProps: {
                    value: this.value,
                },
                ref: 'message',
                on: {
                    input: function (event) {
                        //_this.value = event.target.value;
                        _this.$emit('input', event.target.value);
                    }
                }
            })
        ])
    },
    methods: {
        focus: function () {
            this.$refs.message.focus();
        }
    },
})
  • list.js
Vue.component('list', {
    props: {
        list: {
            type: [Array],
            default: function () {
                return [];
            }
        }
    },

    render: function (createElement) {
        var _this = this;
        var list = [];
        this.list.forEach((item, index) => {
            var node = createElement('div', {
                attrs: {
                    class: 'list-item',
                }
            }, [
                createElement('span', item.username + ':'),
                createElement('div', {
                    attrs: {
                        class: 'list-msg',
                    }
                }, [
                    createElement('p', item.message),
                    createElement('a', {
                        attrs: {
                            class: 'list-reply'
                        },
                        on: {
                            click: function () {
                                _this.handleReply(index);
                            }
                        },
                    }, '回覆'),
                ])
            ]);
            list.push(node);
        });
        if (this.list.length) {
            return createElement('div', {
                attrs: {
                    class: 'list',
                },
            }, list);
        } else {
            return createElement('div', {
                attrs: {
                    class: 'list-nothing',
                }
            }, '列表爲空')
        }
    },

    methods: {
        handleReply: function (index) {
            this.$emit('reply', index);
        }
    },
})
  • main.css
[v-cloak] {
    display: none;
}

* {
    padding: 0;
    margin: 0;
}

.message {
    width: 450px;
    text-align: right;
}

.message div {
    margin-bottom: 12px;
}

.message span {
    display: inline-block;
    width: 100px;
    vertical-align: top;
}

.message input,
.message textarea {
    width: 300px;
    height: 32px;
    padding: 0 6px;
    color: #657180;
    border: 1px solid #d7dde4;
    border-radius: 4px;
    cursor: text;
    outline: none;
}

.message input:focus,
.message textarea:focus {
    border: 1px solid #3399FF;
}

.message textarea {
    height: 60px;
    padding: 4px 6px;
}

.message button {
    display: inline-block;
    padding: 6px 15px;
    border: 1px solid #39F;
    border-radius: 4px;
    color: #FFF;
    background-color: #39F;
    cursor: pointer;
    outline: none;
}

.list {
    margin-top: 50px;
}

.list-item {
    padding: 10px;
    border-bottom: 1px solid #e3e8ee;
    overflow: hidden;
}

.list-item span {
    display: block;
    width: 120px;
    float: left;
    color: #39F;
}

.list-msg {
    display: block;
    margin-left: 60px;
    text-align: justify;
}

.list-msg a {
    color: #9ea7b4;
    cursor: pointer;
    float: right;
}

.list-msg a:hover {
    color: #39F;
}

.list-nothing {
    text-align: center;
    color: #9ea7b4;
    padding: 20px;
}

遇到問題

在測試過程中,沒有發現功能問題,但是,點開 F12 後
error

雖然標記的是 Vue warn ,僅僅是警告,而且也沒有影響到功能的使用,但顯示成紅色的 Error 總歸有些不爽的。
於是,開始查找原因 …
發現是 由於 input.js 中監聽 input 事件時 _this.value = event.target.value; 這一行代碼引起的錯誤。

查閱本書之前對組件傳值的描述,並結合Vue的官方文檔,得出問題原因如下:

  • 錯誤描述:
    Vue Warning:
    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. Prop being mutated: “value”
  • 中文對照:
    避免直接修改屬性,因爲只要父組件重新渲染,該值就會被覆蓋,應該根據 prop 的值使用 data 或計算屬性
  • 本書資料:
    Vue 1.X 中提供 .sync 修飾符支持雙向綁定,Vue 2.X 中只支持 props 單向傳遞數據,目的是儘可能的將父子組件解耦,避免子組件無意中修改父組件內容。
  • 官方資料:
    所有的prop都使其父子之間形成單向下行綁定:即父級prop的更新會流動到子級,但是反向不允許,爲了防止子級意外改變父級組件狀態,每次父組件發生更新時,子組件中的所有prop都會刷新爲最新值。這意味着操作者不應該在子組件中修改prop的值。如果這樣做,Vue將會在瀏覽器中發出警告。

也就是說,報錯的根本原因是 _this.value = event.target.value; 這一句話在子組件中對父組件的 value 進行了賦值操作,這在 Vue 2.X 中是不被允許的。

修改方法

首先要分析是否的確需要在子組件中更新父組件的值。

  • 確認需要在子組件中操作父組件的值
    • 父組件直接調用子組件方法,子組件通過參數傳值
    • 子組件使用 $emit() 方法觸發事件,將值當做參數傳遞,父組件使用 on 方法監聽事件。(示例中使用該方法)
    • 示例:
    render: function (createElement) {
    	......
    		on: {
                input: function (event) {
                	_this.$emit('input', event.target.value);
                }
           }
    }
    
  • 僅在子組件中使用且操作該值
    • 使用 data 在子組件中存儲該值,所有對該值的操作盡在子組件中有效,不影響父組件
    • 使用 computed 計算屬性存儲該值,效果與使用 data 類似。
    • 示例:
    Vue.component('vInput', {
    props: {
        value: {
            type: [String, Number],
            default: ''
        }
    },
    data(){
        return{
            text : this.value,
        }
    },
    render: function (createElement) {
    	......
    		on: {
                input: function (event) {
                	_this.text = event.target.value;
                }
           }
    }
    ....       
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章