- 组件基础概念
- 全局注册
- 局部注册
- is属性强制挂载组件
- 组件的其他属性
- 父组件向子组件传递数据:props使用字符串数组
- 单向数据流
- 父组件向子组件传递数据:props使用对象
组件基础概念
一个组件在HTML里就像是一个自定义标签,一个组件同时也是一个Vue实例,会根据实例的内容被渲染成实际的样子。
一个组件可以单独使用,也可以放在其他实例的内部使用,但在使用之前,都需要注册。
注册有两种方式,全局注册和局部注册。全局注册就是直接写在js代码或者文件的外层,这样在其后的所有代码都可以使用这个组件。局部注册就是在一个Vue实例的内部进行注册。
全局和局部并不是对于页面来说的。仅仅进行注册,而不在Vue实例的$el属性绑定的元素内,是无法使用组件的(因为HTML会把其当做一个普通的元素,在Vue内部才能对组件进行渲染。
全局注册
可以认为组件也是一个Vue实例,只不过写法和普通的Vue实例有些区别,现在先看一个最简单的全局注册例子,即组件的template属性可以渲染HTML元素:
<div id="app">
<my-comp></my-comp>
</div>
<script>
// 全局注册一个叫做my-comp的组件
Vue.component('my-comp', {
template: "<div>组件内容</div>",
});
//这是使用组件的实例,也是my-comp组件的父实例
var app = new Vue({
el:"#app",
})
</script>
这个例子会实际显示出template属性中的字符串以HTML解析后的内容,也就是组件内容四个字。
template属性中的内容,必须包含在HTML标签内才会被渲染,如果写成<div>组件内容</div>123
,运行页面后可以看到Vue的提示:
[Vue warn]: Error compiling template:
text "321" outside root element will be ignored.
1 | <div>组件内容</div>321
| ^^^
found in
---> <MyComp>
<Root>
即321不会被渲染。
局部注册
局部注册是使用Vue实例的components属性进行注册。把上边全局注册的例子改成如下:
<div id="app">
<my-comp></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div>组件内容</div>"
}
}
})
</script>
只要是一个带有template属性的对象,就可以注册为一个组件,后边还会学到许多其他的属性。
is属性强制挂载组件
有些HTML标签,其中默认不会出现其他的元素,比如table内部。
<div id="app">
<table>
<tbody>
<tr>
<td>1</td>
</tr>
<my-comp></my-comp>
<tr>
<td>2</td>
</tr>
</tbody>
</table>
</div>
如果是普通渲染,会发现组件被自动移动到了表格外部,如果想让组件强制显示,可以使用is属性:
<div id="app">
<table>
<tbody>
<tr>
<td>1</td>
</tr>
<tr>
<td is="my-comp"></td>
</tr>
<tr>
<td>2</td>
</tr>
</tbody>
</table>
</div>
此时这个td标签,会被替换成组件的div标签。这就是强制渲染。像ol,ul,select元素都有这方面的限制。
组件的其他属性
除了template之外,组件中也可以使用data、computed、methods等属性,除了data之外都和原来的一样。
data属性则必须是一个匿名函数,返回一个对象,对象中包含属性名称和值即可。
<div id="app">
<my-comp></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><p>{{message}}</p><input type='text' v-model='message'><p><button @click='alertMessage'>alert</button></p></div>",
data:function () {
return {
message:"组件的message属性",
count: 0
}
},
methods:{
alertMessage:function() {
this.count++;
console.log(this.message + this.count);
}
}
}
}
})
</script>
data返回的对象中,如果引用到了外部对象,则会同时修改对象的属性的值。这点和普通的Vue实例是一样的。
这里不要受到返回一个对象的干扰,在组件中,依然可以使用this.属性名来找到对应的data属性。只是形式的变化,用法没有发生变化。
父组件向子组件传递数据
为什么要有组件,主要是为了可以方便的复用。想象一下这个场景:在一个聊天页面中,很多地方都需要显示用户的名称。
如果不使用组件,而是使用Vue实例控制每个小区域,那么这些组件都要互相去找用户使用的名称。
如果使用组件,让组件进行父子组件之间的通信,只要给父组件传递数据,数据就自动会被放到子组件中更新和处理,就会非常方便,也利于解耦。
props
属性用于在组件中声明需要从父组件中接收的数据,值可以有两种,一种是字符串数组代表的各个变量名称,一种是对象。
先来看一个字符串数组的例子
<div id="app">
<my-comp message="From Father Vue instance"></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><p>{{message}}</p></div>",
props: ['message'],
data:function () {
return {
count: 0
}
}
}
}
})
</script>
props属性中的字符串数据中的每一个元素实际上是属性名称,就和data返回的count属性一样可以在子组件的作用域内使用。二者的不同是data返回的属性只能在子组件作用域内使用,而props的值来自于父级。要传递多个
如果把父组件的传递的内容绑定上属性,在父组件内就可以控制子组件的数据了:
<div id="app">
<my-comp :message="mess"></my-comp>
<input type="text" v-model="mess">
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><p>{{message}}</p><p>{{message2}}</p></div>",
props: ['message'],
data:function () {
return {
count: 0
}
}
}
},
data:{
mess: ""
}
})
</script>
这个例子是把父组件传递给子组件的数据绑定给父组件的input框,输入什么,子组件就显示什么。
唯一要注意的是如果在HTML属性名中使用了减号,props中的属性名称要写成驼峰:
<div id="app">
<my-comp :message-new="mess"></my-comp>
<input type="text" v-model="mess">
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><p>{{messageNew}}</p></div>",
props: ['messageNew'],
data:function () {
return {
count: 0
}
}
}
},
data:{
mess: ""
}
})
</script>
如果props里写成了message-new
是没有作用的。
使用v-bind的好处是,可以传递除了字符串以外的值。如果直接写死,传递的都是字符串。使用v-bind可以传递对象或者其他类型的数据:
<div id="app">
<my-comp :message1="mess" message2="[1,2,3,4]"></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><p>message1的长度是:{{message1.length}}</p><p>message2的长度是:{{message2.length}}</p></div>",
props: ['message1','message2'],
data:function () {
return {
count: 0
}
}
}
},
data:{
mess: [1,2,3,4]
}
})
</script>
可以看到message1的长度是4,即数组的元素数,而message2的长度是9,是字符串的长度。
这里只需要在<my-comp :message1="mess" message2="[1,2,3,4]"></my-comp>
中,将message2
前边加上一个:
或者v-bind:
指令,传给子组件的就是js表达式解析出的数组而不是字符串了。
单向数据流
为什么要提到单向数据流,因为在Vue 1.x中,子组件修改props属性会连带修改父组件的数据。在Vue 2.x中就不行了,这是避免子组件污染父组件。看这个例子:
<div id="app">
<my-comp :message="mess"></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><p>message是:{{message}}</p><button @click='modify'>修改子组件数据</button></div>",
props: ['message'],
data:function () {
return {
count: 0
}
},
methods:{
modify:function () {
this.message++;
}
}
}
},
data:{
mess: 100
}
})
</script>
父组件传给子组件一个数据是100,子组件用按钮来直接将其自增。每点一次按钮,控制台内会有Vue的警告:
vue.js:634 [Vue warn]: 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: "message"
found in
---> <MyComp>
<Root>
这里会警告不要直接修改父组件的对应数据,实际上也没能修改成功,在控制台里查看app.mess
依然是100。
但是这样很不好,父子组件的数据其实没同步。Vue也给出了解决方案,就是在data里创建一个自己的局部变量用来接受初始值,或者使用计算属性基于传进来的值计算出新的值。
例子可以修改成这样:
<div id="app">
<my-comp :message="mess"></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><p>initCount是:{{initCount}}</p><p>计算后的是{{computedCount}}</p><button @click='modify'>修改子组件数据</button></div>",
props: ['message'],
data:function () {
return {
initCount: this.message
}
},
methods:{
modify:function () {
this.initCount++;
}
},
computed:{
computedCount: function () {
return this.initCount + 100000;
}
}
}
},
data:{
mess: 100
}
})
</script>
不过要注意的是,如果传递了一个对象比如数组,由于JS传递的是引用,所以依然会被修改:
<div id="app">
<my-comp :message="mess"></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div><p>initCount是:{{initCount}}</p><p>计算后的是{{computedCount}}</p><button @click='modify'>修改子组件数据</button></div>",
props: ['message'],
data:function () {
return {
initCount: this.message
}
},
methods:{
modify:function () {
this.initCount.push(666);
}
},
computed:{
computedCount: function () {
return this.initCount.length;
}
}
}
},
data:{
mess: [1, 2, 3, 4, 5]
}
})
</script>
每次点击按钮,查看父组件的mess属性,会发现也一并发生了变化。
因此子组件最好不要直接修改父组件的数据,尤其父组件的数据还有其他用途的时候。
props传递对象
当传入的数据需要验证的时候,就可以使用对象传递,并在每个属性名后跟上需要验证的类型,有特定的写法,看几个例子:
<div id="app">
<p>传给子组件的是{{message}}</p>
<my-comp :message6="message"></my-comp>
</div>
<script>
var app = new Vue({
el: "#app",
components: {
"my-comp": {
template: "<div>{{message6}}</div>",
props: {
// 传入Number类型的值
message1:Number,
//String和Number类型
message2:[String,Number],
//布尔类型,默认是true
message3:{
type:Boolean,
default: true
},
//Number类型的必传值
message4:{
type: Number,
required: true
},
//数组或者对象类型,必须用一个函数来返回
message5:{
type:Array,
default: function () {
return []
}
},
//自定义验证器
message6:{
validator:function (value) {
return value.length >= 5 && value instanceof Array;
}
}
},
}
},
data: {
message: [1, 3, 4, 5,5]
}
})
</script>
如果类型不匹配,在控制台里可以看到警告。
type支持的类型有:String,Number,Boolean,Object,Array,Function。type也可以传入一个自定义验证器,用instanceof来验证:
message5:{
type:function (obj) {
return obj instanceof Function;
},
default: function () {
return []
}
},