1. 组件嵌套
1.1 组件的嵌套使用
之前有说过,Vue组件跟Vue实例是一样的,因此在Vue中一个组件中也可以定义并使用自己的局部组件,这就是组件的嵌套使用
例如:示例代码如下:
<div id="app">
<!-- 3. 使用组件 -->
<my-component></my-component>
</div>
<script>
// 子组件选项
let sonComponent = {
template:`
<span>我是子组件</span>
`,
}
// 父组件选项对象
let MyComponent = {
template: `
<div>
<h2>我是父组件</h2>
<my-son></my-son>
<my-son />
</div>
`,
// 将子组件定义为父组件的局部组件
components: {
mySon: sonComponent
}
}
// 2. 注册局部组件
const vm = new Vue({
el:"#app",
components: {
"MyComponent": MyComponent
}
})
</script>
显示结果:
通过示例我们就了解到,组件可以嵌套使用,那么我们就不得不思考一个问题,他们各自之间的数据关系如何?能否相互使用对方的数据呢?
1.2 组件间的数据关系
组件实例的作用域是孤立的。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。
示例代码如下
<div id="app">
<!-- 3. 使用组件 -->
<my-component></my-component>
</div>
<script>
// 子组件选项
let sonComponent = {
template:`
<span>我是子组件{{msg}}</span>
`,
}
// 父组件选项对象
let MyComponent = {
template: `
<div>
<h2>我是父组件</h2>
<h3>父组件中使用子组件</h3>
<my-son></my-son>
<my-son />
<h3>父组件中使用父组件数据</h3>
{{ msg }}
</div>
`,
data(){
return {
msg: "哈哈,我是父组件数据"
}
},
// 将子组件定义为父组件的局部组件
components: {
mySon: sonComponent
}
}
// 2. 注册局部组件
const vm = new Vue({
el:"#app",
components: {
"MyComponent": MyComponent
}
})
</script>
显示结果:
如果在子组件中强行使用父组件的数据 ,就会报错,
那么子组件如何才能获取父组件的数据呢?
2. props 属性
父组件可以使用 props 把数据传给子组件。
2.1 props基本使用
父组件在使用子组件时, 可以将父组件的数据绑定到使用子组件的自定义标签上,
子组件在选项中添加一个props属性来接收数据
示例代码如下:
<div id="app">
<!-- 使用组件 -->
<my-component></my-component>
</div>
<!-- 子组件模板 -->
<template id="son">
<div>
<table border="1">
<tr colspan="3">子组件数据</tr>
<tr>
<td>my name</td>
<td>{{ myName }}</td>
</tr>
<tr>
<td>my age</td>
<td>{{ myAge }}</td>
</tr>
</table>
</div>
</template>
<!-- 父组件模板 -->
<template id="MyComponent">
<div>
<h2>显示父组件传给子组件的数据</h2>
<!-- 通过两个组件的契合点,父组件通过自定义属性将数据传递给子组件 -->
<!-- 在属性传输数据的时候使用连字符语法 -->
<my-son :my-name="name" :my-age="age"></my-son>
</div>
</template>
<script>
// 子组件选项
let sonComponent = {
// 子组件通过props接受数据并使用
// 数组里放父组件中自定义属性的名字
// props 里面使用驼峰写法
props:["myName","myAge"],
template:`#son`,
}
// 父组件选项对象
let MyComponent = {
template: `#MyComponent`,
data(){
return {
name:"wuwei",
age:18
}
},
// 将子组件定义为父组件的局部组件
components: {
mySon: sonComponent
}
}
// 实例中注册组件
const vm = new Vue({
el:"#app",
components: {
"MyComponent": MyComponent
}
})
</script>
显示结果:
2.2 数据传递分类
都知道在JS中数据分为两大类,基本数据类型和引用数据类型
因此在父组件向子组件传递数据时也分为两种:为传值和传引用
传值:String Number Boolean
传引用: Array Object
那么接下来我们好好研究研究两者的不同
2.2.1 传递的是基本类型
基本类型,顾名思义,就是传递JS基本数据类型的数据
示例代码如下:
<div id="app">
<conter :count="3"></conter>
<conter :count="4"></conter>
</div>
<script>
var conter = {
props: ['count'],
template:'<div @click="handleClick">{{count}}</div>',
methods:{
handleClick(){
// console.log(this.count)
this.count++
}
}
}
var app = new Vue({
el:'#app',
components:{
conter
}
})
</script>
其实这个时候我们发现在使用子组件传递数据时并没有使用父组件data中的数据,但仍然使用了v-bind
动态绑定指令, Why 为什么呢? 不用v-bind
指令就不能传递数据了吗?
答案当然不是啦, 之前有讲过, 属性如果不使用v-bind
指令绑定,那么属性值将是字符串,如果使用v-bind
指令绑定, 属性值虽然还是引号,但是引号中确实JavaScript表达式
因此上面的示例中我们希望的是传递数据过去,那么引号中的3 只有在JavaScript表达式中才表示数字的字面量, 因此需要v-bind
指令
我们也可以尝试一下,不是用v-bind的情况
<div id="app">
<!-- 不使用v-bind 指令-->
<conter count="3"></conter>
<conter count="4"></conter>
</div>
<script>
var conter = {
props: ['count'],
template:'<div @click="handleClick">{{count}}</div>',
methods:{
handleClick(){
// console.log(this.count)
this.count++
}
}
}
var app = new Vue({
el:'#app',
components:{
conter
}
})
</script>
显示结果:
此时通过两个案例的对比,我们就可以发现, 使用和不使用v-bind
指令的区别
也就是说不使用指令, 传递给子组件的数据将永远是字符串, 不管你写成什么样子
使用v-bind
指令, 属性值引号中的内容将变成表达式,那么你就可以传递任何JS数据类型的数据
这既是Vue官网中关于props 静态传输和动态传输.
解析来我们在来看看父组件给字符串传递引用值的情况
2.2.2 传递的是引用类型的值
传引用就是传递引用类型的数据,
其实我们最想关注的是,传递引用类型的数据是数据的拷贝,还是内存地址的拷贝
因为这涉及到在子组件中是否可以通过props
修改父组件中的数据
示例代码如下:
<div id="app">
<child1 :aa="obj"></child1>
<child2 :bb="obj"></child2>
</div>
<script>
var child1 = {
props:['aa'],
template:`<div>{{aa}}</div>`,
mounted(){
console.log(this.aa)
}
}
var child2 = {
props:['bb'],
template:`
<div>
<div>{{this.bb}}</div>
<button @click="handleClick">点击</button>
</div>
`,
methods: {
handleClick(){
this.bb.name="haha"
}
}
}
new Vue({
el:'#app',
data: {
obj:{
name: 'wuwei'
}
},
components: {
child1,child2
}
})
</script>
通过示例我们发现父组件向两个不同的子组件传递了同一个引用数据类型,
两个子组件都拿到了相同的数据,也显示相同的内容,
可是在child2
子组件中定义了一个按钮用于改变child2
这个子组件通过props
从父组件获取过来的数据
var child2 = {
props:['bb'],
template:`
<div>
<div>{{this.bb}}</div>
<button @click="handleClick">点击</button>
</div>
`,
methods: {
handleClick(){
this.bb.name="haha"
}
}
}
点击后显示的结果:
通过示例我们就了解到,父组件向子组件传递数据是,子组件通过props
属性接受,
但请不要随意的更改props
属性中的数据, 因为可能会有你意向不到的问题,
那么怎么解决这类问题呢, 接着往下看.
2.3 单向数据流
vue默认是单向数据流,所谓的单向数据流,就是数据传递是单向的
既然父组件将数据传递给了子组件,那么子组件中如果对于数据进行改变就有可能影响其他使用这数据的组件
注意:
这个只限传递基本数据类型的值,传递引用数据类型的问题上例中已经处理过
示例代码如下:
<div id="app">
<!-- 使用组件 -->
<my-component></my-component>
</div>
<!-- 子组件模板 -->
<template id="son">
<table border="1">
<tr>
<th colspan="3">子组件数据</th>
</tr>
<tr>
<td>my name</td>
<td>{{ myName }}</td>
<td>
<input type="text" v-model="myName">
</td>
</tr>
<tr>
<td>my age</td>
<td>{{ myAge }}</td>
<td>
<input type="text" v-model="myAge">
</td>
</tr>
</table>
</template>
<!-- 父组件模板 -->
<template id="MyComponent">
<div>
<table border="1">
<tr>
<th colspan="3">父组件数据</th>
</tr>
<tr>
<td>name</td>
<td>{{ name }}</td>
<td>
<input type="text" v-model="name">
</td>
</tr>
<tr>
<td>age</td>
<td>{{ age }}</td>
<td>
<input type="text" v-model="age">
</td>
</tr>
</table>
<!-- 通过两个组件的契合点,父组件通过自定义属性将数据传递给子组件 -->
<!-- 在属性传输数据的时候使用连字符语法 -->
<my-son :my-name="name" :my-age="age"></my-son>
</div>
</template>
<script>
// 子组件选项
let sonComponent = {
// 子组件通过props接受数据并使用
// 数组里放父组件中自定义属性的名字
// props 里面使用驼峰写法
props:["myName","myAge"],
template:`#son`,
}
// 父组件选项对象
let MyComponent = {
template: `#MyComponent`,
data(){
return {
name:"wuwei",
age:18
}
},
// 将子组件定义为父组件的局部组件
components: {
mySon: sonComponent
}
}
// 实例中注册组件
const vm = new Vue({
el:"#app",
components: {
"MyComponent": MyComponent
}
})
</script>
修改父组件数据:
修改子组件中数据,
通过示例测试,我们发现
- 当父组件数据更新时,传递给子组件的props也会更新,
- 当子组件修改了props的数据, 父组件不会有任何变化(基本数据类型报错,引用类型直接替换引用地址也报错, 但是直接修改属性不报错,但是不建议这么用)
这就是所谓的单项下行数据流
这么做的目的是为了防止子组件无意修改了父组件的状态
总结:
- 子组件不能(也不要)直接修改父组件传递过来的值,否则就会报错
- 修改父组件传递过来引入类型的属性不会报错,但是会导致,其他使用了这个数据的子组件里面的数据也会发生变化(这也是很大的entity)
2.4 prop响应式
通过上一个例子,我们发现父组件的数据发生变化, 子组件也会随着发生变化, 也就是,父组件在使用子组件时,给子组件prop
传递的数组无论何时发生改变, 在子组件内任何使用该prop
的地方都会发生更新
这就是props
响应式
示例代码如下:
<div id="app">
<!-- 使用组件 -->
<my-component></my-component>
</div>
<!-- 子组件模板 -->
<template id="son">
<div>
被点击了{{ num }}次
</div>
</template>
<!-- 父组件模板 -->
<template id="MyComponent">
<div>
<my-son :num="num"></my-son>
<!--
每次点击更改父组件data中的数据, 子组件prop关联的数据也会跟随发生改变
-->
<button @click="handleClick">点击+1</button>
</div>
</template>
<script>
// 子组件选项
let sonComponent = {
// 子组件通过props接受数据并使用
props:["num"],
template:`#son`,
}
// 父组件选项对象
let MyComponent = {
template: `#MyComponent`,
data(){
return {
num: 1
}
},
// 将子组件定义为父组件的局部组件
components: {
mySon: sonComponent
},
methods:{
handleClick(){
this.num++
}
}
}
// 实例中注册组件
const vm = new Vue({
el:"#app",
components: {
"MyComponent": MyComponent
}
})
</script>
点击结果
3. 如何修改数据
上一小节学习我们已经知道了:
- 父组件可以向子组件传递基本数据类型和引用数据类型的数据
- 子组件如果直接修改
props
中的基本数据类型数据和引用类型数据的内存地址就报错 - 如果修改引用类型数据里的值虽然不会报错,但是这样非常不友好
- 因此不推荐在子组件直接修改
props
数据中的数据
可是有的是需要修改传递过来的数据,那么如何解决这样的需求呢?
其实我们可以将父组件传递过来的值赋值给组件自己的数据,这样我们就可以修改自己的数据了
3.1 子组件修改基本数据
示例代码如下:
var conter = {
props: ['count'],
data(){
return {
num: this.count
}
},
template:'<div @click="handleClick">{{num}}</div>',
methods:{
handleClick(){
// console.log(this.num)
this.num++
}
}
}
3.2 子组件修改引用类型
如果父组件向子组件传递的引用数据类型的数据,我们按照基本数据类型那种直接将数据赋值给子组件自己的数据就行不同了,因为赋值给自己的数据也是内存地址的赋值,因此直接赋值,修改还是会改变父组件的数据. 那么要怎么办嗯?
其实我们可以在子组件内的data属性中浅拷贝一份父组件传递过来的引用数据类型,子组件如果想修改自己的显示,就修改自己data中的数据
将上面例子中child2
子组件修改为如下代码
var child2 = {
props:['bb'],
// 在自己的data数据中浅拷贝一份父组件传过来的引用数据
data(){
return {
cc: JSON.parse(JSON.stringify(this.bb))
}
},
// 在自己的视图中显示自己的数据
template:`
<div>
<div>{{cc}}</div>
<button @click="handleClick">点击</button>
</div>
`,
// 如果有修改就修改自己data中的数据
methods: {
handleClick(){
this.cc.name="haha"
}
}
}
点击结果:
这样就可以在子组件中使用自己的数据,修改自己的数据, 进而解决问题
4. Prop 验证
我们可以为组件的 prop 指定验证规则。如果传入的数据不符合要求,Vue 会发出警告。这对于开发给他人使用的组件非常有用。
要指定验证规则,需要用对象的形式来定义 prop,而不能用字符串数组:
4.1 验证父组件传递过来的数据类型
// 子组件选项
let sonComponent = {
// props 验证数据类型
props:{
myName:String,
myAge: Number
},
template:`#son`,
}
如果数据类型验证通过, 则正常运行,如果验证不通过,则报错
4.2 允许数据传递多种数据类型
如果一个数据可以接受多种数据类型,则使用数组将所有允许的类型罗列在一起
// 子组件选项
let sonComponent = {
// props 验证数据类型
props:{
myName:String,
myAge: [Number,String]
},
template:`#son`,
}
这样myAge 在接受的数据是Number 类型或String 都不会报错
注意:
这两种验证数据的类型,只验证父组件传递过来数据的类型是否符合, 并不关心用户是否传数据过来, 不传也不会报错,
那么type
属性都可以指定哪些类型呢?
4.3 type类型
验证类型的type属性的值就是原生的构造器(构造函数)
- String
- Number
- Boolean
- Function
- Object
- Array
- Symbol
4.4 必须传递的数据
有的时候我们需要指定一些数据为必须传递的, 如果不传递就会报错, 这个时候,数据的只是一个对象
对象就是对于props传递数据的配置对象
验证的配置对象中
- type: 验证数据类型
- required: 验证数据是否为必须传递,true,是必须传递,不传就报错
// 子组件选项
let sonComponent = {
props:{
myName:String,
myAge: {
type:Number, // type为验证数据类型
required: true // required为数据是否必须传递,true是false 否
}
},
template:`#son`,
}
4.5 指定默认值
如果父组件未传递数据过来,则使用默认值
注意:
配置对象中required 必传选项 和 default 默认值选项,不能同时使用
默认是就是父组件在未传递数据的时候使用, 如果你还需要父组件必须传递数据, 默认值就不没有意义了吗
// 子组件选项
let sonComponent = {
props:{
myName:{
type: String, // 验证类型
default: '默认姓名' // 默认值
},
myAge: {
type:Number, // type为验证数据类型
required: true // required为数据是否必须传递,true是false 否
}
},
template:`#son`,
}
如果传递过来的是是引用类型, 那么在定义默认值的时候必须是一个函数,函数返回引用类型的数据
为什么是一个函数就不用在说吧,和组件数据data
是函数同一个意思, 保存每次传递都是第一无二的数据
示例代码如下
let sonComponent = {
props:{
myName:{
type: String, // 验证类型
default: '默认姓名' // 可以赋予默认值,如果父组件没有传值,使用默认值
},
myAge: {
type:Number, // type为验证数据类型
required: true // required为数据是否必须传递,true是false 否
// 此属性表示必须传值,但是不能跟default同用
},
myLike:{
type:Array, // 限定的数据类型是引用类型的数组
default: function(){ //如果传递过来的是一个引用类型的值,默认值是函数
return []
}
}
},
template:`#son`,
}
4.6 自定义验证规则
自定义验证是一个函数,返回true则验证通过,返回false则验证不通过
示例代码如下:
let sonComponent = {
// 子组件通过props接受数据并使用
// 数组里放父组件中自定义属性的名字
// props 里面使用驼峰写法
props:{
myName:{
type: String, // 验证类型
default: '默认姓名' // 默认值
},
myAge: {
validator:function(value){ // 自定义验证器
return value > 16 // 返回true 验证通过, 返回false 验证不通过报错
}
}
},
template:`#son`,
}
注意
props 会在组件实例创建之前进行校验,
所以在
default
或validator
函数里,诸如data
、computed
或methods
等实例属性都还无法使用。
props
特性就是父组件通过属性传值,子组件有对应的props
接受,那么这个属性不会出现在网页的标签属性上
5. 非Props 特性
5.1 非prop 属性的了解
尽管为组件定义明确的 props
是推荐的传参方式,组件的作者却并不总能预见到组件被使用的场景。
所以,组件可以接收任意传入的特性,这些特性都会被添加到组件的根元素上。
简单的说就是父组件可以在使用子组件的时候给子组件传递n多的属性, 只有子组件使用props
接受的才会成为子组件的参数, 没有通过props
声明的属性就是非props, 这些属性会自动添加为子组件根标签的属性
通过示例了解:
<div id="app">
<!-- 使用组件 -->
<my-component></my-component>
</div>
<!-- 子组件模板 -->
<template id="son">
<div>
{{msg}}
</div>
</template>
<!-- 父组件模板 -->
<template id="MyComponent">
<div>
<!-- msg因为子组件prop定义了所以会成为子组件的参数 -->
<!-- title属性因为子组件没有在props声明,所以是非prop, 会自动添加为 子组件根标签的属性-->
<my-son :msg="msg" :title="title"></my-son>
</div>
</template>
<script>
// 子组件选项
let sonComponent = {
// 子组件通过props接受数据并使用
props:["msg"],
template:`#son`,
}
// 父组件选项对象
let MyComponent = {
template: `#MyComponent`,
data(){
return {
msg:"我想说一句话",
title:"标题"
}
},
// 将子组件定义为父组件的局部组件
components: {
mySon: sonComponent
}
}
// 实例中注册组件
const vm = new Vue({
el:"#app",
components: {
"MyComponent": MyComponent
}
})
</script>
显示结果
在总结一下:
非props就是子组件并没有明确props
来接受父组件的传值,那么在网页中子组件传值的属性将会成为标签的私有属性
上面一个例子,了解到如果父组件给子组件传递非prop属性,会自动成为子组件模板中根标签的标签属性, 适用于任何HTML属性或特性,
可是如果父组件传递的非prop属性与子组件的根标签的属性重名了怎么办呢?
会发生两种情况, 一,替换,二合并, 先来看看替换的情况
5.2 非prop属性的替换
如果父组件传递的prop属性与子组件的根标签的属性重名,大部分情况会覆盖子组件根标签上的同名属性. 即替换效果
示例代码如下:
<div id="app">
<!-- 使用组件 -->
<my-component></my-component>
</div>
<!-- 子组件模板 -->
<template id="son">
<!-- 子组件根标签的type属性值text 被父组件传递过了的非prop type属性值 radio替换了 -->
<input type="text" />
</template>
<!-- 父组件模板 -->
<template id="MyComponent">
<div>
<!-- 父组件向子组件传递非prop属性 type 值为radio -->
<my-son :type="type"></my-son>
</div>
</template>
<script>
// 子组件选项
let sonComponent = {
// 子组件通过props接受数据并使用
props:["num"],
template:`#son`,
}
// 父组件选项对象
let MyComponent = {
template: `#MyComponent`,
data(){
return {
type: "radio"
}
},
// 将子组件定义为父组件的局部组件
components: {
mySon: sonComponent
}
}
// 实例中注册组件
const vm = new Vue({
el:"#app",
components: {
"MyComponent": MyComponent
}
})
</script>
显示结果
通过示例.发现: 子组件根标签的type
属性值text
被父组件传递过了的非props type
属性值 radio
替换了
这样的效果非常不好,可能会破坏子组件. 所以一定要注意.
5.3 非prop属性的合并
当然了大部分会发生替换, 但是也有两个特殊的属性,会发生合并的效果,这两个属性就是class
与style
属性
示例:
<div id="app">
<!-- 使用组件 -->
<my-component></my-component>
</div>
<!-- 子组件模板 -->
<template id="son">
<div class="box" style="background:skyblue;">
我是子组件
</div>
</template>
<!-- 父组件模板 -->
<template id="MyComponent">
<div>
<my-son :class="className" :style="style"></my-son>
</div>
</template>
<script>
// 子组件选项
let sonComponent = {
// 子组件通过props接受数据并使用
props:["num"],
template:`#son`,
}
// 父组件选项对象
let MyComponent = {
template: `#MyComponent`,
data(){
return {
className: "wrap",
style: {
width: '100px',
height: '100px'
}
}
},
// 将子组件定义为父组件的局部组件
components: {
mySon: sonComponent
}
}
// 实例中注册组件
const vm = new Vue({
el:"#app",
components: {
"MyComponent": MyComponent
}
})
</script>
示例中,父组件通过非prop传递过去的class 和style属性 与子组件根标签的class 和 style 属性发生合并.
6. 遍历传值,
利用v-for
循环指令. 多次使用组件.
在配合使用 v-bind
指令将数据传到每个组件中:
<div id="app">
<ol>
<!--
现在我们为每个 todo-item 传递了数据,即其内容可以是动态的。
我们也需要为每个组件提供一个“key”,稍后再
作详细解释。
-->
<todo-item
v-for="item in groceryList"
v-bind:text="item.text"
v-bind:key="item.id">
</todo-item>
</ol>
</div>
<script>
// 定义子组件
let todoItem = {
props:["text"],
template:`
<li>{{text}}</li>
`
}
// Vue实例
var vm = new Vue({
el: '#app',
data: {
groceryList: [
{ id: 0, text: '蔬菜' },
{ id: 1, text: '奶酪' },
{ id: 2, text: '随便其它什么人吃的东西' }
]
},
components:{
todoItem
}
})
</script>
显示结果