聲明:文中代碼整體思路來源於 樑灝 編著的 【Vue.JS 實戰】一書,學習過程中發現一處問題。以做記錄
效果圖
代碼
- 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 後
雖然標記的是 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; } } } ....