Vue3学习
创建Vue3工程
基于vue-cli创建
点击查看官方文档
备注:目前
vue-cli
已处于维护模式,官方推荐基于Vite
创建项目。
1 | ## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上 |
基于vite创建(推荐)
vite
是新一代前端构建工具,官网地址:https://vitejs.cn,vite
的优势如下:
轻量快速的热重载(
HMR
),能实现极速的服务启动。对
TypeScript
、JSX
、CSS
等支持开箱即用。真正的按需编译,不再等待整个应用编译完成。
webpack
构建 与vite
构建对比图如下:具体操作如下(点击查看官方文档)
1 | ## 1.创建命令 |
安装官方推荐的vscode
插件:
- TypeScript Vue Plugin(Volar)
- Vue-Official
Vue项目env.d.ts内飘红
在命令行执行一下npm i
,安装依赖,然后重新打开Vscode
总结:
Vite
项目中,index.html
是项目的入口文件,在项目最外层。- 加载
index.html
后,Vite
解析<script type="module" src="xxx">
指向的JavaScript
。 Vue3
中是通过createApp
函数创建一个应用实例。
Vue3核心语法
【OptionsAPI 与 CompositionAPI】
Vue2
的API
设计是Options
(配置)风格的。Vue3
的API
设计是Composition
(组合)风格的。
Options API 的弊端
Options
类型的
API
,数据、方法、计算属性等,是分散在:data
、methods
、computed
中的,若想新增或者修改一个需求,就需要分别修改:data
、methods
、computed
,不便于维护和复用。
Composition API 的优势
可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。
【拉开序幕的 setup】
setup 概述
setup
是Vue3
中一个新的配置项,值是一个函数,它是
Composition API
“表演的舞台_”_,组件中所用到的:数据、方法、计算属性、监视......等等,均配置在setup
中。
特点如下:
setup
函数返回的对象中的内容,可直接在模板中使用。setup
中访问this
是undefined
。setup
函数会在beforeCreate
之前调用,它是“领先”所有钩子执行的。
1 | <template> |
setup 的返回值
- 若返回一个对象:则对象中的:属性、方法等,在模板中均可以直接使用(重点关注)。
- 若返回一个函数:则可以自定义渲染内容,代码如下:
1 | // 一开始的形式 |
setup 与 Options API 的关系
setup和Options API能不能同时存在?能共存
Vue2
的配置(data
、methods
......)中可以访问到setup
中的属性、方法。(setup是最早的生命周期,比data等早)- 但在
setup
中不能访问到Vue2
的配置(data
、methos
......)。 - 如果与
Vue2
冲突,则setup
优先。
setup 语法糖
setup
函数有一个语法糖,这个语法糖,可以让我们把setup
独立出去(不用写setup函数,也不用写return,会自动return)
原代码如下:
1 | <template> |
使用语法糖的代码如下:
1 | <template> |
扩展:上述代码,还需要编写一个不写setup
的script
标签,去指定组件名字,比较麻烦,我们可以借助vite
中的插件简化
- 第一步:
npm i vite-plugin-vue-setup-extend -D
- 第二步:
vite.config.ts
1 | import { defineConfig } from 'vite' |
- 第三步:
<script setup lang="ts" name="Person2">
注:也可以不下插件指定名字,直接将
1 | <script lang="ts"> |
删除,但组件的名字就是文件的名字
【ref 创建:基本类型的响应式数据】
- 作用:定义响应式变量。
- 语法:
let xxx = ref(初始值)
。 - 返回值:一个
RefImpl
的实例对象,简称ref对象
或ref
,ref
对象的value
属性是响应式的。(图中RefImpl对象,只有value是给我们用的,其他的都不是) - 注意点:
JS
中操作数据需要:xxx.value
,但模板中不需要.value
(因为自动.value
了,模板指的是<template>
标签内),直接使用即可。- 对于
let name = ref('张三')
来说,name
不是响应式的,name.value
是响应式的。 - 不是所有的数据都需要定义成响应式的,要修改的数据定义成即可。比如:有修改姓名和年龄的需求,没有修改电话号码和地址的需求,那么姓名和年龄可以定义成响应式变量。
1 | <template> |
reactive 创建:对象类型的响应式数据】
- 作用:定义一个响应式对象(基本类型不要用它,要用
ref
,否则报错) - 语法:
let 响应式对象= reactive(源对象)
。 - 返回值:一个
Proxy
的实例对象,简称:响应式对象。 - 注意点:
reactive
定义的响应式数据是“深层次”的。(下面代码的obj.a.b.c.d)
1 | <template> |
【ref 创建:对象类型的响应式数据】
- 其实
ref
接收的数据可以是:基本类型、对象类型。 - 若
ref
接收的是对象类型,内部其实也是调用了reactive
函数。
1 | <template> |
【ref 对比 reactive】
宏观角度看:
ref
用来定义:基本类型数据、对象类型数据;
reactive
用来定义:对象类型数据。
- 区别:
ref
创建的变量必须使用.value
(可以使用volar
插件自动添加.value
)。
reactive
重新分配一个新对象,会失去响应式(可以使用Object.assign
去整体替换)。
- 使用原则:
- 若需要一个基本类型的响应式数据,必须使用
ref
。- 若需要一个响应式对象,层级不深,
ref
、reactive
都可以。- 若需要一个响应式对象,且层级较深,推荐使用
reactive
。
1 | <template> |
【toRefs 与 toRef】
问题引入:
1 | <template> |
引出一个问题:解构出来的不是响应式的,那么如何让它变成响应式的?
- 作用:将一个响应式对象中的每一个属性,转换为
ref
对象。 - 备注:
toRefs
与toRef
功能一致,但toRefs
可以批量转换。 - 语法如下:
1 | <template> |
【computed】
用方法也可以实现计算属性的功能,不过如果有多个相同的fullName,计算属性只会计算第一个,后面相同的不会计算,直接拿上一次计算的用(缓存)。而方法没有缓存,每次都要重新计算,开销大。
作用:根据已有数据计算出新数据(和Vue2
中的computed
作用一致)。
1 | <template> |
【watch】
- 作用:监视数据的变化(和
Vue2
中的watch
作用一致) - 特点:
Vue3
中的watch
只能监视以下四种数据:
ref
定义的数据。reactive
定义的数据。- 函数返回一个值(
getter
函数)。- 一个包含上述内容的数组。
我们在Vue3
中使用watch
的时候,通常会遇到以下几种情况:
* 情况一
监视ref
定义的【基本类型】数据:直接写数据名即可,监视的是其value
值的改变。
1 | <template> |
* 情况二
监视ref
定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。
注意:
若修改的是
ref
定义的对象中的属性,newValue
和oldValue
都是新值,因为它们是同一个对象。若修改整个
ref
定义的对象,newValue
是新值,oldValue
是旧值,因为不是同一个对象了。
1 | <template> |
* 情况三
监视reactive
定义的【对象类型】数据,且默认开启了深度监视。(关不掉)
1 | <template> |
* 情况四
监视ref
或reactive
定义的【对象类型】数据中的某个属性,注意点如下:
- 若该属性值不是【对象类型】,需要写成函数形式。
- 若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。
结论:监视的要是对象里的属性,那么最好写函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。
1 | <template> |
* 情况五
监视上述的多个数据
1 | <template> |
【watchEffect】
官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。
watch
对比watchEffect
都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch
:要明确指出监视的数据watchEffect
:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。
示例代码:
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>
<div class="person">
<h1>需求:水温达到50℃,或水位达到20cm,则联系服务器</h1>
<h2 id="demo">水温:{{temp}}</h2>
<h2>水位:{{height}}</h2>
<button @click="changePrice">水温+1</button>
<button @click="changeSum">水位+10</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,watch,watchEffect} from 'vue'
// 数据
let temp = ref(0)
let height = ref(0)
// 方法
function changePrice(){
temp.value += 10
}
function changeSum(){
height.value += 1
}
// 用watch实现,需要明确的指出要监视:temp、height
watch([temp,height],(value)=>{
// 从value中获取最新的temp值、height值
const [newTemp,newHeight] = value
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if(newTemp >= 50 || newHeight >= 20){
console.log('联系服务器')
}
})
// 用watchEffect实现,不用
const stopWtach = watchEffect(()=>{
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if(temp.value >= 50 || height.value >= 20){
console.log(document.getElementById('demo')?.innerText)
console.log('联系服务器')
}
// 水温达到100,或水位达到50,取消监视
if(temp.value === 100 || height.value === 50){
console.log('清理了')
stopWtach()
}
})
</script>
【标签的 ref 属性】
用在普通DOM
标签上
问题:
如果要获取标签内的元素,比如获取下方代码中h2标签的北京文字,用js的获取方法是document.getElementById('title2')
,这样获取存在着问题。
比如,下方代码是定义的一个组件,假设这个组件在App.vue里面引用,而App.vue里面有个元素的id也被定义为了title2,那么由于渲染顺序的,会执行不一样的结果。
1 | <template> |
解决办法:
用ref代替id
Person.vue:
1 | <template> |
App.vue:
1 | <template> |
结果:
用在组件标签上
App.vue:
1 | <template> |
Person.vue:
1 | <template> |
没有暴露:
暴露后:
【接口、泛型、自定义类型】
1
2
3
4
5
6
7
8
9
10 // 定义一个接口,用于限制person对象的具体属性
export interface PersonInter {
id: string,
name: string,
age: number
}
// 一个自定义类型
// export type Persons = Array<PersonInter> // 这种方法写也行
export type Persons = PersonInter[]
App.vue
中代码:
1
2
3
4
5
6
7 <template>
<Person ref="ren" />
</template>
<script lang="ts" setup name="App">
import Person from './components/Person.vue';
</script>
Person.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 <template>
<div class="person">
???
</div>
</template>
<script lang="ts" setup name="Person">
import { type PersonInter, type Persons } from '@/types' // 引入接口的时候前面要加上type,不然会报错,防止和引入的属性搞混
// let person: PersonInter = { id: 'asysasasak01', name: '张三', age: 60 } // 定义一个person对象符合PersonInter规范
// 数组如何用接口进行规范
// 下面语句含义:定义一个变量,这个变量是数组,并且数组里面的每一项要符合PersonInter规范
// let personList: Array<PersonInter> = [
// { id: 'asasajshajsh01', name: '张三', age: 60 },
// { id: 'asasajshajsh02', name: '李四', age: 90 },
// { id: 'asasajshajsh03', name: '王五', age: 6 },
// ]
let personList: Persons = [
{ id: 'asasajshajsh01', name: '张三', age: 60 },
{ id: 'asasajshajsh02', name: '李四', age: 90 },
{ id: 'asasajshajsh03', name: '王五', age: 6 },
]
</script>
【props】
1
2
3
4
5
6
7
8
9
10
11 // 定义一个接口,用于限制person对象的具体属性
export interface PersonInter {
id: string,
name: string,
age: number
x?:number // 加?表示x是可选参数,写不写都行
}
// 一个自定义类型
// export type Persons = Array<PersonInter>
export type Persons = PersonInter[]
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 <template>
<!-- 关于冒号 -->
<!-- 下面的传过去,a是字符,b是2,c是字符,d是9,有冒号的内容就会认定为表达式 -->
<!-- <h2 a="1+1" :b="1 + 1" c="x" :d="x"></h2> -->
<Person :list="personList" /> <!-- 如果list前面不加:,传过去的是字符串personlist -->
<!-- -->
</template>
<script lang="ts" setup name="App">
import { reactive } from 'vue';
import Person from './components/Person.vue';
import { type Persons } from '@/types';
let x = 9
// 这样写不太好,如果里面的属性名出错,提示不太清楚
// let personList: Persons = reactive([
// { id: 'asaddds01', name: '张三', age: 18 },
// { id: 'asaddds02', name: '李四', age: 20 },
// { id: 'asaddds03', name: '王五', age: 79 },
// ])
// 建议写法
let personList = reactive<Persons>([
{ id: 'asaddds01', name: '张三', age: 18 },
{ id: 'asaddds02', name: '李四', age: 20 },
{ id: 'asaddds03', name: '王五', age: 79, x: 9 },
])
console.log(personList)
</script>
Person.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 <template>
<div class="person">
<ul>
<!-- v-for: ?? in ?? -->
<!-- 不加key的话,会将索引值当做key(即下标1、2、3...)
问题:如果更新数据的话,会错乱(比如插入了数据,删除了数据)
-->
<!-- 注意key前面也要加:,使其成为表达式,不然就是字符串,这样所有的key都是一样的会报错 -->
<li v-for="person in list" :key="person.id">
{{ person.name }} -- {{ person.age }}
</li>
</ul>
</div>
</template>
<script lang="ts" setup name="Person">
// defineProps和withDefaults属于宏函数,不用引入
import { defineProps, withDefaults } from 'vue';
import { type Persons } from '@/types'
// 只接收list
// 存在的问题:如果传过来的list是一个数字,也不会报错
// defineProps(['list']) // 传一个也得写成数组形式
// // 接收list+限制类型
// defineProps<{ list: Persons }>()
// 接收list+限制类型+限制必要性+指定默认值
// ?是限制必要性 withDefaults指定默认值
withDefaults(defineProps<{ list?: Persons }>(), {
list: () => [{ id: 'sasjkas01', name: '鲨鲨鲨', age: 19 }]
})
// 接收list,同时将props保存起来
// let x = defineProps(['list'])
</script>
【生命周期】
概念:
Vue
组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue
会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子规律:
生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。
Vue2
的生命周期创建阶段:
beforeCreate
、created
挂载阶段:
beforeMount
、mounted
更新阶段:
beforeUpdate
、updated
销毁阶段:
beforeDestroy
、destroyed
Vue3
的生命周期创建阶段:
setup
挂载阶段:
onBeforeMount
、onMounted
更新阶段:
onBeforeUpdate
、onUpdated
卸载阶段:
onBeforeUnmount
、onUnmounted
常用的钩子:
onMounted
(挂载完毕)、onUpdated
(更新完毕)、onBeforeUnmount
(卸载之前)示例代码:
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<template>
<div class="person">
<h2>当前求和为:{{ sum }}</h2>
<button @click="changeSum">点我sum+1</button>
</div>
</template>
<!-- vue3写法 -->
<script lang="ts" setup name="Person">
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
// 数据
let sum = ref(0)
// 方法
function changeSum() {
sum.value += 1
}
console.log('setup')
// 生命周期钩子
onBeforeMount(()=>{
console.log('挂载之前')
})
onMounted(()=>{
console.log('挂载完毕')
})
onBeforeUpdate(()=>{
console.log('更新之前')
})
onUpdated(()=>{
console.log('更新完毕')
})
onBeforeUnmount(()=>{
console.log('卸载之前')
})
onUnmounted(()=>{
console.log('卸载完毕')
})
</script>
父子组件执行顺序:
下面vue2的执行顺序(vue3也是一样的顺序,只不过前面两个生命周期变成setup)
1.初始化与挂载生命周期的顺序 父组件beforeCreate => 父组件created => 父组件beforeMount =>
子组件beforeCreate => 子组件created => 子组件beforeMount =>
子组件mounted => 父组件mounted
父组件的生命周期到虚拟DOM挂载后开始执行子组件的生命周期,最后在执行父组件的真实DOM挂载
2.父子组件更新前后生命周期的顺序 父组件beforeUpdate => 子组件beforeUpdate =>子组件updated => 父组件updated
为了保证父组件的视图与子组件的数据同步,Vue 在子组件数据变化后先触发父组件的生命周期钩子函数,然后再更新子组件的视图
3.父子组件销毁前后生命周期的顺序 父组件beforeDestroy => 子组件beforeDestroy =>子组件destroyed => 父组件destroyed
当子组件全部销毁完成后,才会开始销毁父组件。这是为了确保子组件中的任何相关的依赖和引用在销毁父组件时不会出现问题。
【自定义hook】
什么是
hook
?—— 本质是一个函数,把setup
函数中使用的Composition API
进行了封装,类似于vue2.x
中的mixin
。自定义
hook
的优势:复用代码, 让setup
中的逻辑更清楚易懂。
示例代码:
useSum.ts
中内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import {ref,onMounted} from 'vue'
export default function(){
let sum = ref(0)
const increment = ()=>{
sum.value += 1
}
const decrement = ()=>{
sum.value -= 1
}
onMounted(()=>{
increment()
})
//向外部暴露数据
return {sum,increment,decrement}
}useDog.ts
中内容如下: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
28import {reactive,onMounted} from 'vue'
import axios,{AxiosError} from 'axios'
export default function(){
let dogList = reactive<string[]>([])
// 方法
async function getDog(){
try {
// 发请求
let {data} = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
// 维护数据
dogList.push(data.message)
} catch (error) {
// 处理错误
const err = <AxiosError>error
console.log(err.message)
}
}
// 挂载钩子
onMounted(()=>{
getDog()
})
//向外部暴露数据
return {dogList,getDog}
}组件中具体使用:
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<template>
<h2>当前求和为:{{sum}}</h2>
<button @click="increment">点我+1</button>
<button @click="decrement">点我-1</button>
<hr>
<img v-for="(u,index) in dogList.urlList" :key="index" :src="(u as string)">
<span v-show="dogList.isLoading">加载中......</span><br>
<button @click="getDog">再来一只狗</button>
</template>
<script lang="ts">
import {defineComponent} from 'vue'
export default defineComponent({
name:'App',
})
</script>
<script setup lang="ts">
import useSum from './hooks/useSum'
import useDog from './hooks/useDog'
let {sum,increment,decrement} = useSum()
let {dogList,getDog} = useDog()
</script>