- 组件(
Component
)是 Vue.js 最强大的功能之一。 - 组件可以扩展 HTML 元素,封装可重用的代码。
- 组件系统让我们可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用的界面都可以抽象为一个组件树:
根组件
每个 Vue 应用都是通过用 createApp()
函数创建的,传递给 createApp() 的选项用于配置根组件。
当我们挂载应用时,该组件被用作渲染的起点。一个应用需要被挂载到一个 DOM 元素中。
<div id="app"></div>
//根组件
const RootComponent = { /* 选项 */ }
const app = Vue.createApp(RootComponent)
const vm = app.mount('#app')
全局组件
全局注册的组件可以在随后创建的 app 实例模板中使用,也包括根实例组件树中的所有子组件的模板中。通过component()
全局注册。
const app = Vue.createApp({...})
app.component('global-component', {
/* ... */
})
global-component
为组件名,/* … */ 为配置选项。注册后,我们可以使用以下方式来调用组件:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue 测试实例</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
</body>
<script>
// 创建一个Vue 应用
const app = Vue.createApp({})
// 定义一个名为 button-counter 的新全局组件
app.component('button-counter', {
template: `
<button @click="count++">
点了 {{ count }} 次!
</button>
`,
data() {
return {
count: 0
}
}
})
app.mount('#app')
</script>
⚠️注意:template 中 ` 是反引号,不是单单引号 '。
结果可见,各个子组件的变量 count 互不影响,子组件间相互独立,各个子组件都有自己的数据域。
局部组件
全局注册往往是不够理想的,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。
在这些情况下,可以注册局部组件。
const ComponentA = {
/* ... */
}
const ComponentB = {
/* ... */
}
const ComponentC = {
/* ... */
}
然后,在 components
选项中定义你想要使用的组件:
const app = Vue.createApp({
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
对于 components 对象中的每个属性来说,其属性名就是自定义元素的名字(component-a、component-b),其属性值就是这个组件的选项对象(ComponentA、ComponentB)。
示例:
<body>
<div id="app">
<component-a></component-a>
<br>
<component-b></component-b>
</div>
</body>
<script>
const ComponentA = {
template: `<div> A custom component! </div>`
}
const ComponentB = {
template: `<div> B custom component! </div>`
}
const app = Vue.createApp({
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
app.mount('#app')
</script>
组件嵌套
组件是可复用的 Vue 实例,所以同样通过选项 components
注册
<body>
<div id="app">
<child></child>
</div>
</body>
<script>
const ComponentA = {
template: `<div> A custom component! </div>`
}
const ComponentB = {
template: `<div> B custom component! </div>`
}
//组件的嵌套。组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,同样通过选项 components 注册
const Child = {
template: `
<div>
<component-a></component-a>
<br>
<component-b></component-b>
</div>
`,
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
}
const app = Vue.createApp({
components: {
'child': Child
}
})
app.mount('#app')
</script>
⚠️注意:template 定义多个标签时,必须用<div> </div>
包括起来。
组件传递数据
- props/$emit
- $ref
- Vuex
- slot
1. props/$emit
- 父传子:子组件通过
props
显式声明 自定义 属性,接收父组件的传值。 - 子传父:子组件通过
$emit()
显式声明 自定义 事件,父组件调用自定义事件接收子组件返回的参数。
1.1 props-父组件传递数据给子组件
props
是子组件用来接受父组件传递过来的数据的一个自定义属性。一个组件需要 显式声明 它所接受的 props
,这样 Vue 才能知道外部传入的哪些是 props。
<body>
<div id="app">
<component-a title="Google" user-name="Lily"></component-a>
<component-a :title="title" :user-name="userName"></component-a>
</div>
</body>
<script>
const ComponentA = {
template: `<div> A custom component! title = {{title}} userName = {{userName}}</div>`,
props: ['title', 'userName']
}
const app = Vue.createApp({
components: {
ComponentA
},
data: function () {
return {
title: 'Taobao',
userName: 'Jack'
}
}
})
app.mount('#app')
</script>
📌 子组件通过 props: ['title','userName']
显式声明它所接受的属性 title、userName。
💦 父组件有两种方式调用:
- 第一种直接赋值:
<component-a title="Google" user-name="Lily"></component-a>
title=“Google”、user-name=“Lily” 将属性值传递给子组件。
- 第二种,使用
v-bind
动态设置标签的属性值。
<component-a :title="title" :user-name="userName"></component-a>
title、userName 在 data
中动态赋值。需要注意一点: 需要使用格式 data: function () { }
,data:{ }
的简写方式会加载失败。
⚠️注意:HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名。
ComponentA --- <component-a>
userName --- user-name
动态 Prop
类似于用 v-bind
绑定 HTML 特性到一个表达式,也可以用 v-bind
动态绑定 props
的值到父组件的数据中。每当父组件的数据变化时,该变化也会传导给子组件。
<body>
<div id="app">
<title-info v-for="item in posts" :id="item.id" :title="item.title"></title-info>
<!-- template 形式
<template v-for="item in posts">
<title-info :id="item.id" :title="item.title"></title-info>
</template>
-->
</div>
</body>
<script>
const titleInfo = {
template: `<h4>{{ id }} - {{ title }}</h4>`,
props: ['id','title']
}
new Vue({
el: '#app',
components: {
titleInfo
},
data: function () {
return {
posts: [
{ id: 1, title: 'One' },
{ id: 2, title: 'Two' },
{ id: 3, title: 'Thre' }
]
}
}
})
</script>
1.2 $emit-子组件传递数据给父组件
- 子组件:
this.$emit('自定义事件名称', '返回参数')
,子组件通过$emit()
显式声明 自定义事件myEvent
和返回参数this.context
。 - 父组件:
@my-event="mainFun"
,父组件调用自定义事件my-event
接收子组件返回的参数。
⚠️注意:事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以自定义事件应使用 @my-event
。
<body>
<div id="app">
<child @my-event="mainFun"></child>
<p> {{context}} </p>
</div>
</body>
<script>
const child = {
template: `<div>
<button @click="funHandle">组件</button> |
<input type="text" v-model="context">
</div>
`,
data: function () {
return {
context: ''
}
},
methods: {
//内建的 $emit 方法返回自定义的事件名称 myEvent
funHandle(){
this.$emit('myEvent', this.context)
}
}
}
new Vue({
el: '#app',
components: {
child
},
data: function () {
return {
context: ''
}
},
methods: {
mainFun(param){
//返回参数
this.context = param
}
}
})
</script>
💦 上述 @my-event="mainFun"
接收子组件传递父组件的值,父组件 mainFun(param)
获取子组件传递值,如果 mainFun
方法需要传递其他参数呢?
$event
<body>
<div id="app">
<child @my-event="mainFun($event, 'aaa')"></child>
<p> {{context}} </p>
</div>
</body>
<script>
...
new Vue({
el: '#app',
...
methods: {
mainFun(param, arg1){
//返回参数
console.log("param=", param)
console.log("arg1=", arg1)
this.context = param
}
}
})
</script>
💦 如果有多个回调参数呢?
箭头函数 +
...
扩展运算符 参考:📖vue如何给自带默认回调参数的函数添加自定义参数
<body>
<div id="app">
<child @my-event="(...event) => {mainFun(event, '自定义参数')}"></child>
<p> {{context}} </p>
</div>
</body>
<script>
const child = {
template: `<div>
<button @click="funHandle">组件</button> |
<input type="text" v-model="context">
</div>
`,
data: function () {
return {
context: ''
}
},
methods: {
//内建的 $emit 方法返回自定义的事件名称 myEvent
funHandle(){
// 返回两个参数
this.$emit('my-event', this.context, '返回参数2')
}
}
}
new Vue({
el: '#app',
...
methods: {
mainFun(event, customParam){
//返回参数
console.log("event[0]=", event[0])
console.log("event[1]=", event[1])
console.log("customParam=", customParam)
this.context = event[0]
}
}
})
</script>
回调方法的两个回调参数和本地参数均能获取到。
💦 箭头函数与直接调用的区别?
(...event) => {mainFun(event, '自定义参数')}
,这实际上创建了一个新的箭头函数,该函数在接收到事件参数后,再调用mainFun
函数并传入处理过的参数。箭头函数的一个重要特性是:自动捕获其所在上下文的this
值,这对于React组件中的事件处理特别有用,可以确保this
指向你期望的组件实例。
如果直接写成
mainFun(...event, '自定义参数')
,这意呀着你希望在事件处理器被注册时就立即调用mainFun
函数,而不是等到实际事件触发时。这显然不是你希望的行为,因为这样会在注册事件处理器的时候就执行函数,而不是在用户触发事件时执行。
总结来说,使用
(...event) => {mainFun(event, '自定义参数')}
是为了创建一个新的函数,这个函数等待事件触发时被调用,并且能够正确地处理event
对象以及传递额外的参数(在这个例子中是字符串 ‘自定义参数’)。直接写mainFun(...event, '自定义参数')
则会尝试立即执行函数,而非作为事件处理器等待事件触发。
2. $ref / $parent
$refs
是用于访问组件实例或 DOM 元素的引用。首先在模板中使用 ref
属性为元素或组件设置引用名称,然后在 Vue 实例中通过 this.$refs
访问这些引用。
<template>
<div>
<input type="text" ref="inputValue" />
<button @click="showValue">显示输入值</button>
</div>
</template>
export default {
methods: {
showValue() {
const inputValue = this.$refs.inputValue.value;
console.log(inputValue);
}
}
}
也可以运用于父子组件传值:
- 父传子:父组件的方法中通过
this.$refs
访问子组件的引用。 - 子传父:子组件中通过
this.$parent.$refs
访问父组件的引用。
2.1 父传子-$refs
<body>
<div id="app">
<button @click="funHandle">点击父组件</button> |
<child ref="child"></child>
<p> {{FaMessage}} </p>
</div>
</body>
<script>
const child = {
template: `<div>
<button @click="funHandle">点击子组件</button> |
child sonMessage = {{sonMessage}} </div>`,
data: function () {
return {
sonMessage: ''
}
},
methods: {
funHandle(){
//子传父方式二:$parent
this.$parent.FaMessage = "子传父$parent"
}
}
}
new Vue({
el: '#app',
components: {
child
},
data: function () {
return {
FaMessage: ''
}
},
methods: {
funHandle(){
//父传子方式二:$refs
this.$refs.child.sonMessage = "父传子$refs"
}
}
})
</script>
this.$refs.child.sonMessage = "父传子$refs"
父组件向子组件传值,还可以调用子组件的方法。
2.2 子传父-$parent
this.$parent.FaMessage = "子传父$parent"
子组件向父组件传值,还可以调用父组件的方法。
3. 插槽-slot
slot 插槽
,是子组件提供给父组件使用的一个 占位符
,父组件可以在该占位符填充任何模板代码。
主要作用:就是为了更好的拓展和定制化组件,例如弹窗组件、表格组件等。分为默认插槽
、具名插槽
、作用域插槽
。
- 默认插槽、具名插槽:本质一样,就是
替换
子组件对应的slot,区别在于具名插槽定义了名字。(父传子) - 作用域插槽:本质上会把父组件内容渲染成函数,子组件调用函数,并将数据传递给它。当需要将子组件的数据传递给父组件展示时,就可以使用作用域插槽。(子传父)
应用场景:自定义的表格组件,允许用户传入自定义的结构和数据
3.1 默认插槽
通过 <slot></slot>
标签来定义一个插槽。这个插槽可以有默认内容,也可以是没有任何内容的。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue slot-插槽</title>
<script src="https://cdn.staticfile.org/vue/2.6.2/vue.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
</head>
<body>
<div id="app">
<div>
<h1>父组件</h1>
<child>
<p>这是插入到子组件中的内容</p>
</child>
</div>
</div>
</body>
<script>
Vue.component('child', {
template: `
<div>
<h2>子组件</h2>
<!-- 这里是插槽 -->
<slot>默认插槽内容</slot>
</div>`
})
// 创建根实例
new Vue({
el: '#app'
})
</script>
子组件通过 <slot></slot>
标签定义一个默认插槽,父组件在 <child>
标签中包含一个 <p>
标签,作为插入到子组件中的内容。
3.2 具名插槽
<body>
<div id="app">
<div>
<h1>父组件</h1>
<child>
<!-- 具名插槽 -->
<p slot="center">center1,无色</p>
<template slot="center">
<p style="color:red;">center2,字体为红色</p>
</template>
<template v-slot:center>
<p style="color:blue;">center3,字体为蓝色</p>
</template>
<template #center>
<p style="color:green;">center4,字体为绿色</p>
</template>
</child>
</div>
</div>
</body>
<script>
Vue.component('child', {
template: `
<div>
<h2>子组件</h2>
<!-- 具名插槽 -->
<slot name="center">插槽默认内容</slot>
</div>`
})
// 创建根实例
new Vue({
el: '#app'
})
</script>
子组件通过 <slot name="center"></slot>
标签定义一个具名插槽,父组件可以通过四种方式插入子组件,并且 最后一个插槽的内容会覆盖掉上面同名插槽的内容。
一个不带 name 的
slot
,会带有隐含的名字 default;
3.3 作用域插槽
作用域插槽用于父组件访问到子组件中的内容,相当于子组件将数据传递给父组件(子传父)
<body>
<div id="app">
<div>
<child>
<!-- 方式一: scope -->
<template scope="scopeData">
<ul>
<li v-for="item in scopeData.games"> {{item}} </li>
</ul>
<h2>{{scopeData.msg}}</h2>
</template>
<!-- 方式二: slot-scope -->
<template slot-scope="scopeData">
<ul>
<li v-for="item in scopeData.games"> {{item}} </li>
</ul>
<h2>{{scopeData.msg}}</h2>
</template>
<!-- 方式三: v-slot -->
<template v-slot:default="scopeData">
<ul>
<li v-for="item in scopeData.games"> {{item}} </li>
</ul>
<h2>{{scopeData.msg}}</h2>
</template>
</child>
</div>
</div>
</body>
<script>
Vue.component('child', {
template: `<div>
<!-- 作用域插槽 -->
<slot :games="gameData" msg="hello"></slot>
</div>`,
data() {
return {
gameData:['红色警戒','穿越火线','劲舞团','超级玛丽']
}
}
})
// 创建根实例
new Vue({
el: '#app'
})
</script>
子组件通过 <slot :games="gameData" msg="hello"></slot>
对外暴露属性 games、msg,父组件通过 v-slot:default="scopeData"
获取子组件传递的数据 scopeData,再通过 scopeData.games
获取实际数据。
⚠️注: 变量 scopeData 可以随便定义,它只是一个标识。scopeData.games
中的 games 才是子组件属性。
还可以
解构
传递的数据
<template v-slot:default="{games,msg}">
<ul>
<li v-for="item in games"> {{item}} </li>
</ul>
<h2>{{msg}}</h2>
</template>
3.4 小结
参考:
📖 插槽Slot的作用和基本使用
📖 Vue 中 slot 是什么?作用?分类?如何实现?
使用插槽 Slot,父组件可以向子组件指定位置插入 html 结构,更好的拓展和定制化组件,也是一种组件间通信的方式。分为:默认插槽
、具名插槽
、作用域插槽
📌 1 默认插槽:
子组件中:
<template>
<div>
<!-- 默认插槽 -->
<slot> 插槽默认内容... </slot>
</div>
</template>
父组件中:
<Category>
<div> html结构1 </div>
<template v-slot:default>>
<div> html结构1 </div>
</template>
<template #default>
<div> html结构1 </div>
</template>
</Category>
- 子组件通过
<slot>
标签来创建默认插槽 - 默认插槽带有隐含的名字 default
- 父组件可以在子组件标签内直接插入 html,也可以使用
<template v-slot:default>
、<template #default>
📌 2 具名插槽:
子组件中:
<template>
<div>
<!-- 具名插槽 -->
<slot name="slot-name">插槽默认内容...</slot>
</div>
</template>
父组件中:
<Category>
<template slot="slot-name">
<div> html结构1 </div>
</template>
<template v-slot:slot-name>
<div> html结构2 </div>
</template>
<template #slot-name>
<div> html结构3 </div>
</template>
</Category>
- 子组件通过给
<slot>
元素添加 name 属性来创建具名插槽 - 父组件中可以使用
<template slot="slot-name">
、<template v-slot:slot-name>
,或者简写<template #slot-name>
- Vue中,自定义的组件标签名称、slot 的 name 属性、变量,多个英文单词之间尽量采用
-
连接,不要使用_或者驼峰命名,比如上例使用 slot-name - Vue2.6.0之前,使用
slot="插槽名"
,Vue2.6.0 之后,建议使用v-slot:插槽名
或者#插槽名
。
📌 3 作用域插槽
子组件中:
<template>
<div>
<slot :games="gameData" title="hello"></slot>
</div>
</template>
<script>
export default {
name:'Category',
data() {
return {
gameData:['红色警戒','穿越火线','劲舞团','超级玛丽'],
}
},
}
</script>
父组件中:
<Category>
<template scope="scopeData">
<h2>{{scopeData.title}}</h2>
<h4 v-for="item in scopeData.games"> {{item}} </h4>
</template>
<template slot-scope="scopeData">
<h2>{{scopeData.title}}</h2>
<h4 v-for="item in scopeData.games"> {{item}} </h4>
</template>
<template v-slot:default="scopeData">
<h2>{{scopeData.title}}</h2>
<h4 v-for="item in scopeData.games"> {{item}} </h4>
</template>
<template #default="scopeData">
<h2>{{scopeData.title}}</h2>
<h4 v-for="item in scopeData.games"> {{item}} </h4>
</template>
</Category>
- Vue2.6.0之前,父组件使用
scope="变量名"
、slot-scope="变量名"
获取子组件数据,Vue2.6.0 之后,使用v-slot:插槽名="变量名"
或者#插槽名="变量名"
,默认插槽还可以直接使用v-slot="变量名"
。 - 可以通过解构获取,
v-slot="{games,title}
3.5 注意点
- vue 2.6.0 引入
v-slot
指令,为具名插槽和作用域插槽提供新的统一的语法,所以 vue 2.6.0 之后的版本就推荐直接使用v-slot
了。 v-slot
属性只能在<template>
上使用,但在只有默认插槽时可以在组件标签上使用。- 默认插槽名为 default,可以省略 default 直接写 v-slot;缩写为 # 时不能不写参数,写成 #default。
- 使用 element-ui 时要注意版本, element-ui 2.13.2 版本中的 vue 是 2.5 的,而 v-slot 是 vue 2.6 中才引入的,使用 v-slot 一直没效果,所以如果想在插槽中使用最新的 v-slot 指令,记得看下 vue 的版本哟。
4. Vuex
Vuex是一个专为vue.js 应用程序开发的状态管理模式, 它采用集中式存储管理数据,以相应的规则保证状态以一种可预测的方式发生改变, 一变全变,同步更新数据。
// 注册代码
const store = new Vue.Store({
state:{
count: 100
},
mutations: {
addCount(state, val = 1) {
state.count += val;
},
subCount(state, val = 1) {
state.count -= val;
}
}
})
// 组件调用
this.$store.commit('addCount'); // 加 1
this.$store.commit('subCount'); // 减 1