声明:文中代码整体思路来源于 梁灏 编著的 【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; } } } ....