Vue3 核心语法围绕响应式系统、组件化开发和组合式 API 创建,通过 <script setup> 简化模板编写,利用 refreactive 实现数据的响应式绑定,借助 computedwatch 处理派生数据和副作用。此外,Vue3 引入了更强大的生命周期钩子,大幅提升了开发效率和应用性能。

OptionsAPI 与 CompositionAPI

  • Vue2API 设计是 Options (配置)风格的
  • Vue3API 设计师 Composition (组合)风格的

Options API 用于 Vue2 及之前的版本,通常用于组织 Vue 组件的代码。Options API 将一个组件分割成了几个部分,如datacomputedmethodswatch等,使得组件的逻辑可以分而治之,易于理解和维护。但随着项目的复杂度增加,Options API 逐渐显示出一些不足之处,如代码重复、逻辑难以复用等。

Composition API 是 Vue3 引入的全新特性,它使用一种全新的方式来组织 Vue 组件的代码。与 Options API 相比,Composition API 更加灵活和可复用,可以将相关逻辑集中在一起,实现更好的代码组织和逻辑封装。通过 Composition API,我们可以更容易地实现代码复用和组件解耦,提高了组件的可维护性和可读性。

setup

学习 Vue3 中的第一步。组合式 API 需要写进 setup 里面,组件中所用到的:数据、方法、计算属性、监视等,均配置在 setup 中。

Vue3 的 setup 和 正常写 data、methods 有什么区别呢?

1、data 和 methods 可以和 setup 同时存在

2、data 和 methods 可以读取 setup 里面的数据,需使用 this 关键字

1
2
3
4
5
6
7
8
<script setup lang='ts'>
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
</script>

响应式框架

vue2 的响应式数据

1
2
3
4
// Vue2
data(){
return { x:900 }
}

Vue3 有两种方式:refreactive

ref 定义基本类型响应式数据

定义响应式变量,哪个是响应式就给哪个加

1
2
let name = ref('张三')
let age = ref('18')

在方法里面需要添加 .value

1
2
3
4
5
6
7
8
function changeName() {
name.value = 'zhang-san'
console.log(name.value)
}
function changeAge() {
age.value += 1
console.log(age.value)
}

reactive 定义对象类型的响应式数据

定义对象类型的响应式数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {reactive} from 'vue'

// 数据
let car = reactive({brand: '奔驰', price: 100})
let games = reactive([
{id: 'dsadasdasd01', name: '元'},
{id: 'dsadasdasd02', name: '新'},
{id: 'dsadasdasd03', name: '房'}
])

// 方法
function changPrice() {
car.price += 10
}
function changeFirstGame() {
games[0].name = '德'
}

ref 定义对象类型的响应式数据

ref 也可以定义对象类型的响应式数据,ref 遇到对象类型时,底层找的是 reactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {ref} from 'vue'

// 数据
let car = ref({brand: '奔驰', price: 100})
let games = ref([
{id: 'dsadasdasd01', name: '元'},
{id: 'dsadasdasd02', name: '新'},
{id: 'dsadasdasd03', name: '房'}
])

// 方法
function changPrice() {
car.value.price += 10
}
function changeFirstGame() {
games.value[0].name = '德'
}

ref 对比 reactive

  • ref 创建的对象必须使用 .value(可以使用 volar 插件自动添加 .value
  • reactive 重新分配一个新对象,会失去响应式(可以使用 Object.assign 去整体替换)
1
2
3
4
5
6
7
8
let car = reactive({brand: '奔驰', price: 100})

function changeCar() {
// 不是响应式数据
car = {brand: 'auto', price: 1}
// 使用 Object.assign
Object.assign(car, {brand: 'auto', price: 1})
}
  1. 若需要一个基本类型的响应式数据,必须使用 ref
  2. 若需要一个响应式对象,层级不深,refreactive 都可以
  3. 若需要一个响应式对象,且层级较深,推荐使用 reactive

toRefs 和 toRef

  • toRef:复制 Reactive 里的单个属性并转换成 ref
  • toRefs:复制 Reactive 里的所有属性并转换成 ref
1
2
3
4
5
6
7
import {reactive, toRefs}
let person = reactive({
name: '张三',
age: 18
})
let nl = toRef(person, 'age')
let {name, age} = toRefs(person)

toRefs 的作用是把 Reactive 对象转换为 Ref 类型的响应式数据,解构数据使其具备响应式能力。

应用场景:移动鼠标并实时显示鼠标的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { reactive, toRefs } from 'vue'
function usePostion(state, x, y) {
// 封装位置函数
let position = reactive({
x: 0,
y: 0
})
// 绑定鼠标移动事件
let onMouseMove = (event) => {
position.x = event.x
position.y = event.y
}
window.addEventListener('mousemove', onMouseMove)
// 返回
return toRefs(position)
}
// 接收 x, y 位置
let {x, y} = usePosition()
return {
x, y
}

案例中将提前封装好的 usePosition 函数通过 toRefs 返回一个响应式数据,然后直接拿来就用。在需要的地方引入进来即可,无需再重复声明。

计算属性

当我们需要将模板中的某一个数据进行一系列处理后得到一个新的值,虽然 Vue 的模板能够支持我们写一些表达式,但这样会使我们的模板变得更臃肿且不够灵活定制化。因此,Vue 推荐使用计算属性(Computed)来描述响应式状态的复杂逻辑。

Computed() 接受一个 getter 函数,返回一个 只读的响应式 ref 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {ref, computed} from 'vue'
let firstName = ref('zhang')
let lastName = ref('san')

// fullName 是计算属性,只可读不可改
let fullName = computed(()=>{
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value
})

// fullName 是计算属性,可读可改
let fullName = computed({
get(){
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value
}
set(val){
const [str1, str2] = val.split('-')
firstName.value = str1
lastName.value = str2
}
})

function changeFullName() {
fullName.value = 'li-si'
}

计算属性所依赖的数据发生变化它就重新计算。

计算属性是有缓存的,方法是没有缓存的

Watch

  • 作用:监视数据的变化。
  • 只能监视以下四种数据:
  1. ref 定义的数据
  2. reactive 定义的数据
  3. 函数返回一个值
  4. 一个包含上述内容的数组
  • ref 监视代码:
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
import {ref,watch} from 'vue'
// 数据
let sum = ref(0)
let person = ref({
name: '张三',
age: 18
})
// 方法
function changeSum() {
sum.value += 1
}
function changeName() {
person.value.name += '~'
}
function changeAge() {
person.value.age += 1
}
function changePerson() {
person.value = {name: '李四', age: 90}
}
// 监视【ref】定义的【基本类型】数据
const stopWatch = watch(sum, (newValue, oldValue)) => {
console.log('sum 变化了', newValue, oldValue)
if (newValue >= 10) {
stopWatch()
}
}
// 监视【ref】定义的【对象类型】数据
watch(person, (newValue, oldValue)=> {
console.log('person 变化了', newValue, oldValue)
}, {deep:true, immediate:true})
  • reactive 监视代码:默认是深度监视
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name: '张三',
age: 18
})
// 方法
function changeName() {
person.name += '~'
}
function changeAge() {
person.age += 1
}
function changePerson() {
Objext.assign(person, {name: '李四', age: 80})
}
// 监视【reactive】定义的【对象类型】数据,默认开启深度监视
watch(person, (newValue, oldPerson)=>{
console.log('person 变化了', newValue, oldValue)
})
  • refreactive 定义的【对象类型】数据中的某个属性

getter 函数:能放回一个值的函数

若该属性不是对象类型,则需要写成 函数 形式

若该属性依然是对象类型,建议写成 函数 形式

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
import {reactive, watch} from 'vue'
// 数据
let person = reactive({
name: '张三',
age: 18,
car: {
c1: '奔驰',
c2: '宝马'
}
})
// 方法
function changeName() {
person.name += '~'
}
function changeAge() {
person.age += 1
}
function changeC1() {
person.car.c1 = '奥迪'
}
function changeC2() {
person.car.c2 = '大众'
}
function changeCar() {
person.car = {c1: '雅迪', c2: '艾玛'}
}
// 监视响应式对象中的【基本类型】属性
watch(()=> person.name, ()=> {
console.log('person.name 发生变化了')
})
// 监视响应式对象中的【对象类型】属性
watch(()=>person.car, ()=>{
console.log('person.car 发生变化了')
}, {deep:true})
  • 监视上述多个数据
1
2
3
watch([()=>person.name, ()=>person.car.c1], (newValue, oldValue)=>{
console.log('多个属性发生变化了')
})

回调函数

Vue3 的组合式 API 中,回调函数 ()=>{}JavaScript 的箭头函数语法,用于定义匿名函数。这些函数可以在各种上下文中使用,比如事件处理、定时器、watch 监听器等。watch 函数的回调是用来响应被监视数据的变化。

1
2
3
watch(() => person.name, () => {
console.log('person.name 发生变化了');
});
  • watch 的第一个参数
    • 第一个参数 ()=>person.name 是一个箭头函数,它没有参数列表(用空括号表示),并且直接返回 person.name 的当前值
    • 这个函数会在每次 person.name 变化时被 watch 调用来获取最新的值
  • watch 的第二个参数
    • watch 的第二个参数是当被监视的数据发生变化时将执行的回调函数。这个回调函数接收两个参数:新的值和旧的值(如果你需要访问旧的值的话)。在例子中,回调函数并没有显式地声明参数,而是使用空的参数列表 () 并且只是简单打印了一条消息到控制台。
    • 如果想访问新旧值,可以这么写:
1
2
3
4
(newValue, oldValue) => {
console.log('新的值:', newValue)
console.log('旧的值:', oldValue)
}

每当 person.name 发生变化时,相应的 watch 监听器就会触发,并执行对应的回调函数。这使得你可以对这些变化作出反应,比如更新用户界面、发送网络请求、记录日志等。

  • 箭头函数 ()=>{} 在这里用作 watch 的参数,分别作为获取被监视数据的 getter 和当数据变化时执行的回调
  • 回调函数允许你在数据变化时执行特定的逻辑,而不需要明确知道是什么导致了变化
  • 使用 deep:true 可以确保即使对象内部的属性发生变化,也会触发监听器。

WatchEffect

响应式追踪其依赖

需求:当水温达到 60,或水位达到 80 时,向服务器发送请求

  • 使用 Watch 监视
1
2
3
4
5
6
7
watch([temp, height], (value)=>{
// 从 value 中获取最新的水温和水位
let [newTemp, newHeight] = value
if (newTemp >=60 || newHeight >= 80){
console('给服务器发请求')
}
})
  • 使用 WatchEffect 监视
1
2
3
4
5
watchEffect(()=>{
if (temp.value >= 60 || height.value >= 80) {
console.log('给服务器发送请求')
}
})

标签的 ref 属性

引用 <template> 里面的标签,用于注册模板引用

1
2
3
4
let title2 = ref()
function showLog() {
console.log(title2.value)
}
  • 用在普通 DOM 标签上,获取的是 DOM 节点
  • 用在组件标签上,获取的是组件实例对象

接口(Interfaces)和类型(Type)

在 TypeScript 中,接口用于定义对象的形状(shape),即对象的属性和数据类型。接口可以保证对象符合特定的结构,可以用于代码提示和错误检查。

除了接口,TypeScript 还提供了类型别名(type aliases),它可以用于定义新的类型名称,包括原始类型、联合类型、元组、对象类型等。

以下的代码定义了一个名为 PersonInter 的接口和一个名为 Persons 的类型别名。

  • 接口:有 idnameage 三个属性和对应的数据类型,使用 export 关键字将接口导出,使得它可以被其它模块导入和使用。

  • 类型别名:将 Persons 定义为 PersonInter 对象的数组,一个或包含多个 PersonInter 对象的数组类型,适用于表示多个人的信息,同样使用 export 关键字将类型别名导出,以便在其它模块中使用。

1
2
3
4
5
6
7
8
// 定义一个接口,用于限制 person 对象的具体属性
export interface PersonInter {
id: string
name: string
age: number
}
// 一个自定义类型
export type Persons = Array<PersonInter>
1
2
3
4
5
6
7
8
9
import {type PersonInter} from '@/types'

let person:PersonInter = {id:'sdsad001', name:'zhang-san', age:18}

let personList:Array<PersonInter> = [
{id: 's', name: 's', age: 80},
{id: 'a', name: 'd', age: 30},
{id: 'g', name: 'ds', age: 60}
]

Props

在 Vue3 中,Props 是用于从父组件向子组件传递数据的机制。子组件通过 defineProps 来声明它期望接收的 props,并且可以对这些 props 进行类型检查、设置默认值和指定是否为必填项。

  • defineProps:用于声明组件期望接收的 props。可以通过数组或对象的形式指定 props 的名称和类型
  • withDefaults:用于为 props 设置默认值,特别是当 props 是可选时特别好用
  • 类型:通过 TypeScript 的类型系统,可以确保传递给组件的 props 符合预期的类型,减少运行时错误
  • 响应式:使用 reactive 创建的 personList 是响应式的,当它发生变化时,依赖它的组件会自动更新

父组件

1
2
<!-- APP.vue -->
<Person a="哈哈" :list="personList"/>
  • 自定义一个子组件 <Person>
  • a="哈哈":传递一个名为 a 的 prop,其值为字符串 "哈哈"
  • :list="personList":传递一个名为 list 的 prop,其值为 personList 变量的内容。注意这里的冒号 : 表示使用 v-bind 指令,即动态绑定属性。

定义 personList 变量

1
2
3
4
5
let personList = reactive<Persons>)([
{id: 's', name: 's', age: 80},
{id: 'a', name: 'd', age: 30},
{id: 'g', name: 'ds', age: 60}
])

定义一个 Persons 类型的数组,关于 Persons 类型,在上一节的接口和类型中有定义,PersonsPersonInter 类型的别名,因此,personList 是由多个 PersonInter 对象组成的数组。

子组件

  • 方法 1:只接收 list
1
2
// 只接收 list
defineProps(['list'])
  • 方法 2:接收 list 并限制类型
1
2
// 接收 List + 限制类型
defineProps<{list:Persons}>()
  • 方法 3:接收 list 并限制类型、必要性及默认值
1
2
3
4
5
6
// 接收 list + 限制类型 + 限制必要性 + 指定默认值
withDefaults(defineProps<{list?:Persons}>(), {
list:()=>[{
id: 'sdasdasdas', name:'dsa-dsad-sad', age: 19
}]
})
  • 方法 4:接收多个 props 并打印
1
2
let x = defineProps(['a', 'list'])
console.log(x)

生命周期

Vue3 的生命周期是组件从创建到销毁过程中的一系列钩子函数,这些钩子函数允许开发者在组件的不同阶段执行特定的操作。

  • 生命周期、生命周期函数、生命周期钩子;
  • 引入了 setup 函数作为新的入口点,允许开发者更灵活地组织逻辑;
  • 生命周期钩子需要通过导入相应的方法来使用,如 onMountedonUnmounted等;
  • 提供了更好的逻辑复用机制(Composables),可以将相关逻辑封装成独立的函数,提升代码的可维护性和可测试性。

创建

使用 setup 模拟,代替了 Vue2 中的 beforeCreateCreated 钩子。setup 函数在组件实例创建后立即被调用,此时可以访问到组件的响应式状态。

1
console.log('创建')

挂载

onBeforeMount():在组件挂载前被调用,此时模板已编译,但尚未插入到 DOM 中。

1
2
3
onBeforeMount(()=>{
console.log('挂载前')
})

onMounted():在组件被挂载到 DOM 后立即调用,此时可以安全地进行 DOM 操作

1
2
3
onMount(()=>{
console.log('挂载完毕')
})

更新

onBeforeUpdate():在数据更新后,DOM 更新之前被调用,此时可以访问旧的 DOM 状态。

1
2
3
onBeforeUpdate(()=>{
console.log('更新前')
})

onUpdated():在组件更新后被调用。此时 DOM 已完成更新

1
2
3
onUpdated(()=>{
console.log('更新完毕')
})

卸载

onBeforeUnmount():在组件实例被卸载之前调用,此时组件仍然可以访问。

1
2
3
onBeforeUnmount(()=>{
console.log('卸载前')
})

onUnmounted():在组件实例被卸载后调用,此时组件的所有指令与事件监听器已被解绑。

1
2
3
onUnmounted(()=>{
console.log('卸载完毕')
})

hooks

“Hooks” 通常指的是 组合式API(Composition API) 中的函数, 它们用于封装和复用有状态逻辑。在 Vue3 中,Hooks 是指通过 setup 函数或自定义组合函数(Composables)封装的逻辑,这些逻辑可以包含响应式状态、计算属性、生命周期钩子、事件处理等。Hooks 的主要目的是将相关的逻辑提取到独立的函数中,使得代码更加模块化、可复用,并且更容易维护。

自定义组合函数是 Vue3 中最常用的 Hook 形式。通常是前缀带有 use 的函数(如 useMouseuseFetch 等),用于封装特定的逻辑,并返回需要暴露的状态或方法。

示例:useDogs 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/hooks/useDogImages.ts
import { ref } from 'vue'
import axios from 'axios'

export function useDogImages() {
// 创建一个响应式的 dogList 数组
let dogList = ref([
'https://images.dog.ceo/breeds/pembroke/n02113023_4024.jpg'
])
// 定义 getDog 方法,从 API 获取一张随机狗狗图片并添加到 dogList 中
async function getDog() {
try {
let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
dogList.value.push(result.data.message)
} catch (error) {
alert (error)
}
}
// 返回 dogList 和 getDog 方法,以便在组件中使用
return {dogList, getDog};
}

接下来,创建一个 Vue 组件来使用 useDogImages 组合函数,并在模板中添加按钮和图片展示区域

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>
<button @click='getDog' class="fetch-button">获取新图片</button>
<div class="dog-images">
<img v-for="(url, index) in dogList" :key="index" :src="url" alt="狗狗图片"/>
</div>
</div>
</template>

<script setup lang='ts'>
import { useDogImages } from '../hooks/useDogImages'
let { dogList, getDog } = useDogImages()
</script>

<style scoped>
.dog-images img {
width: 200px;
margin: 10px;
border-radius: 8px;
}
.fetch-button {
background-color:rgb(50, 149, 189); /* 绿色背景 */
color: white; /* 白色文本 */
}
</style>