重要声明:本文章仅仅代表了作者个人对此观点的理解和表述。读者请查阅时持自己的意见进行讨论。
在 Vue 项目开发中,Vue 框架的确给了我们许多便利,不用再频繁的获取某dom然后修改里面的内容,使用vue只需将字段重新赋值, 界面也会自动刷新。但有时候发现,我们给子组件的某个属性赋值后,界面却没有及时得到更新,出现这种现象的时候一刹那间是摸不着头脑的。
为了从头到尾把事情讲清除,咱们从子组件的编写开始。
一、建立子组件
我们这一节将建立一个实现数字增减的控件。它的外观看起来是这样的:。 新建 Vue 文件,使文件名(组件名)为:NumberMaker.vue, 开始干代码:
<!-- NumberMaker.vue -->
<template>
<div class="numbermaker">
<!-- 减按钮 -->
<button @click="number--">-</button>
<input min="0" max="99" type="number" v-model="number" autocomplete="off"/>
<!-- 加按钮 -->
<button @click="number++">+</button>
</div>
</template>
<script>
export default {
data () {
return {
number: 0
}
}
}
</script>
<style scoped>
/* 加边框,圆角,内边距 */
.numbermaker {
border: 1px solid gray;
display: inline-block;
border-radius: 3px;
padding: 3px 6px;
}
/* 去除输入框边框,限定宽度,文字居中 */
input {
outline: none;
width: 30px;
border: none;
text-align: center;
}
/* 去掉输入框 type=number 时的上下箭头 */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
input[type="number"] {
-moz-appearance: textfield;
}
</style>
这个组件很简单,一个减按钮、一个数字输入框、一个加按钮即完成了这个组件的样式。由于这个组件是要实现对数字的增加和减少功能, 所以很容易想到,组件内自己应该维护一个 number 变量,所以,data 模块里定义了 number 属性。
二、加入界面
为了引发本文的问题,我们在界面上设定一个输入框,这个输入框可以随时输入任意的数字。下面列出界面的代码:
<!-- App.vue -->
<template>
<div id="app">
<!-- 界面上的可输入数字的输入框 -->
<input v-model="myNum" type="number" autocomplete="off">
<br/>
<br/>
<!-- 使用子组件 -->
<number-maker/>
</div>
</template>
<script>
// 引入子组件
import NumberMaker from './components/NumberMaker.vue'
export default {
components: {
NumberMaker // 注册
},
data () {
return {
myNum: 0, // 页面上输入框的值
}
}
}
</script>
<style>
#app {
text-align: center;
margin-top: 60px;
}
</style>
此时运行程序你应该看到这样的界面:
要把页面上维护的 myNum
字段的值传入到子组件 NumberMaker
里面, 必然是要为子组件添加一个属性,刚才在定义组件是,
并没有想过要添加属性什么的,所以,再次修改组件的代码部分,加入 props
属性,我们就命名这个属性为 pNumber
好了。
修改组件代码如下:
<!-- NumberMaker.vue -->
<template>
...
</template>
<script>
export default {
// 新增属性节点
props: {
// 添加一个 pNumber 属性,该属性只能是数字,让界面可以传入数字
pNumber: Number
},
data () {
return {
number: this.pNumber // 不再使用 固定值0, 而是使用属性具体的值。
}
}
}
</script>
<style>
...
</style>
首先我们新增了 props
节点, 此节点的作用就是暴露组件支持的参数。然后我们将 data
模块中的数据 number
字段使用参数pNumber
的值,
这样一来,组件在渲染时就可以接收到页面传递进入的值了。
为了体现效果,将页面中的字段 myNum
默认值修改为非零的其它值, 并将参数传递个子组件:
<!-- App.vue -->
<template>
<div id="app">
<!-- 界面上的可输入数字的输入框 -->
<input v-model="myNum" type="number" autocomplete="off">
<br/>
<br/>
<!-- 使用子组件,并传入参数 -->
<number-maker :p-number="myNum"/>
</div>
</template>
<script>
// 引入子组件
import NumberMaker from './components/NumberMaker.vue'
export default {
components: {
NumberMaker // 注册
},
data () {
return {
myNum: 5, // 页面上输入框的值
}
}
}
</script>
<style>
...
</style>
此时,刷新界面,可以看到,组件里的值已经按照设定的值进行了响应变化。
三、重现问题
经过了上面一系列的常规操作,问题也应运而生了。修改输入框里的值,却不能同时更新到子组件里面的值:
这个时候,你心里肯定就想,我要是能在子组件里监听到父界面里面这个 myNum
字段的变化,这不就能实现组件里的值
也随之变化嘛。这个想法非常好,但不得不告诉你,子组件是不能实现监听父界面的参数变化的。仔细想想,我作为子组件,
我怎么知道你父界面上有些什么东西,我只管好我自己的事情就好了。但是难道就不能实现这样的功能了吗?当然能实现了。
虽然子组件不能监听到父界面的东西,但是我可以使用另一种方法来实现这样的需求。
四、解决问题
我们在 NumberMaker
组件中使用了属性 pNumber
,虽然我子组件不知道父界面有什么东西,但是 pNumber
参数是我自己创建的,父界面要把参数传进来,就得通过这个字段,我不能监听父界面的参数,那我还不能监听我自己定义的参数
pNumber
吗,这个我总可以监听了吧。说干就干,现在来监听 pNumber
字段试试,修改 NumberMaker
组件的代码:
<!-- NumberMaker.vue -->
<template>
...
</template>
<script>
export default {
props: {
pNumber: Number
},
watch: {
// 监听参数pNumber, 并将新值赋给data里面的number字段。
pNumber (newvalue) {
this.number = newvalue;
}
},
data () {
return {
number: this.pNumber
}
}
}
</script>
<style>
...
</style>
添加了 watch
节点,加入了对参数 pNumber
的监听支持,并将值赋值给了组件的number字段。
这样一来,再看看界面的运行效果:
cool! 这不就是想要的效果嘛。现在已经做到了修改父界面的内容,子组件同时做到响应的更新。 那如果更新子组件的内容,父界面的值会更新吗?先来看看会不会:
看起来子组件内容变化并不能同步更新父界面的值。其实要实现组件里的字段更新通知到父界面,
可以使用 $emit
方法,将事件暴露给父界面,代码就像下面:
效果:
可以看到,让子组件的值变化暴露给父界面,使用 $emit
方法的确可以实现,不过在父界面中我们写了相应的
2 处代码来实现这个功能。我们能不能在自己的组件里实现像 input
组件那样的 v-model
功能呢,
最终达到,能在自己的组件上使用 v-model
功能呢?
当然能, 不过这就对 props
里的属性名和 $emit
出去的事件名有硬性要求了。要了解的关键点如下:
- 使用v-model传递给组件的数据,在组件内须使用属性字段 value 来接收。
- 要将数据可以同时对外输出,在 $emit 的时候,须将事件名固定为 input。
明白了这2点,就可以重新修改 NumberMaker
组件的代码:
<!-- NumberMaker.vue -->
<template>
...
</template>
<script>
export default {
props: {
value: Number
},
watch: {
// 监听参数value, 并将新值赋给data里面的number字段。
value (newvalue) {
this.number = newvalue;
},
// 监听字段number,并将变更事件发出,事件名为 input
number (newvalue) {
this.$emit('input', newvalue);
}
},
data () {
return {
number: this.value
}
}
}
</script>
<style>
...
</style>
由于 v-model
要求属性字段需要是 value
,所以将组件里的 pNumber
修改为value,然后将监听 pNumber
的方法修改为监听 value 的方法,将 number
的变化事件通过 input
事件名称向外暴露,
这样就能在父界面不用再写一堆监听的代码了,只需要一个 v-model
即可实现。重新修改 App.vue
界面代码:
<template>
<div id="app">
<!-- 界面上的可输入数字的输入框 -->
<input v-model="myNum" type="number" autocomplete="off">
<br/>
<br/>
<!-- 使用子组件,直接使用v-model实现双向绑定,就如同上边的input那样 -->
<number-maker v-model="myNum"/>
</div>
</template>
<script>
// 引入子组件
import NumberMaker from './components/NumberMaker.vue'
export default {
components: {
NumberMaker // 注册
},
// 不再需要监听方法了。
// methods: {
// // 监听子组件值变化结果的方法
// onNumChange (val) {
// this.myNum = val;
// }
// },
data () {
return {
myNum: 5, // 页面上输入框的值
}
}
}
</script>
<style>
...
</style>
修改引用组件处的代码,移除了原来的监听,移除了原来的 pNumber
参数,直接使用 v-model
将父界面的
myNum
放入,实现双向绑定。效果图:
总结
这也是为啥,常常看到许多出名的vue组件库里面的组件里,参数里有个 index
,而data里面又有一个 currentIndex
,
或相似的有两个字段,不仅仅是因为要实现双向绑定的功能,还有一个原因就是,Vue组件里,不能直接修改 props
里面的属性内容,所以要定义组件自身使用的字段在data里,也要定义外界能传入的参数在props里。组件实现监听父界面
的内容,其实就是组件监听自己定义的props里面的属性来实现。永远不要去想直接监听父界面的参数,这有背于组件的概念。
本例涉及的项目你可以在这里下载到:百度网盘, 提取码:rhqe
。