目录
一. 认识组件化
1.什么是组件化?
引入: 人在面对复杂问题处理方式?
下拉查看(理解)
- 当面对一个非常复杂的问题时, 我们不太可能一次性搞定全部内容
- 但是, 我们有一种天生的能力, 就是将问题进行拆解
- 如果将一个复杂的问题, 拆分成很多个可以处理的小问题, 再将其放在整体当中, 你会发现大的问题也会迎刃而解
组件化也是类似的思想:
下拉查看(理解)
- 如果我们将一个页面中所有的处理逻辑全部放在一起, 处理起来就会变得非常复杂, 而且不利于后续的管理以及扩展
- 但如果, 我们将一个页面拆分成一个个小的功能块, 每个功能块完成属于自己这部分独立的功能, 那么之后整个页面的管理和维护就变得非常容易了
- 我们将一个完成的页面分成多个组件
- 每个组件都用于实现页面的一个功能
- 而每一个组件又可以进行细分
2.Vue组件化思想
- 组件是Vue.js中重要思想
- 它提供了一种抽象, 我们可以开发出一个独立可复用的小组件来构造我们的应用
- 组件可以扩展 HTML 元素,封装可重用的代码
- 组件系统让我们可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用的界面都可以抽象为一个组件树

- 组件化思想应用
- 有了组件化的思想, 我们之后开发中就要充分的利用它
- 尽可能将页面拆分成一个个小的, 可复用的组件
- 这样让我们代码更方便组织和管理, 并且扩展性也强
二. 注册组件
1.全局组件注册
组件注册分为三个步骤
创建组件构造器对象
Vue.extend({ })注册组件
Vue.component('tagName',options)使用组件
<my-cpn/>
当我们通过调用
Vue.component()注册组件时, 组件的注册是全局的- 该组件可以在任意Vue实例下使用
2.局部组件注册
- 如果我们注册的组件是挂载某个实例中, 那么就是一个局部组件
- 只能在当前Vue实例管理下使用
- 在Vue实例 options 中定义,
components带s
<div id="app">
<my-component></my-component>
</div>
<script>
// 注册
var Child = {
template: '<div>A custom component!</div>'
};
// 创建根实例
new Vue({
el: '#app',
components: {
// <my-component> 将只在父模板可用
'my-component': Child
}
})
</script>
3.父组件和子组件
- 前面我们看到了组件树
- 组件和组件之间存在层级关系
- 而其中一种非常重要的关系就是父子组件的关系
- 父子组件的错误用法:
- 当子组件注册到父组件的
components时, Vue会编译好父组件的模块 - 该模块的内容已经决定了父组件要渲染的HTML(相当于父组件已经有了子组件的内容了)
- 子组件只能在父组件中被识别的
- 当子组件注册到父组件的
4.注册组件语法糖
- 主要省去了
Vue.extend()步骤, 而是可以用一个对象来替代
全局注册组件语法糖
<cpn2></cpn2> // 语法糖写法 Vue.component('cpn2', { template: ` <div> <h2>我是标题22</h2> <div>我是内容,哈哈哈哈</div> <div>我是内容,呵呵呵呵</div> </div> ` })局部组件注册语法糖
components: { 'cpn3':{ template: ` <div> <h2>我是标题33</h2> <div>我是内容,哈哈哈哈</div> <div>我是内容,呵呵呵呵</div> </div> ` })}
5.模板的分离写法
- 将
template中的HTML分离出来, 然后挂载到对应的组件, 结构会非常清晰 - Vue提供了两种方案来定义HTML模板内容
- 使用
script:<script type="text/x-template" id="cpn1"></script> - 使用
template:<template id="cpn2"></template> - 在
template填写: 对应模板的id值
- 使用
1.script模板示例
2.template模板示例
三. 组件中数据的存放
1.组件中数据的访问
组件是一个单独功能模块的封装
- 这个模块有属于自己的HTML模板, 也应该有自己的数据data
组件中的可以访问顶层Vue实例中的数据吗?
- 不能访问Vue实例中的数据, 即使能访问, 所有数据放在Vue实例中, Vue实例会变得非常臃肿
2.组件数据的存放
- 组件的数据放在组件对象的
data属性当中- 只是这个data属性必须是一个函数
- 而且这个函数要返回一个对象
Vue.component('cpn', {
template: '#cpn',
// 组件数据的存放
data() {
return {
counter: 0
}
}
})
3.组件中data为什么必须是函数?
- 如果在组件中
data是一个对象会发生什么?❌- 多个组件会共用相同的数据, 数据发生改变后会相互影响
- 最重要的是在组件中data是一个对象会报错, 可以返回一个对象的引用 ( 就是声明一个对象在data中引用 ,来实现数据互通 )
- 如果在组件中
data是一个函数👌- 因为组件可能被多处使用,但它们的data是私有的,所以每个组件都要 return 一个新的data对象,如果共享data,修改其中一个会影响其他组件
- 组件之间的数据都是相互独立存在的, 数据改变后, 不会相互影响
四. 组件的通信
1.父子组件的通信
- 在上面, 我们提到了子组件是不能引用父组件或Vue实例的数据的
- 但是, 开发中, 往往一些数据确实需要从上层传递到下层
- 比如在一个页面中, 我们从服务器请求到了很多数据
- 其中一部分数据, 并非是我们整个页面大的组件来展示的, 而是需要下面的子组件来进行展示的
- 这个时候, 不会让子组件再放一次网络请求, 而是让大组件(父组件)将数据传递给小组件(子组件)
- 如何进行父子组件之间的通信呢?
- 通过
props向子组件传递数据 - 通过自定义事件向父组件发送消息
- 引用官网的一句话:父子组件的关系可以总结为 props 向下传递,事件向上传递
- 通过

2.非父子组件通信(事件总线)
- 有时候两个组件也需要通信(非父子关系)
- 在简单的场景下,可 以使用一个空的Vue实例作为中央事件总线:
let $bus = new Vue()
// 触发组件 A 中的事件
$bus.$emit('event', 1)
// 在组件 B 创建的钩子中监听事件
$bus.$on('event',id => {
// ...
})
3.祖先和子孙组件通信(provide/inject)
provide可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用inject来接收provide提供的数据或方法
// 父级组件提供 'foo'
var Provider = {
provide: {
foo: 'bar'
},
}
// 子组件注入 'foo'
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
}
4.组件间的通信(图示)

五. 父组件向子组件传值
1.props基本用法
在子组件中, 使用选项
props用于接收来自父组件的数据
- 父组件如何向子组件传递数据?
- 1.在子组件定义
props属性, 值可以是 数组 | 对象 - 2.在使用子组件时, 用v-bind绑定属性将父组件的值传递给 子组件props 定义的属性
- 1.在子组件定义
props的值有两种方式:- 方式一:
字符串数组, 数组中的字符串就是传递来的值, 使用时的名称 - 方式二:
对象, 对象可以类型检测, 自定义验证, 默认值等.
- 方式一:
2.props数据校验
| 属性 | 描述 |
|---|---|
type |
检测是否是给定的类型, 否则抛出警告, 支持String、Number、Boolean、Array、Object、Date、Function、Symbol、任何自定义构造函数、或上述内容组成的数组 |
default:any |
为该 prop 指定一个默认值。如果该 prop 没有被传入,则换做用这个值。对象或数组的默认值必须从一个工厂函数返回。 |
required:Boolean |
定义该 prop 是否是必填项 |
validator:Function |
自定义验证函数会将该 prop 的值作为唯一的参数代入 |
const cpn = {
template: '#cpn',
data() { return {} },
// props: ['cmovies', 'cmessage']
props: {
//1.可以类型限制
height: Number,
//2.提供默认值,以及必传值
cmessage: {
type: String,
default: 'aaa',
required: true
},
//3.类型是对象或者数组时,定义默认值必须是一个函数
cmovies: {
type: Array,
default() {
return ['a','b']
},
//4.自定义验证
age: {
validator: function (value) {
return value >= 0
}
}
}
}
- 注意: 在props定义的属性中使用驼峰形式,模板中需要使用短横线的形式, 使用组件时驼峰标识的前用 - 代替
六. 子组件向父组件传值
1. 自定义事件
props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中。
我们应该如何处理呢?这个时候,我们需要使用**
自定义事件**来完成。
- 什么时候需要自定义事件?
- 当子组件需要向父组件传递数据时, 我们就要用到自定义事件了
- 之前学习的
v-on不仅可以监听DOM事件, 也可以用于组件间的自定义事件
- 自定义事件流程
子组件中, 将数据传递给父组件, 通过**
this.$emit('eventName', [...args])** 触发当前实例上的事件, 附加参数都会传给监听器回调在使用子组件时, 监听在$emit参数中定义的事件, 值是定义在实例中定义的事件, 触发后实例方法中接收子组件的值
<cpn @eventNamek="cpnClick"></cpn>
- 注意:
$emit参数中的事件名不能是驼峰命名
<!-- 2.在使用组件时,监听定义的事件,并在父组件中定义接收 -->
<cpn @item-Click="cpnClick"></cpn>
<template id="cpn">
<div>
<button v-for="item in categories" @click="btnClick(item)">{{item.name}}</button>
</div>
</template>
<script>
const cpn = {
template: '#cpn',
data() {
return {
categories: [
{ id: 'aaa', name: '热门推荐' },
{ id: 'bbb', name: '手机数码' },
{ id: 'ccc', name: '家用家电' },
{ id: 'ddd', name: '电脑办公' },
]
}
},
methods: {
btnClick(item) {
/// 1.在子组件中点击触发获得到的值,发射/触发事件事件
this.$emit('item-click', item)
}
},
}
new Vue({
el: '#app',
components: {
cpn
},
methods: {
// 3.在父组件中方法接收数据
cpnClick(item) {
console.log(item);
}
}
})
</script>
2. sync修饰符
- 使用 .sync修饰符 场景: 子组件想修改父组件传递的数据时
- 在使用改子组件时, 使用 .sync 修饰符
- 使用该组件时不需要在父组件中监听事件, 改变数据
<!-- 父组件 -->
<demo :money="money" @update:money="fn"></demo>
<!-- 作用相等,使用: .sync 修饰符,省略了定义事件,由子组件来修改数据(语法糖) -->
<demo :money.sync="money"></demo>
<!-- demo子组件-->
<script>
export default {
props: {
money: Number
},
methods: {
changeMoney() {
this.$emit('update:money',200)
}
}
}
</script>.
<!-- demo子组件-->
<script>
export default {
props: {
money: Number
},
methods: {
changeMoney() {
this.$emit('update:money',200)
}
}
}
</script>.
七. 父子组件的访问
1.父访问子: $children
有时候我们需要父组件直接访问子组件对象, 或子组件直接访问父组件, 或子组之间访问根组件
- 父访问子组件: 使用
$children或$refs (reference 引用)- 子组件访问父组件: 使用
$parent
$children的访问(不常用):this.children获取的是一个数组类型, 它包括所有子组件对象
methods: {
btnClick() {
// 1.$children: 获取所有子组件对象
console.log(this.$children);
console.log(this.$children[2].name);
this.$children[2].cmessage();
// 遍历"所有子组件"
for (const item of this.$children) {
console.log(item.name);
item.cmessage()
}
}
}
2.父访问子: $refs
$children的缺陷:- 通过
$children访问子组件时, 是一个数组类型, 访问其中的子组件必须通过索引 - 当子组件过多, 或在子组件前新添加的了组件, 往往不能确认拿到目标子组件的索引值
- 有时候, 想获取明确的其中一个特定的组件时, 可以用
$refs
- 通过
**
$refs**的使用(常用):$refs 和 ref指令通常是一起 使用的首先给, 目标子组件添加
ref绑定一个id其次在父组件中, 通过
this.$refs.id就可以访问到该组件了
<child-cpn1 ref="child1"></child-cpnl>
<child-cpn2 ref="child2"></child-cpn2>
<button @click="showRefscpn">通过refs访问子组件</button>
/* Vue的methods中 */
showRefscpn(){
// 获取子组件中的message的值
console.log(this.Srefs.child1.message);
console.log(this.Srefs.child2.message);
}
3.子访父: $parent
- 在子组件中访问父组件:
$parent(尽量避免使用) - 在子组件中访问根组件:
$root(尽量避免使用) - 注意事项
- 尽管 Vue 在开发中, 允许我们通过 $parent 来访问父组件, 但开发中尽量不要这么做
- 子组件尽量避免直接访问父组件的数据, 因为这样耦合度太高
- 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性, 往往会引起问题。
- 另外, 更不好做的是通过$parent直接修改父组件的状态, 那么父组件中的状态将变得飘忽不定, 很不利于我的调试和维护
八. 插槽slot
1.为什么使用插槽
- slot翻译为插槽:
- 插槽的目的是让我们原来的设备具备更多的扩展性。
- 比如电脑的USB接口我们可以插入U盘、硬盘、手机、音响、键盘、鼠标等等。
- 组件的插槽:
- 组件的插槽也是为了让我们封装的组件更加具有扩展性。
- 让使用者可以决定组件内部的一些内容到底展示什么。
- 举个🌰: 移动网站中的导航栏
- 移动开发中, 几乎每个页面都有导航栏
- 导航栏我们必然会封装成一个插件, 比如nav-bar组件。
- 一旦有了这个组件, 我们就可以在多个页面中复用了。

- 总结:
Slot通俗的理解就是“占坑”,在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot 位置)
2.如何封装这类组件
如何封装合适呢? 抽取共性,保留不同。 最好的封装方式就是将共性抽取到组件中, 将不同内容的地方暴露为插槽。
一旦我们预留了插槽, 就可以让使用者根据自己的需求, 决定插槽中插入什么内容。 是搜索框, 还是文字, 还是菜单。由调用者自己来决定。
3. slot 基本使用
- 基本使用
slot- 在子组件中, 定义一个
<slot></slot>就可以为组件开启一个插槽 - 该插槽插入什么内容取决于父组件如何使用 👇👇👇
- 在子组件中, 定义一个
4. slot 插槽默认值
有时候我们需要给插槽设置一个具体的默认内容,它只会在没有提供内容的时候就会被渲染
<template id="cpn">
<div>
<!-- 当使用子组件没有提供内容时候,就会渲染默认内容:按钮 -->
<slot><button>按钮</button></slot>
</div>
</template>
4.具名插槽 slot
- 当子组件有多个插槽时, 想替换掉指定的插槽怎么办?
- 对于这样的情况,
<slot>元素有一个特殊的 attribute:name - 在向具名插槽提供内容的时候, 我们在
template元素使用v-slot指令, 并以v-slot的参数形式提供其名称,v-slot:header
- 对于这样的情况,
<cpn>
<!-- 向具名插槽提供内容, 我们在template元素上使用 v-slot 指令 -->
<template v-slot:header>
<h1>Here might a page title</h1>
</template>
<!-- 默认会填没有具名插槽的坑 -->
<template v-slot:default>
<p>我是填的主要内容元素的坑</p>
<p>主要内容</p>
</template>
<template v-slot:footer>
<p>这里是页脚</p>
</template>
</cpn>
<!-- 模板内容👇👇👇 -->
<template id="cpn">
<div>
<header>
<!-- 我们希望页头放在这里 -->
<slot name="header"></slot>
</header>
<main>
<!-- 我们希望主要内容放在这里 -->
<!-- 不带 name 的 slot 默认name包含 "default" -->
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
<!-- 我们希望页脚放在这里 -->
</footer>
</div>
</template>
- 一个不带
name的<slot>出口会带有隐含(默认)的名字 "default"
5.编译作用域
准则: 父组件模板的所有东西都会在父级作用域内编译; 子组件模板的所有东西都在子级作用域内编译
展开查看案例
6.作用域插槽
使用场景
- 在父组件中展示子组件的内容, 对子组件内容展示不满意
- 在其插槽中获取子组件展示的数据, 进行重新修改内容重新展示
如何使用
- 在子组件插槽中绑定自定义属性用来要展示的数据(供父作用域访问的数据) 在定义子组件中:
<template id="cpn"> <div> <span> <!-- 为了让 user 在父级插槽中可用, 我们将 user 作为 <slot> 元素的一个属性绑定 --> <slot :user="user">{{user.lastName}}</slot> </span> </div> </template>- 在使用子组件中, 获取传递过来的数据, 进行修改展示 使用子组件中:
<!-- 在使用子组件中, 对组件展示的内容不满意, 在父作用域对子组件展示的内容进行修改 --> <current-user> <!-- 在这里想访问子组件中的 user 数据 --> <template v-slot:default="slotProps"> {{slotProps.user.firstName}} </template> </current-user>v-slot的使用范围:v-slot只能在组件中 或 template元素中使用备注
- 1.
v-slot指令自Vue 2.6.0起被引入,提供更好的支持v-slot和slot-scope attribute的 API 替代方案
- 2.在接下来所有的2.x版本中
v-slot和slot-scope attribute仍会被支持,但已经被官方废弃且不会出现在Vue3中- 1.
总结
- 有时让插槽内容能够访问子组件中才有的数据是很有用的
- 父组件替换插槽的标签,但是内容由子组件来提供
独占默认插槽
当提供的内容只有默认插槽时, 组件的标签才可以被插槽的模板来使用
这样我们就可以把
v-slot直接写在组件上
- 不带参数的
v-slot对应默认插槽
<current-user v-slot="slotProps">
{{slotProps.user.lastName}}
</current-user>
- 注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确
解构插槽prop
slot的值可以是任何能够作为函数形参数的 JavaScript 表达式, 可以使用 ES6 解构来传入具体的插槽
<!-- 1."解构"传递来的对象 -->
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
<!-- 2.重命名(起别名), 将 user 重命名为 person -->
<urrent-user v-slot="{ user: person }">
{{person.firstName}}
{{person.firstName}}
</urrent-user>
7.动态插槽名
动态指令参数也可以用在
v-slot上,来定义动态的插槽名
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
8.具名插槽的缩写
跟
v-on和v-bind一样,v-slot也有缩写, 即把参数之前的所有内容(v-slot:)替换为字符#例如v-slot:header可以被重写为#header
<cpn>
<!-- 向具名插槽提供内容, 使用 # 简写形式(代替: v-slot ) -->
<template #header>
<h1>Here might a page title</h1>
</template>
<!-- 如果你希望使用缩写的话,你必须始终以明确插槽名取而代之: -->
<template #default="{ user }">
{{ user.firstName }}
</template>
</cpn>

