写在前面
最近看尚硅谷的视频有点懵逼了,直接跳过了组件来讲用CLI搭建项目了,找到了coderwhy老师的vue教学视频,好好学了一下组件。
组件的创建及使用步骤
- 创建组件构造器对象 Vue.extend()
- 注册组件
- 使用组件 同样也是一个例子,先在js中创建一个组件构造器和注册该组件(注意到template的字符串是模板字符串的形式,而且template的内容最外部必须至少要有一个包裹元素):
//创建组件构造器对象 Vue.extend()
const cpnC = Vue.extend({
template: `
<div>
<h2>我是标题</h2>
<p>我是内容</p>
</div>
`
})
//注册组件 Vue.component(全局注册)
Vue.component('my-cpn', cpnC)
new Vue({
el: '#app',
})
之后就可以在HTML中使用该组件了,这样看来组件应该算是HTML的语法糖,增加了复用率,减少了广大CV开发者的工作。
<body>
<div id="app">
<my-cpn></my-cpn>
</div>
</body>
全局组件和局部组件
刚刚的例子可以看到是使用Vue的函数对象方法注册组件,这样注册的组件是全局组件,该组件在任何Vue实例中都可以被解析。
另外一种注册方式是局部注册,该注册方式只能在指定Vue实例的解析元素中使用:
const cpnC = Vue.extend({
template: `
<div>
<h2>我是标题</h2>
<p>我是内容</p>
</div>
`
new Vue({
el: '#demo',
components: {
cpn: cpnC,
}
父子组件
组件内部是也可以注册组件的,这样在父组件内部的template也可以使用已经注册了的子组件。真·奥义·套娃:
//组件1
const cpnC1 = Vue.extend({
template: `
<div>
<h2>组件1</h2>
<p>组件1的内容</p>
</div>
`
})
//组件2,里面注册了组件1
const cpnC2 = Vue.extend({
template: `
<div>
<h2>组件2</h2>
<p>组件2的内容</p>
<cpnc1></cpnc1>
</div>
`,
components: {
cpnc1: cpnC1,
}
})
//el为#app的Vue实例,注册了组件2
new Vue({
el: '#app',
components: {
cpnc2: cpnC2,
}
})
在div#app中使用组件2和组件1(注意html标签不支持驼峰命名法,大写字母会被识别成小写字母):
<div id="app">
<cpnc2></cpnc2>
<cpnc1></cpnc1>
</div>
在这里由于组件1并没有在Vue实例中注册,也没有在全局中注册,因此无法被解析。其实该Vue实例也可以看成是一个根组件(root),它就是无尽套娃的起点。
注册组件的语法糖
前面注册组件的步骤还是有些繁琐的,现在已经很少能看到这种注册方式了,语法糖的写法如下:
//注册组件 Vue.component(全局注册)
Vue.component('my-cpn', {
template: `
<div>
<h2>我是标题</h2>
<p>我是内容</p>
</div>
`
})
语法糖的写法省略了组件构造器的创建步骤,直接来了一个二合一,但是底层还是调用Vue.extend()实现的功能。
组件模板(template)抽离
注册组件的语法糖写法确实写起来轻松了,但是把html内容都塞到一个地方看起来有些混乱,尤其是存在父子组件嵌套的关系下。
我们可以在html中用template标签写我们组件模板的内容,加上id进行对应:
<template id="cpn">
<div>
<h2>我是标题</h2>
<p>我是内容</p>
</div>
</template>
//全局注册
Vue.component('cpn', {
template: '#cpn'
})
//局部注册
new Vue({
el: '#demo',
components: {
cpn: {
template: '#cpn'
}
}
})
组件的data函数
上面这些写法写出来的html元素虽然能够重复利用,但是都是静态的,不符合Vue的响应式特点,我们能不能在组件中设置响应式数据呢?yes!我们可以在组件中配置data函数(注意!不同于实例中的配置,不是一个对象嗷。),该函数的返回值是一个对象,以及methods方法等。如下注册了一个加减器的组件并在html中使用了该组件:
new Vue({
el: '#demo',
components: {
cpn: {
template: '#cpn',
data() {
return {
counter: 0
}
},
methods: {
inc() {
this.counter++
},
dec() {
this.counter--
}
}
}
}
})
<template id="cpn">
<div>
<h2>当前计数:{{counter}}</h2>
<button @click='inc'>+</button>
<button @click='dec'>-</button>
</div>
</template>
<div id="demo">
<cpn></cpn>
</div>
那么为神魔这个data属性要设置成函数呢?我们看这样的场景:这个加减器我们需要重复使用,那么多个加减器中的counter属性是不是会共享值呢?我们来试试:
每个加减器组件的counter值都是独立不影响的!这是因为每次使用组件会调用data函数,每一次调用返回的新对象都是存储在不同内存空间之中的,相互之间不会产生影响。
父子组件通信
- 父组件通过props向子组件传递信息
- 子组件通过自定义事件向父组件传递信息
- 父组件访问子组件: refs
- 子组件访问父组件: root
父传子
在子组件中配置props(props可以使多种形式,以数组为例),props中的元素是被传递变量在子组件中的属性名的字符串,子组件通过v-bind绑定被传递变量,之后在子组件中就能通过vue的语法进行操作被传递变量:
<body>
<template id="cpn1">
<div>
<h2>{{cmsg}}</h2>
<h2 v-for="(item,index) in cmovies" :key='item'>{{item}}</h2>
</div>
</template>
<div id="demo">
<cpn1 :cmsg='message' :cmovies='movies'></cpn1>
</div>
</body>
<script>
const cpn1 = {
template: '#cpn1',
props: ['cmsg', 'cmovies']
}
//注册组件 Vue.component
new Vue({
el: '#demo',
data: {
message: 'hello',
movies: ['Batman', 'Superman', 'Wonderwoman']
},
components: {
cpn1
}
})
</script>
当然,props的配置不止于数组,我们也可以配置对象,可以以对象形式列出 prop,这些 property 的名称和值分别是 prop 各自的名称和类型,当然也可以给这些prop提供默认值以及required属性:
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
//提供了默认值的prop
movies:{
type:String,
default:'Hello',
required:true // 在传props时,该prop必须要被传入
}
msgs:{
type:Array,
default(){
return []
}, // 当类型指定为数组或对象时,default属性必须是带返回值的函数
required:true // 在传props时,该prop必须要被传入
}
}
子传父
当子组件向父组件传递数据时需要用到自定义事件。自定义事件的使用步骤可以归纳为如下:
- 子组件中的原生事件会向父组件抛出自定义事件(this.$emit('myevent',[**args]))
- 父组件接收到自定义事件(记得别用驼峰命名),调用对应的事件回调函数
- 回调函数可以接收到从子组件传过来的数据(可选)并进行处理 举个例子:
<body>
<!-- 子组件模板 -->
<template id="cpn1">
<div>
<button v-for="(item,index) in categories" @click='btnClick(item)'>{{item.name}}</button>
</div>
</template>
<!-- 父组件模板 -->
<div id="demo">
<cpn1 @my-click='cpnClick'></cpn1>
</div>
</body>
<script>
//子组件
const cpn1 = {
template: '#cpn1',
data() {
return {
categories: [
{ id: 1, name: '手机家电' },
{ id: 2, name: '实用工具' },
{ id: 3, name: '计生用品' },
{ id: 4, name: '运动护具' },
]
}
},
methods: {
btnClick(item) {
this.$emit('my-click', item)
}
}
}
//父组件
new Vue({
el: '#demo',
components: {
cpn1
},
methods: {
cpnClick(item) {
alert(item.name)
}
}
})
</script>
父访子
简单的案例:
<body>
<template id="cpn">
<div></div>
</template>
<div id="demo">
<cpn></cpn>
<cpn ref="cpn2"></cpn>
<button @click='btnClick1'>父组件按钮--操控组件1</button>
<button @click='btnClick2'>父组件按钮--操控组件2</button>
</div>
</body>
<script>
//注册组件 Vue.component
const cpnC = {
template: '#cpn',
methods: {
showMessage() {
alert('子组件的显示函数!')
}
}
}
new Vue({
el: '#demo',
components: {
cpn: cpnC,
},
methods: {
btnClick1() {
this.$children[0].showMessage()
},
btnClick2() {
this.$refs.cpn2.showMessage()
}
}
})
</script>
子访父
和父访子的使用差不多,在子组件里面用 this.root 可以访问到根组件对象。但实际开发中不推荐用这个功能,因为子组件如果依赖父组件,则该子组件的复用性会变差。
插槽(slot)
何为插槽?插槽可以扩展设备的功能:比如笔记本的USB插槽、网线插槽。Vue的插槽同样用来强化组件的扩展性,利用slot,抽取共性,预留不同(设置插槽)。
插槽的基本使用
也是一个小案例:
<body>
<template id="cpn">
<div>
<h2>我是标题</h2>
<p>我是内容</p>
<slot>
<p>我是插槽默认值</p>
</slot>
</div>
</template>
<div id="demo">
<cpn></cpn>
<cpn>
<button>按钮嗷</button>
</cpn>
<cpn>
<button>按钮嗷</button>
<button>按钮嗷</button>
<input type="text" placeholder="文本框">
</cpn>
</div>
<script>
//创建组件构造器对象 Vue.extend()
const cpnC = Vue.extend({
template: '#cpn'
})
//注册组件 Vue.component
new Vue({
el: '#demo',
components: {
cpn: cpnC,
}
})
</script>
</body>
具名插槽
可以看到,一个插槽位置可以插任意个元素,而且可以给插槽一个默认值。当一个组件有多个插槽时,如果我们仍然像上面那么写,则所有插槽都会被替换相同内容,所以我们需要给插槽命名(具名插槽):
<template id="cpn1">
<div>
<slot name="l">
</name=>>
<span>左</span>
</slot>
<slot name='m'>
<span>中</span>
</slot>
<slot name='r'>
<span>右</span>
</slot>
</div>
</template>
<cpn1>
<span slot="l">改了左边</span>
<span slot="m">改了中间</span>
<span slot="r">改了右边</span>
</cpn1>
编译作用域
类似于js中的作用域,在html中使用了vue的数据时,我们也可以通过作用域来确定vue数据的作用范围,假设一对父子组件,在父子组件中都设置了isShow属性,一个为true,一个为false,那么如下的情况,该子组件会不会显示呢:
<!-- 父组件 -->
<div id="demo">
<!-- 父组件作用域 -->
<cpn v-show='isShow'><!-- 子组件作用域 --></cpn>
</div>
这里,由于vshow所处作用域是父组件的作用域,因此isShow的值为true。
作用域插槽
父组件替换插槽的标签,标签内容由子组件提供。v-slot是新语法,Vue3.0之后旧语法(slot-scope)被抛弃了。v-slot在使用时推荐写在template标签下
<!-- 默认插槽-->
<foo v-slot="{ msg }">
{{ msg }}
</foo>
<!--具名插槽 -->
<foo>
<template v-slot:one="{ msg }">
{{ msg }}
</template>
</foo>
动态组件
这部分的内容在掘金上有一些教程,其中这篇文章比较深入分析了动态组件的原理,奈何现在水平不够,不太能看懂,先马着,日后再看深入剖析Vue源码