Vue3 组件

一、单文件组件

1. 组件定义

  • 组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构

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
41
42
43
44
45
46
47
48
49
50
51
52
53
<div id="box">
<!-- vue会找名字叫xxx的组件 -->
<my-navbar></my-navbar>
<my-sidebar></my-sidebar>
<my-global-button></my-global-button>
</div>
<script>
var obj = {
data() {
return {}
},
}
var app = Vue.createApp(obj)
//全局组件定义
app.component("my-navbar",{
template:`
<nav style="background:yellow;">
<ul>
<li v-for="item in datalist">
{{item}}
</li>
</ul>
<my-global-button></my-global-button>
</nav>
`,
data(){
return {
datalist:["首页","新闻","产品"]
}
}
})

app.component("my-global-button",{
template:`<button style="background:blue;">全局</button>`
})

app.component("my-sidebar",{
template:`
<aside>
我是侧边栏
<my-global-button></my-global-button>
<my-button></my-button>
</aside>
`,
//局部组件定义
components:{
"my-button":{
template:`<button style="background:red;">局部</button>`
}
}
})
app.mount("#box")
</script>

2. 单文件组件 SFC

  • Vue 的单文件组件(即 *.vue 文件)是一种特殊的文件格式,可以将一个 Vue 组件的模板、逻辑与样式封装在单个文件中

(1) 创建项目

1
$ npm create vue@latest

(2) 项目目录介绍

  • node_modules:依赖的命令、环境等
  • package.json:启动入口
1
2
3
4
5
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
  • index.html:主页面,会被注入 JS 脚本
1
2
3
4
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
  • vite.config.js:Vue 相关配置,如代理(改完需要重启服务)
1
2
3
4
5
6
7
8
devServer: {
proxy: {
'/api': {
target: 'https://i.maoyan.com',
changeOrigin: true
}
}
}
  • main.js:项目入口
1
2
3
4
import {createApp} from 'vue'  // 从node_modules中找
import App from './App.vue' // 根组件;从相对路径找

createApp(App).mount('#app')
  • App.vue:根组件
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
<template>
<div>
{{title}}
<button @click="handleClick">click</button>
<Sidebar></Sidebar>
<my-button></my-button>
</div>
</template>

<script>
import Sidebar from './components/Sidebar.vue'
import myButton from './components/MyButton.vue'
export default{
data(){
return {
title:"test"
}
},
// 注册局部组件(可在main.js注册全局组件)
components:{
Sidebar,
myButton
},
methods:{
handleClick(){
fetch("/api/mmdb/movie/v3/list/hot.json?ct=%E5%8C%97%E4%BA%AC&ci=1&channelId=4").then(res=>res.json())
.then(res=>{
console.log(res)
})
}
}
}
</script>

<!-- 局部生效 -->
<style scoped>
div{
background: yellow;
}
</style>

二、组件基础

1. 父传子(props)

  • props 遵循单向绑定原则:每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,但不会逆向传递(避免了子组件意外修改父组件的状态)
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
41
42
43
44
45
46
47
48
<!-- 父组件 -->
<template>
<Navbar :my-title="title" left="返回" right="首页"></Navbar>
<Navbar v-bind="{'my-title':'电影', left:'返回', right:'首页'}"></Navbar>
<Navbar v-bind="propobj"></Navbar>
</template>

<!-- 子组件 -->
<template>
<div>{{left}}——{{myTitle}}——{{right}}</div>
</template>
<script>
export default {
// props:["myTitle","left","right"]
props: {
// 基础类型检查(给出`null`和`undefined`值则会跳过任何类型检查)
// String、Number、Boolean、Array、Object、Date、Function、Symbol
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为String类型
propC: {
type: String,
required: true
},
// Number类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或者数组应当用工厂函数返回
// 工厂函数会收到组件所接收的原始props作为参数
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
return ['success', 'warning', 'danger'].includes(value)
}
}
}
}
</script>
  • 属性透传:指传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和 id
  • 当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 父组件 -->
<template>
<Navbar class="navbar" id="navbarid" style="background-color: yellow;" @click="handleClick()"></Navbar>
</template>

<!-- 子组件 -->
<template>
<div><!-- 单元素根 -->
<!-- `$attrs`能够获取父组件传递的参数,即使禁止透传 -->
<button v-bind="$attrs">test</button>
</div>
</template>
<script>
export default {
inheritAttrs:false // 禁止透传(不加则默认透传到根节点)
}
</script>

2. 子传父($emit)

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
<!-- 父组件 -->
<template>
<Child @myevent.once="handleMyEvent($event)" @my-event="handleMyEvent"/>
</template>
<script>
export default {
methods:{
handleEvent(data){
console.log("app-event",data)
}
}
}
</script>

<!-- 子组件 -->
<template>
<div>
<!-- <button @click="$emit('myevent',childtitle)">click</button> -->
<button @click="handleClick">click</button>
</div>
</template>
<script>
export default {
data(){
return {
childtitle:"child-111111"
}
},
methods:{
handleClick(){
// `$emit`触发自定义事件
this.$emit("myEvent",this.childtitle)
}
}
}
</script>

3. 父组件强权($refs)

  • ref 如果绑定在 DOM 节点上,拿到的就是原生 DOM 节点
  • ref 如果绑定在组件上,拿到的就是组件对象,可以实现通信功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 父组件 -->
<template>
<div>
<input ref="myinput"/>
<div ref="mydiv">aaa</div>
<Child ref="mychild"/>
<button @click="handleClick()">click</button>
</div>
</template>
<script>
export default {
methods:{
handleClick(){
console.log(this.$refs.mychild.childtitle)
this.$refs.myinput.value = "hello";
this.$refs.mydiv.style.background = "red";
this.$refs.mychild.childtitle = "22222"
}
}
}
</script>

4. 子组件强权($parent、$root)

  • 在子组件中通过 $parent 访问父组件,通过 $root 访问根组件
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 子组件 -->
<script>
export default {
methods:{
handeClick(){
console.log(this.$parent.title)
console.log(this.$parent.$parent.title)
console.log(this.$root.title)
}
}
}
</script>

5. 跨级通信(provide、inject)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 父组件 -->
<script>
export default {
provide(){ // 给后代用
return {
navTitle:this.navTitle, // 不支持响应式
app:this // 可以传一个支持响应式的对象
}
}
}
</script>

<!-- 子组件 -->
<script>
export default {
inject:["navTitle","app"],
methods:{
handleClick(){
this.app.navTitle = this.item // 不建议这样用
}
}
}
</script>

6. 订阅发布

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
<!-- store.js -->
export default {
datalist:[],
subscribe(cb){
this.datalist.push(cb)
console.log(this.datalist)
},
publish(value){
this.datalist.forEach(cb=>cb(value))
}
}

<!-- 订阅 -->
<script>
import store from './store'
export default {
mounted(){
store.subscribe((value)=>{
this.title = value
})
}
}
</script>

<!-- 发布 -->
<script>
import store from './store';
export default {
methods:{
handleClick(){
store.publish(this.title)
}
}
}
</script>

7. 动态组件与 KeepAlive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 不活跃的组件将会被缓存(不使用KeepAlive会被重新加载) -->
<keep-alive>
<component is="Home" /><!-- 注意这里是字符串 -->
</keep-alive>

<!-- 以英文逗号分隔的字符串,字符串是组件名(name属性) -->
<KeepAlive include="home,center">
<component :is="activeComponent" />
</KeepAlive>

<!-- 正则表达式(需使用`v-bind`) -->
<KeepAlive :include="/home|center/">
<component :is="activeComponent" />
</KeepAlive>

<!-- 数组(需使用`v-bind`) -->
<KeepAlive :exclude="['home', 'center']">
<component :is="activeComponent" />
</KeepAlive>

8. 组件中的 v-model

  • v-model 原理
1
2
3
4
5
6
7
8
9
10
<!-- 模板编译器会对v-model进行展开: -->
<input v-model="mytext" />
<input :value="mytext" @input="mytext = $event.target.value" />

<!-- 如果用在组件上,会被展开为: -->
<Child v-model="mytext" />
<Child :modelValue="mytext" @update:modelValue="newValue => mytext = newValue" />

<Child v-model:hello="mytext" />
<Child :hello="mytext" @update:hello="newValue => mytext = newValue" />

9. 异步组件

  • Vue 提供了 defineAsyncComponent 方法实现需要时再从服务器加载相关组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
import { defineAsyncComponent } from 'vue';
export default {
components: {
Home: defineAsyncComponent(() => import('./views/Home.vue')),
Center: defineAsyncComponent({
// 加载异步组件
loader: () => import('./views/Center.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认200ms
delay: 0,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果超时会显示errorComponent,默认Infinity
timeout: 1000
})
}
}
</script>

三、组件插槽

1. 基本使用

slot

  • <slot> 元素是一个插槽出口,标识了父元素提供的插槽内容将在哪里被渲染
  • 插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 父组件 -->
<template>
<Child>
<div>hello</div>
</Child>
</template>

<!-- 子组件 -->
<template>
<div>
<slot></slot>
<slot></slot>
</div>
</template>

2. 具名插槽

  • 渲染到指定名字的插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 父组件 -->
<template>
<Child>
<template v-slot:hello>
<div>hello</div>
</template>
<template #world>
<div>world</div>
</template>
<div>LB</div>
</Child>
</template>

<!-- 子组件 -->
<template>
<div>
<slot name="hello"></slot>
<slot name="world"></slot>
<slot></slot>
</div>
</template>

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
<!-- 父组件:重新渲染数据 -->
<template>
<div>
<Child #movie="{mylist}">
<ul><!-- 重新渲染 -->
<li v-for="item in datalist" :key="item.id">
{{ item.title }} --- {{ item.date }}
</li>
</ul>
</Child>

<Child v-slot="myprops">
{{ myprops.mylist }}
{{ myprops.a }}
</Child>
</div>
</template>

<!-- 子组件:暴露数据 -->
<template>
<div>
<slot :mylist="datalist" name="movie"><!-- 具名方式 -->
<ul><!-- 默认渲染 -->
<li v-for="item in datalist" :key="item.id">
{{ item.title }}
</li>
</ul>
</slot>

<slot :mylist="datalist" a="1"><!-- 无名方式 -->
</slot>
</div>
</template>

四、生命周期

lifecycle

  • 每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码
  • beforeCreate() 会在实例初始化完成、props 解析之后、data()computed 等选项处理之前立即调用
  • created() 被调用时,以下内容已经设置完成:响应式数据、计算属性、方法和侦听器。然而,此时挂载阶段还未开始,因此 $el 属性仍不可用
  • beforeMount() 被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程
  • mounted() 所有同步子组件都已经被挂载,其自身的 DOM 树已经创建完成并插入了父容器中。这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用
  • beforeUpdate() 用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的
  • updated() 会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代
  • beforeUnmount() 被调用时,组件实例依然还保有全部的功能
  • unmounted() 所有子组件都已经被卸载,所有相关的响应式作用都已经停止。可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接
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
41
42
43
44
45
46
47
48
49
50
51
<script>
import * as echarts from 'echarts'
export default {
data() {
return {
option: {},
mywidth: '600px'
}
},
methods:{
handleClick(){
this.mywidth = '800px'
this.$nextTick(()=>{
console.log("nexttick")
this.myChart.resize()
})
}
},
created() {
this.option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
}
},
mounted() {
this.myChart = echarts.init(document.getElementById('main'));
this.myChart.setOption(this.option);
window.onresize = ()=>{
console.log("resize")
this.myChart.resize()
}
},
unmounted(){
console.log("unmounted")
window.onresize = null
}
}
</script>

五、其他

1. 自定义指令

  • 除了 Vue 内置的一系列指令(比如 v-model 或 v-show),Vue 还允许注册自定义指令。自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 全局指令
app.directive('color', {
mounted(el,binding) {
// directive里用this拿不到组件实例
el.style.background = binding.value
}
})

// 局部指令
const focus = {
mounted: (el) => el.focus()
}
export default {
directives: {
focus
}
}

// 使用
<div v-color="'red'"></div>
<div v-focus></div>
  • 自定义指令的生命周期:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const myDirective = { 
// 在绑定元素的attribute前或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {},
// 在元素被插入到DOM前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
  • 对于自定义指令来说,一个很常见的情况是需要在 mountedupdated 上实现相同的行为,并且不需要其他钩子。这种情况下可以直接用一个函数来定义指令:
1
app.directive('color', (el, binding) => {...})

2. 过渡效果

  • Vue 提供了两个内置组件,可以帮助制作基于状态变化的过渡和动画
    • Transition 会在一个元素或组件进入和离开 DOM 时应用动画
    • TransitionGroup 会在一个 v-for 列表中的元素或组件被插入、移动或移除时应用动画
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<!-- 基本使用 -->
<Transition name="aa"><!-- name默认为v -->
<div v-if="isShow">11111111</div><!-- 只能包含单个元素 -->
</Transition>
<style>/* 可以用animation,见官网 */
.aa-enter-active,
.aa-leave-active {
transition: all 1s ease;
}
.aa-enter-from,
.aa-leave-to {
transform: translateX(100px);
opacity: 0;
}
html,body{
overflow-x: hidden;
}
</style>

<!-- 自定义class -->
<Transition enter-active-class="animate__animated animate__bounceIn"
leave-active-class="animate__animated animate__bounceOut">
<div v-if="isShow">11111111</div>
</Transition>

<!-- JS钩子 -->
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@enter-cancelled="onEnterCancelled"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
@leave-cancelled="onLeaveCancelled">
...
</Transition>

<!-- 过度组件 -->
<Transition name="fade" mode="out-in" appear>
<component :is="activeComponent"></component>
</Transition>

<!-- 列表过度 -->
<TransitionGroup tag="ul">
<li v-for="(item, index) in datalist" :key="item"><!-- 必须加key -->
...
</li>
</TransitionGroup>

<!-- 可复用过度 -->
<template>
<!-- 包装内置的Transition组件 -->
<Transition name="my" @enter="onEnter" @leave="onLeave">
<slot></slot><!-- 向内传递插槽内容 -->
</Transition>
</template>
<style>/* 避免在这里使用scoped,因为那不会应用到插槽内容上 */
</style>

<MyTransition>
<div v-if="show">Hello</div>
</MyTransition>

3. 其他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// es6 第一种导入导出
export default {
createApp:function(){...}
}

import Vue from 'vue'
Vue.createApp

// es6 第二种导入导出
function createApp(){}
export {createApp}

import {createApp} from 'vue'
import * as vue from 'vue'

// ES6 直接使用模块化
<script type="module">
import obj from './1.js'
console.log(obj)
</script>