可以扩展 HTML 元素,封装可重用的代码,是 Vue 的强大功能之一。
一、认识组件
1. 为什么要组件化
- 如果将一个页面中的所有逻辑都放在一起,处理时会非常复杂,不利于之后的维护与扩展
- 如果将页面拆分成独立的组件,由各个组件完成自己独立的功能,整个页面的管理和扩展就会更加容易
2. Vue 组件
- 通过组件,可以扩展 HTML 元素,封装可重用的代码
- 通过组件,可以由独立且可复用的小组件构建大型应用
- 一切应用都可以抽象为一个组件树
- 应该将应用尽可能拆分为小的、独立的、可复用的组件
二、组件的基本使用
1. 基本步骤
2. 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <div> <!--使用组件--> <cpn></cpn> </div>
// 创建组件构造器 const myComponent = Vue.extend({ template: ` <div> <h2>标题</h2> <p>文字</p> </div>` }) // 注册组件 Vue.component("cpn", myComponent); // 创建Vue实例 const app = new Vue({ el: 'div', });
|
3. 创建组件构造器
1 2 3
| const 变量名 = Vue.extend({ template: `组件模板` })
|
- 通过
Vue.extend
创建组件构造器
-
Vue.extend
的写法已经被更加简单的语法糖替代
- 在 template 中填入组件的模板,即显示的 HTML 代码
- 可以通过
``
包裹字符串,它的优点是可换行
4. 注册组件
1
| Vue.component("组件名", 组件构造器);
|
通过 Vue.component
将组件构造器注册为组件,并为它起名
5. 使用组件
- 组件必须放置在 Vue 实例之下,否则将无法生效
- 可以通过组件名进行任意次数的复用
三、全局组件和局部组件
1. 全局组件
全局注册的组件便是全局组件,可以在任意 Vue 实例中使用它。
1 2 3 4 5
| Vue.component("组件名", 组件构造器);
const app = new Vue({ el: 'div', });
|
2. 局部组件
在实例中注册的组件便是局部组件,它只能在对应 Vue 实例中使用。
1 2 3 4 5 6
| const app = new Vue({ el: 'div', components: { 组件名: 组件构造器, }, });
|
四、父组件和子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <div> <father></father> </div>
// 子组件构造器 const son = Vue.extend({ template: ` <div> <p>这是son</p> </div> `, }) // 父组件构造器 const father = Vue.extend({ template: ` <div> <h2>这是father</h2> <son></son> </div> `, // 在父组件构造器中注册子组件 components: { son: son, } }) // 注册父组件 const app = new Vue({ el: 'div', components: { father: father, } });
|
- 组件和组件之间存在层级关系
- 只引入父组件,便能够将子组件和父组件都显示
- 因为子组件只在父组件中注册,因此在 Vue 实例中无法使用
五、语法糖
1. 注册组件
(1) 原版写法
1 2 3 4 5 6 7 8 9 10 11 12
| const 变量名 = Vue.extend({ template: `组件模板` }) // 全局注册 Vue.component("组件名", 组件构造器); // 局部注册 const app = new Vue({ el: 'div', components: { 组件名: 组件构造器, }, });
|
(2) 简写写法
1 2 3 4 5 6 7 8 9 10 11 12 13
| // 全局注册 Vue.component("组件名", { template: `组件模板` }); // 局部注册 const app = new Vue({ el: 'div', components: { 组件名: { template: `组件模板` }, }, });
|
2. 模板的分离写法
(1) script
1 2 3 4 5 6 7 8 9 10 11 12
| <script type="text/x-template" id="template"> <div> <p>这是一句话</p> </div> </script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> const cpn = Vue.extend({ template: `#template`, }) </script>
|
将 HTML 模板放置在 <script type="text/x-template"></script>
之中,为模板设置 id ,并通过 id 匹配模板。
(2) template
1 2 3 4 5 6 7 8 9 10 11 12
| <template id="template"> <div> <p>这是一句话</p> </div> </template>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> const cpn = Vue.extend({ template: `#template`, }) </script>
|
将 HTML 模板放置于 template 中,为 template 设置 id ,并通过 id 匹配模板。
六、组件的 data
1. 组件数据应该如何处理
2. 实例和组件的 data
(1) 实例
1 2 3 4 5 6
| var app = new Vue({ el: 'div', data: { 属性名: '属性值', } })
|
(2) 组件
1 2 3 4 5 6 7 8
| const 变量名 = Vue.extend({ template: `组件模板`, data() { return { 属性名: '属性值', } } })
|
(3) 区别
(4) 原因
使用对象:
1 2 3 4 5 6 7
| var obj = { 属性名: '属性值', };
var obj1 = obj; var obj2 = obj; var obj3 = obj;
|
三个对象将用共用数据,因为它们指向了同一个对象。
使用函数:
1 2 3 4 5 6 7 8 9
| function creatObj() { return { 属性名: '属性值', } }
var obj1 = creatObj(); var obj2 = creatObj(); var obj3 = creatObj();
|
三个对象将分别拥有不同的数据对象,它们不会共用数据,因此每次函数执行后都会返回一个新的对象。
总结:
通过 data 函数,可以使每个组件都能够获得一份独立的数据对象,从而使得组件复用时组件之间不会相互影响。
3. 使组件共用数据
1 2 3 4 5 6 7 8 9 10
| var obj = { 属性名: '属性值', };
const 变量名 = Vue.extend({ template: `组件模板`, data() { return obj } })
|
首先新建对象,然后在组件的 data 中直接返回该对象。如此便能实现所有组件指向同一对象,共用同一份数据的效果。
七、父子组件的通信
1. 为什么需要通信
在实际开发中,页面的数据往往都来自服务器,此时需要与服务器进行通信。
通常做法是在最外层向服务器请求数据,然后从父组件向子组件传递。(而不是每个组件进行一次网络请求)
2. 通信方式
- 父→子:通过 props 向子组件传递数据
- 子→父:通过事件向父组件发送消息
3. 父传子
(1) props
prop 用于在子组件上新增属性,之后在父组件中通过 v-bind
绑定该属性,从而实现数据的传入。
(2) 语法
1 2 3 4 5 6 7 8 9 10
| // 在父组件中绑定属性 <子组件名 :属性="数据属性"></子组件名> // 也可以在父组件中直接给值 <子组件名 属性=数据></子组件名>
// 在构造器中新建属性 { props: 属性列表, }
|
(3) 数组方式
在数组中填入若干个属性名,之后在父组件中依次为每一个数组名做绑定。
语法:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <template id="fatherTemplate"> <div> <h1>父组件说:{{fathersay}}</h1> <h1>父组件说:{{son}}</h1> // 通过v-bind绑定属性,从而传入数据 <son :son="son"></son> </div> </template>
<template id="sonTemplate"> <div> <p>子组件说:{{son}}</p> </div> </template>
const app = new Vue({ el: 'div', components: { // 实例中注册father组件 father: { template: `#fatherTemplate`, data() { return { fathersay: '我是父组件', son: '我是子组件', } }, // father组件中注册son组件 components: { son: { // 通过props新增属性 props: ["son"], template: `#sonTemplate`, } } } } });
|
(4) 对象方式
语法:
1 2 3 4 5
| props: { 属性1: { 可选参数 } }
|
可选参数 type
用于规定传递给属性的数据类型。
当可能得到数据类型有多个时,在数组中填写。
1 2 3
| 属性1: { type: [数据类型1, 数据类型2], }
|
当仅有 type 这一个参数时,还可以简写:
可选参数 default
用于给属性一个默认值。
当类型是对象或数组时,默认值应该为一个返回对应数据的函数。
1 2 3 4 5 6
| 属性1: { type: String, default() { return []; } }
|
可选参数 required
为 true 时,规定属性必须有一个值,没有时便会报错。
1 2 3
| 属性1: { required: true|false, }
|
(5) 驼峰命名法
因为 HTML 中不区分大小写,而 JavaScript 中区分大小写。因此 v-bind
并不能很好地支持驼峰命名法。
如果需要使用驼峰命名法,请在绑定时将大写转换成小写,并添加 -
。例如将 myCase
修改为 my-case
。
4. 子传父
(1) 实现方法
通过自定义事件完成:
- 通过
$emit()
向父组件发送事件
- 通过
v-on
监听事件
(2) 子组件发射事件
通过 $emit()
用于在子组件中向父组件发射事件。
- 用“事件名称”来为自定义事件取名,从而能够在父组件中监听事件
- 可以增加可选参数,这些参数将会传入事件处理方法
(3) 父组件监听事件
在父组件中,通过 v-model
监听自定义事件。
如果需要访问从子组件中发送的参数,可以通过 $event
访问,但最好的方式是通过事件处理函数访问
(4) 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <div> <h1>{{num}}</h1> // 在父组件中监听事件 <but @itemclick="fun"></but> </div>
<template id='but'> <div> // 通过不同的方式向父组件传递不同的参数 <input type="button" value="加一下" @click="butclick(1)"> <input type="button" value="加两下" @click="butclick(2)"> <input type="button" value="加三下" @click="butclick3"> </div> </template>
var app = new Vue({ el: 'div', data: { num: 0, }, methods: { fun(num){ this.num += num } }, components: { but: { template: '#but', methods: { // 向父组件发送事件 butclick(num){ this.$emit('itemclick', num) }, butclick3(){ this.$emit('itemclick', 3) } } } } })
|
八、父子组件的互相访问
1. 访问方式
- 父组件访问子组件:使用 $children 或 $refs
- 子组件访问父组件:使用 $parent
2. $children
(1) 说明
(2) 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <div id="app"> <son></son> <input type="button" @click="$children[0].fun()" value="调用子元素的方法试试"> </div>
var app = new Vue({ el: '#app', components: { son: { template: son, methods: { fun() { alert('子组件喊了一声') } } } } })
|
2. $refs
(1) 语法
1 2 3 4 5
| // 在子组件上做标记 <子组件名 ref="标记"></子组件名>
// 调用子组件 $refs.标记
|
(2) 说明
$refs 为一个对象,持有已注册过 ref 得到所有子组件。通过注册 ref 时给定的标记来访问对象。
(3) 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <div id="app"> <son ref="aaa"></son> <input type="button" @click="$refs.aaa.fun()" value="调用子元素的方法试试"> </div>
var app = new Vue({ el: '#app', components: { son: { template: son, methods: { fun() { alert('子组件喊了一声') } } } } })
|
3. $parent
(1) 说明
开发中不建议使用,因为它会影响组件的独立性,进而影响组件的复用。
(2) 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <div id="app"> <son></son> </div>
<template id="son"> <input type="button" value="试试调用父方法" @click="$parent.fun()"> </template>
var app = new Vue({ el: '#app', methods: { fun() { alert('父组件喊了一声') } }, components: { son: { template: son, } } })
|
4. $root
用于访问根组件,即 Vue 实例。
九、插槽
1. 组件的插槽
组件插槽通过 slot 实现
组件插槽使组件具有更好的扩展性
2. 基本使用
- 在组件的 HTML 模板中增加一个 slot 标签
- 在调用组件时,组件标签中间的内容将会被拿来替换 slot 标签
- 内容可以是任何 HTML 代码,甚至是其它组件,它们都会被用来替换 slot 标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <div id="app"> <son><p>这是由父组件给定的一句话</p></son> </div>
<template id="son"> <div> <h1>这是一个标题</h1> <slot></slot> </div> </template>
var app = new Vue({ el: '#app', components: { son: { template: son, } } })
|
3. 具名插槽
(1) 普通插槽
如果没有为插槽指定名称,则内容会被插入到每一个插槽之中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <div id="app"> <son><span>一句话</span></son> </div>
<template id="son"> <div> <h1>这是一个标题</h1> 这是第一个插槽:<slot></slot> <br> 这是第二个插槽:<slot></slot> <br> 这是第三个插槽:<slot></slot> </div> </template>
var app = new Vue({ el: '#app', components: { son: { template: son, } } })
|
(2) 具名插槽
可以为插槽指定名字,并为内容指定插槽,具体做法是:
通过 name 为插槽指定名字
通过 slot 为内容指定插槽
slot
已被弃用,建议使用 v-slot
设置了具名插槽之后,内容只能被插到对应的插槽之上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <div id="app"> <son> <span slot="1">一句话</span> <span slot="2">二句话</span> <span slot="3">三句话</span> </son> </div>
<template id="son"> <div> <h1>这是一个标题</h1> 这是第一个插槽:<slot name="1"></slot> <br> 这是第二个插槽:<slot name="2"></slot> <br> 这是第三个插槽:<slot name="3"></slot> </div> </template>
var app = new Vue({ el: '#app', components: { son: { template: son, } } })
|
(3) v-slot
v-solt
用于替代被废弃的 slot
需要注意的是,v-slot
只能添加在 <template>
之上。
除非独占默认插槽的缩写语法
1 2 3 4 5 6 7 8 9
| // 子组件处 <slot name="插槽名"></slot>
// 父组件处 <son> <template v-slot:插槽名> ··· </template> </son>
|
4. 编译作用域
父级模板里的所有内容都是在父级作用域中编译的,子模板里的所有内容都是在子作用域中编译的。
Vue 模板只能访问对应实例中的属性,而不能访问子组件中的属性。
5. 作用域插槽
(1) 作用
假设有这么一种需求,希望在父组件中更改子组件的内容,但这个内容存在于子组件之中,便可以使用作用域插槽。
在父组件中用插槽替换内容,但是内容中的数据由子组件提供。
(2) 步骤
(3) 语法
1 2 3 4 5 6 7 8 9
| // 子组件处 <slot :插槽属性="数据属性" :插槽属性="数据属性"></slot>
// 父组件处 <son> <template v-slot="数据对象名"> ···数据对象名.插槽属性··· </template> </son>
|
十、动态组件
有时候,我们希望能够在不同组件上切换,实现的方法如下:
通过 router 的方式实现
通过 v-if、v-else 实现
通过动态组件实现
使用 Vue 的保留元素 component 占位,为组件添加动态的 is 属性,它将会根据 is 的值渲染为对应名称的组件。
1
| <component :is="组件名变量"></component>
|
动态组件外层还可以包裹 keep-alive 元素,组件将会被缓存,而不会在切换后来后重写生成
1 2 3
| <keep-alive> <component :is="组件名变量"></component> </keep-alive>
|
十一、异步组件
Vue 允许定义异步组件,它将会在需要时才加载。
1 2 3 4 5 6 7 8 9
| const myAsyncComponent = () => import('./my-async-component')
···
{ components: { myAsyncComponent } }
|
十二、provide/inject
可以将 provide/inject 看作可跨级的 prop,它用于向后代组件传递信息,provide 用于发送,inject 用于接收。
- provide 选项允许定义希望提供给后代组件的数据和方法
- inject 选项用于在后代组件中接收祖先组件提供的数据和方法
provide/inject 使组件耦合度增高,并且它不是响应式的,建议慎用。
如果希望大范围传递信息,更好的方式是使用 vuex。
参考