目录

1. 介绍

1.1 Vue是什么

1.2 为什么学习Vue

1.3 渐进式框架

1.4 Vue版本

2. 环境准备

2.1 Node.js 安装

2.2 基于vite创建

2.3 简单介绍

 2.4 模版语法

3. Vue3基础知识

3.1 什么是Vue3

3.2 Vue3的起点setup

 setup 语法糖

3.3 ref 基本类型的响应式数据

3,4 reactive 对象类型的响应式数据

3.5  ref 对象类型的响应式数据

3.6 toRefs 与 toRef

3.7 computed 计算属性

基础用法

Getter & Setter

特性总结

3.8 watch 监视

 情况一 较多使用

情况二

情况三

 情况四 较多使用

 情况五

停止监视

 3.9 watchEffect

3.10 标签的 ref 属性

 3.11 TS 接口,泛型,自定义类型

接口(Interfaces)

泛型(Generics)

自定义类型(Custom Types)

 3.12 props 父传子数据

1. 父组件中准备数据

2. 子组件中声明props

 3.13 生命周期

Vue2的生命周期

Vue3的生命周期

 3.14 自定义hook

4. Vue3进阶知识

4.1 路由

4.2 Vue Router

创建路由

集成Vue Router到Vue应用

使用路由

4.3 两个注意点

4.4 to的两种写法

4.5 路由器的工作模式

4.6 命名路由

4.7 嵌套路由

设置路由

使用嵌套路由 

 4.8 路由传参

query参数

params参数

4.9 路由的props配置

4.10 replace属性

4.11 编程式导航

 4.12 重定向

5. Pinia

Pinia 的特点

5.1、安装 Pinia

5.2、创建 Store

创建一个 store 文件 counter.ts

5.3、使用 Store

在组件中使用 store

5.4、修改pinia数据

1、第一种修改方式,直接修改

2、第二种修改方式:批量修改

3、第三种修改方式:借助action修改(action中可以编写一些业务逻辑)

5.5 storeToRefs

5.6 getters

5.7 $subscribe 订阅

 5.8 store组合式写法

6. 组件通信

6.1. 【props】

6.2. 【自定义事件】

6.3. 【mitt】

6.4.【v-model】

6.5.【$attrs 】

6.6. 【$refs、$parent】

6.7. 【provide、inject】

6.8. 【pinia】

6.9. 【slot】

1. 默认插槽

2. 具名插槽

更简洁的语法:

3. 作用域插槽

7. 其它 API

7.1.【shallowRef 与 shallowReactive 】

shallowRef

使用示例:

shallowReactive

使用示例:

总结

选择哪个?

7.2.【readonly 与 shallowReadonly】

readonly

使用示例:

shallowReadonly

使用示例:

总结

选择哪个?

7.3.【toRaw 与 markRaw】

toRaw

使用示例:

markRaw

使用示例:

总结

选择哪个?

7.4.【customRef】

使用 customRef 的基本步骤:

总结

8. Vue3新组件

8.1. 【Teleport】

Teleport 的基本用法

HTML 结构

Vue 组件中使用 Teleport

8.2. 【Suspense】

Suspense 的基本用法

异步组件

使用 Suspense

特性

注意事项

总结

8.3.【全局API转移到应用对象】

移动的全局 API

示例

创建 Vue 应用实例

注册全局组件

注册全局指令

注册插件

配置选项

挂载应用

总结


1. 介绍

在本篇文章中,我们将详细讲解如何使用Vue3,从基本概念到高级技巧,最终通过一个项目实战,让大家全面掌握Vue3开发技能。

Vue基于标准HTML,CSS,JavaScript构建,并提供了一套声明式,组件化的编程模型,帮助你高效地开发用户页面。无论是简单还是复杂页面,Vue都可以胜任

1.1 Vue是什么

渐进式JavaScript框架,易学易用,性能出色,适用场景丰富的Web前端框架

1.2 为什么学习Vue

  • Vue是目前前端最火的框架之一
  • Vue是目前企业技术栈中要求的知识点
  • Vue可以提升开发体验
  • ....

1.3 渐进式框架

Vue是一个框架,也是一个生态。其功能覆盖了大部分前端开发常见的需求。但web世界是十分多样化的,不同的开发者在web上构建的东西可能在形式和规模上会有很大的不同。考虑到这一点,Vue的设计非常注重灵活性和“可以被逐步集成”这个特点。

1.4 Vue版本

目前,在开发中,Vue有两大版本vue2vue3,老项目一般都是vue2,但是因为vue2已经停止维护了,新的项目一般都是会选择vue3开发(vue3涵盖了vue2的知识体系,当然vue3也增加了许多新的特性)

2. 环境准备

2.1 Node.js 安装

Windows 安装

  1. 访问官方网站下载: 访问Node.js官方网站 Node.js — Run JavaScript Everywhere ,选择"Downloads"页面,找到适合你系统的安装包(推荐使用LTS版本,即长期支持版,更稳定)。

  2. 运行安装程序: 下载完成后,双击安装包开始安装。在安装界面,你可以选择是否加入PATH环境变量(建议勾选),这样安装完成后就可以直接在命令行中使用nodenpm命令。

  3. 验证安装: 安装完成后,打开命令提示符(cmd)或PowerShell,输入以下命令检查安装是否成功:

node -v
npm -v

分别会显示Node.js和npm(Node包管理器)的版本号。 

2.2 基于vite创建

vite 是新一代前端构建工具

优势

  • 轻量快速的热重载,能实现极速的服务启动。
  • 对 ts、jsx、css等支持开箱即用。
  • 真正的按需求编辑,不再等待整个应用编辑完成。
  • webpack构建与vite构建对比图如下:

  • 具体操作如下: 
## 1.创建命令
npm create vue@latest

## 2.具体配置
## 配置项目名称
√ Project name: vue3_test
## 是否添加TypeScript支持
√ Add TypeScript?  Yes
## 是否添加JSX支持
√ Add JSX Support?  No
## 是否添加路由环境
√ Add Vue Router for Single Page Application development?  No
## 是否添加pinia环境
√ Add Pinia for state management?  No
## 是否添加单元测试
√ Add Vitest for Unit Testing?  No
## 是否添加端到端测试方案
√ Add an End-to-End Testing Solution? » No
## 是否添加ESLint语法检查
√ Add ESLint for code quality?  Yes
## 是否添加Prettiert代码格式化
√ Add Prettier for code formatting?  No

总结:

  • Vite 项目中,index.html 是项目的入口文件,在项目最外层。

  • 加载index.html后,Vite 解析 <script type="module" src="xxx"> 指向的JavaScript

  • Vue3中是通过 createApp 函数创建一个应用实例。  

2.3 简单介绍

.vscode --- VSCode工具的配置文件夹
node_modules --- Vue项目的运行依赖文件夹
public --- 资源文件夹(浏览器图标)
src --- 源码文件夹
.gitignore --- git忽略文件
index.html --- 如果HTML文件
package.json --- 信息描述文件
README.md --- 注释文件
vite.config.js --- Vue配置文件

 2.4 模版语法

 main.ts

// 引入createApp用于创建应用
import { createApp } from "vue";
// 引入App根组件
import App from "./App.vue";

createApp(App).mount("#app");

 App.vue

<template>
    <!-- html -->
</template>

<script lang="ts">
    // JS 或 TS
</script>

<style>
    /* css样式 */
</style>

 文本插值

最基本的数据绑定形式是文本插值,它使用的是"Mustache"语法(即双大括号):

<template>
    <div class="text">
         <h3>模版语法</h3>
         <p>{{ msg }}</p>
    </div>
</template>

<script setup lang="ts">
    import { ref } from 'vue'

    const msg = ref('神奇的语法')
    
</script>

<style scoped>
    .text{
        background-color: #f10808;
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
    }
</style>

 

使用 JavaScript 表达式 

每个绑定仅支持单一表达式,也就是一段能够被求值的JavaScript代码。一个简单的判断方法是是否合法地写在 return 后面

<template>
    <div class="text">
         <h3>模版语法</h3>
         <p>{{ count+1 }}</p>
         <p>{{ ok? 'YES':'NO' }}</p>
    </div>
</template>

<script setup lang="ts">
    import { ref,onMounted } from 'vue'

    const count = ref(0)

    const msg = ref('神奇的语法')

    const ok = true
    
</script>

<style scoped>
    .text{
        background-color: #f10808;
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
    }
</style>

无效

<!-- 这是一个语句,而非表达式 -->
{{ var a = 1 }}

<!-- 条件控制也不支持,请使用三元表达式 -->
{{ if(ok) { return message } }}

3. Vue3基础知识

3.1 什么是Vue3

  • Vue2 的API设计是Option(配置)风格的。
  • Vue3 的API设计是Composition(组合)风格的。

Option API的弊端

Options类型的 API ,数据、方法、计算属性等、是分散在:datamethodscomputed 中的,若想新增或者修改一个需求,就需要分别修改:datamethodscomputed,不便于维护和复用。

Composition API的优势

可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。

3.2 Vue3的起点setup

setup 概述

setup 是 Vue3 中一个新的配置项,值是一个函数,它是 composition API "表演的舞台",组件中所用到的:数据,方法,计算属性,监视....等等,均配置在 setup 中。

特点如下:

  • setup 函数返回的对象中的内容,可直接在模版中使用。
  • setup 中访问 this 是 undefined
  • setup 函数会在 beforeCreate 之前调用,它是"领先"所有钩子执行的。
<template>
    <!-- html -->
     <div class="person">
        <h2>姓名:{{ name }}</h2>
        <h2>年龄:{{ age }}</h2>
        <button @click="changeName">修改名字</button>
        <button @click="changeAge">修改年龄</button>
        <button @click="showTel">查看联系方式</button>
     </div>
</template>

<script lang="ts">
    // JS 或 TS
    export default {
        name: 'Person', // 组件名称
        setup(){
            // 数据
            let name = '张三' // 注意这样写name不是响应式数据
            let age = 12 // 注意这样写age不是响应式数据
            let tel = '138888888'

            // 方法
            function changeName(){
                name = '李四'
            }
            function changeAge(){
                age += 1
            }
            function showTel(){
                alert(tel)
            }
            // 将数据,方法交出去,模版中才可以使用
            return {name,age,tel,changeName,changeAge,showTel}
        }
    }
</script>

<style>
    /* css样式 */
    .person{
        background-color: skyblue;
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
    }
    button{
        margin: 0 5px;
    }
</style>
 setup 语法糖

setup 函数有一个语法糖,这个语法糖,可以让我们把 setup 独立出去,代码如下:

<template>
    <!-- html -->
     <div class="person">
        <h2>姓名:{{ name }}</h2>
        <h2>年龄:{{ age }}</h2>
        <button @click="changeName">修改名字</button>
        <button @click="changeAge">修改年龄</button>
        <button @click="showTel">查看联系方式</button>
     </div>
</template>

<script setup lang="ts" name="Person234">
    // 数据
    let name = '张三' // 注意这样写name不是响应式数据
    let age = 12 // 注意这样写age不是响应式数据
    let tel = '138888888'

    // 方法
    function changeName(){
        name = '李四'
    }
    function changeAge(){
        age += 1
    }
    function showTel(){
        alert(tel)
    }
</script>

<style>
    /* css样式 */
    .person{
        background-color: skyblue;
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
    }
    button{
        margin: 0 5px;
    }
</style>

扩展:上述代码,还需要编写一个不写 setup 的 script 标签,去指定组件名字,比较麻烦,我们可以借助 vite 中的插件简化

  1. 第一步: npm i vite-plugin-vue-setup-extend -D 
  2. 第二步:vite.config.ts
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    VueSetupExtend()
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

3.3 ref 基本类型的响应式数据

作用:定义响应式变量。

语法: let xxx = ref(初始值) .

返回值:一个 RefImpl 的实例对象,简称 ref 对象 或 ref ref 对象的 value 属性是响应式的。

注意点:

  •  JS 中操作数据需要: xxx.value ,但模版中不需要 .value , 直接使用即可。
  • 对于 let name = ref('张三') 来说, name 不是响应式的, name.value 是响应式的。
<template>
    <!-- html -->
     <div class="person">
        <h2>姓名:{{ name }}</h2>
        <h2>年龄:{{ age }}</h2>
        <h2>地址:{{ address }}</h2>
        <button @click="changeName">修改名字</button>
        <button @click="changeAge">修改年龄</button>
        <button @click="showTel">查看联系方式</button>
     </div>
</template>

<script setup lang="ts" name="Person234">
    import { ref,onMounted } from 'vue'
    // 数据
    let name = ref('张三') // 注意这样写name不是响应式数据
    let age = ref(12) // 注意这样写age不是响应式数据
    let tel = '138888888'
    let address = '北京'
    // 方法
    function changeName(){
        name.value = '李四'
    }
    function changeAge(){
        age.value += 1
    }
    function showTel(){
        alert(tel)
    }
</script>

<style>
    /* css样式 */
    .person{
        background-color: skyblue;
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
    }
    button{
        margin: 0 5px;
    }
</style>

3,4 reactive 对象类型的响应式数据

作用:定义一个响应式对象(基本类型不要用它,要用ref,否则报错)

语法:let 响应式对象= reactive(源对象)

 返回值:一个Proxy的实例对象,简称:响应式对象。

注意点:reactive定义的响应式数据是“深层次”的。

<template>
    <div class="text">
        <h1>汽车信息:</h1>
        <h2>一辆{{ car.brand }}车</h2>
        <h2>价值:{{ car.price }}万</h2>
        <button @click="changePrice">修改汽车的价格</button>
        <hr>
        <h1>游戏列表:</h1>
        <ul>
            <li v-for="(game,index) in games" :key="index">{{ game.name }}</li>
        </ul>
        <button @click="changeFirstGame">修改第一个游戏的名字</button>
    </div>
</template>

<script setup lang="ts">
import { reactive } from 'vue';
    // 数据
    let car = reactive({
        brand: '奔驰',
        price: 100
    })
    let games = reactive([
        {id:'aysdytfastr01',name:'王者荣耀'},
        {id:'aysdytfastr02',name:'原神'},
        {id:'aysdytfastr03',name:'火影忍者'},
    ])
    // 方法
    function changePrice() {
        car.price += 10
    }
    function changeFirstGame(){
        games[0].name = '绝地求生'
    }
</script>

<style scoped>
.text {
    background-color: #f10808;
    box-shadow: 0 0 10px;
    border-radius: 10px;
    padding: 20px;
    margin: 20px 0px;
}
li {
    font-size: 20px;
}
</style>

3.5  ref 对象类型的响应式数据

 其实ref接收的数据可以是:基本类型(详细可以看3.3)、对象类型

1、定义与初始化:

使用ref函数可以将一个初始值(不论是基本类型还是对象/数组)封装成一个响应式引用。例如,定义一个对象类型的响应式数据:

import { ref } from 'vue';
const user = ref({ name: '张三', age: 25 });

在这个例子中,user是一个响应式引用,其.value属性包含实际的对象数据。

2、访问与更新:

访问ref包裹的值时,需要通过.value来读取或修改其内部的值。在JavaScript代码中: 

console.log(user.value.name); // 输出: 张三
user.value.name = '李四'; // 更新用户名

而在Vue模板中,可以直接使用user,Vue会自动解包.value给你:

<template>
  <div>{{ user.name }}</div>
</template>

3、 内部机制:

  • 对于基本类型,ref通过Object.defineProperty来实现响应式。
  • 对于对象或数组这类复杂类型,虽然你直接使用ref,但其内部实际上会使用reactive来创建一个深层次的响应式代理对象,然后将其包装在一个简单的包装器中,暴露.value属性。

4、响应性

ref包裹的对象内部属性发生变化时,Vue的响应系统会自动追踪并触发依赖更新,确保UI能够相应地更新。

5、与reactive的区别

ref    ===> 基本 对象
reactive => 对象

区别  

 ref 创建的变量必须使用 .value (可以使用volar插件自动添加.value).

 reactive 重新分配一个新对象,会失去响应式。(可以使用 去整体替换)。

reactive 更改对象
// car = {brand:'保时捷',price:100}  这样写页面是不刷新的
// car = reactive({brand:'宝马',price:200}) 这样写页面是不刷新的

Object.assign(car, {brand:'宝马',price:200})

ref     更改对象
car.value = {brand:'保时捷',price:100}

使用原则

1、若需要一个基本类型的响应式数据,必须使用 ref 

2、若需要一个响应式对象,层级不深、refreactive 都可以。

3、若需要一个响应式对象,且层级较深,推荐使用 reactive 。 

3.6 toRefs 与 toRef

作用:将一个响应式对象中的每一个属性,转换为ref对象。

备注:toRefstoRef功能一致,但toRefs可以批量转换。

总结

  • toRef用于将响应式对象的一个属性转换为一个独立的ref对象,便于单独操作和传递。
  • toRefs则是将整个响应式对象转换为一个各属性均为独立ref对象的普通对象,适合需要分解响应式对象以便于外部使用的情况。
<template>
    <div class="person">
        <h3>姓名: {{ name }}</h3>
        <h3>年龄:{{ person.age }},{{n1}}</h3>
        <button @click="changeName">修改名字</button>
        <button @click="changeAge">修改年龄</button>
    </div>
</template>

<script setup lang="ts">
import { reactive,toRefs,toRef } from 'vue';

    // 用户属性
    let person = reactive({
        name: '张三',
        age: 18
    })

    let {name, age} = toRefs(person)
    let n1 = toRef(person, 'age')
    console.log(n1.value);
    
    
    // 方法
    function changeName() {
        name.value += '~';
        console.log(name.value);
    }
    function changeAge() {
        age.value += 1;
        console.log(age.value);
    }

</script>

<style scoped>
    .person{
        background-color: yellow;
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
    }
    button{
        margin: 0 5px;
    }
    h3{
        text-align: center;
    }
</style>

3.7 computed 计算属性

作用:根据已有数据计算出新数据(和Vue2中的computed作用一致)。

基础用法

在Vue 3的Composition API中,你不再直接在组件的options对象里定义computed属性,而是使用setup函数,通过import { computed } from 'vue'导入computed函数来定义计算属性。

import { ref,computed } from 'vue';

let firstName = ref('zhang');
let lastName = ref('san');

let fullName = computed(() => {
     return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + '_' + lastName.value 
})

在这个例子中,fullName就是一个计算属性,它依赖于firstNamelastName的值,并且只有当firstName或lastName的值变化时,fullName才会重新计算。 

Getter & Setter

Vue 3的计算属性同样支持定义getter和setter,允许你不仅读取而且修改计算属性的值,并触发相应的副作用。

const fullName = computed({
  get() {
    return this.firstName + ' ' + this.lastName;
  },
  set(val) {
    const [str1,str2] = val.split("_")
    firstName.value = str1
    lastName.value = str2
  }
});
特性总结
  • 响应式依赖:计算属性自动追踪依赖,当依赖的数据变化时自动重新计算。
  • 缓存机制:计算属性的结果会被缓存,直到依赖项变化,这提高了应用的性能。
  • 组合API集成:在setup函数中使用,与Vue 3的其他 Composition API 特性无缝集成。
  • getter/setter:可以定义setter来改变计算属性所依赖的数据,从而提供了更灵活的数据处理能力。

与其他API的关系

在Vue 3中,computed常与refreactive一起使用,前者用于创建基本类型的响应式引用,后者用于创建复杂对象或数组的响应式代理。computed结合这些API可以构建复杂的响应式逻辑,同时保持代码的清晰和高效。

3.8 watch 监视

作用:watch是一个用于观察和响应Vue实例上的数据变化的重要特性。它主要用于在某些数据变化时执行异步或开销较大的操作,或者执行那些不应该在每个组件渲染周期内运行的代码。

特点: Vue3 中的 watch 只能监视以下四种数据

  1.  ref 定义的数据。
  2.  reactive 定义的数据。
  3. 函数返回的一个值。
  4. 一个包含上述内容的数组。
 情况一 较多使用

监视 ref 定义的【基本类型】数据:直接写数据名即可,监视的是其 value 值的改变。

语法

watch(谁?, 回调函数)

import { ref,watch } from 'vue';

// 数据
let sum = ref(0)
    
// 方法
function changeSum(){
    sum.value += 1
}
// 监视
watch(sum, (newValue, oldValue)=>{
    console.log('sum变了',newValue, oldValue);
})

使用watch函数监视sum的变化。当sum.value的值发生变化时,传入的回调函数会被调用,其中newValue是变化后的新值,oldValue是变化前的旧值。这段代码会打印出sum变化的前后值。 

 

情况二

监视 ref 定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。

注意:

  • 若修改的是 ref 定义的对象中的属性, newValue oldValue 都是新值,因为它们是同一个对象。
  • 若修改的是 ref 定义的对象, newValue 是新值, oldValue 是旧值,因为不是同一个对象了。

 深度监视

如果你想监视对象内部属性的变化,需要设置deep: true选项:

import { ref, watch} from 'vue';

// 数据
let person = ref({
    name:'张三',
    age: 18
})

// 监视
watch(person, (newValue, oldValue) => {
    console.log('person变化了', newValue, oldValue);
},{deep:true})

watch的第一个参数是:被监视的数据

watch的第二个参数是:监视的回调

watch的第三个参数是:配置对象(deepimmediate等等.....)

情况三

监视 reactive 定义的【对象类型】数据,且默认开启了深度监视,且是不可以关闭的。

import { reactive,watch } from 'vue'
// 数据
let person = reactive({
    name:'张三',
    age: 18
})

// 监视
watch(person, (newValue, oldValue) => {
    console.log('person变化了', newValue, oldValue);
})
 情况四 较多使用

监视 ref reactive 定义的【对象类型】数据中的某个属性,注意点如下:

  1. 若该属性不是【对象类型】,需要写成函数形式。
  2. 若该属性值依然是【对象类型】,可直接编,也可写成函数,不过建议写成函数。

结论

监视若是对象里的属性,那么最好写函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。

import {reactive,watch} from 'vue'

// 数据
let person = reactive({
    name: '张三',
    age:18,
    car:{
        c1:'奔驰',
        c2:'宝马'
    }
})

// 监视,情况四:监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式
watch(()=> person.name, (newValue, oldValue) => {
    console.log('person.name 变化了',newValue, oldValue);
})
// 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数式,更推荐写函数
watch(() => person.car, (newValue, oldValue)=>{
    console.log('person.car 变化了',newValue, oldValue)
},{deep: true})
 情况五

监视上述的多个数据

import {reactive,watch} from 'vue'

// 数据
let person = reactive({
    name: '张三',
    age:18,
    car:{
        c1:'奔驰',
        c2:'宝马'
    }
})

// 监视,情况五:
watch([ () => person.name, () => person.car.c1], (newValue, oldValue)=>{
    console.log('person.car 变化了',newValue, oldValue)
},{deep: true})

// 监视,情况五:
watch([ () => person.name, person.car], (newValue, oldValue)=>{
    console.log('person.car 变化了',newValue, oldValue)
},{deep: true})
停止监视
// 停止监视
const stopWatch = watch(sum, (newValue, oldValue)=>{
    console.log('sum变了',newValue, oldValue);
    if(newValue > 5){
        alert('你妈了个逼的,上次白银山就应该弄死你的!')
        sum.value = oldValue
        stopWatch()
    }
})

 3.9 watchEffect

watchEffect是Vue 3引入的一个重要功能,它是Composition API的一部分,用于自动收集依赖并执行副作用函数,每当这些依赖发生变化时,副作用函数就会重新运行。这使得开发者能够更容易地处理响应式数据变化带来的副作用,如数据更新后的DOM操作、发送网络请求等。

官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改是重新执行该函数。

watch对比watchEffect

  1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
  2.  watch :要明确指出监视的数据
  3.  watchEffect :不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)
  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()
    }
  })

3.10 标签的 ref 属性

作用:用于注册模板引用。

  • 用在普通DOM标签上,获取的是DOM节点。

  • 用在组件标签上,获取的是组件实例对象。

用在普通DOM标签上:  

<template>
  <div class="person">
    <h1 ref="title1">尚硅谷</h1>
    <h2 ref="title2">前端</h2>
    <h3 ref="title3">Vue</h3>
    <input type="text" ref="inpt"> <br><br>
    <button @click="showLog">点我打印内容</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {ref} from 'vue'
	
  let title1 = ref()
  let title2 = ref()
  let title3 = ref()

  function showLog(){
    // 通过id获取元素
    const t1 = document.getElementById('title1')
    // 打印内容
    console.log((t1 as HTMLElement).innerText)
    console.log((<HTMLElement>t1).innerText)
    console.log(t1?.innerText)
    
		/************************************/
		
    // 通过ref获取元素
    console.log(title1.value)
    console.log(title2.value)
    console.log(title3.value)
  }
</script>

 用在组件标签上:

<!-- 父组件App.vue -->
<template>
  <Person ref="ren"/>
  <button @click="test">测试</button>
</template>

<script lang="ts" setup name="App">
  import Person from './components/Person.vue'
  import {ref} from 'vue'

  let ren = ref()

  function test(){
    console.log(ren.value.name)
    console.log(ren.value.age)
  }
</script>


<!-- 子组件Person.vue中要使用defineExpose暴露内容 -->
<script lang="ts" setup name="Person">
  import {ref,defineExpose} from 'vue'
	// 数据
  let name = ref('张三')
  let age = ref(18)
  /****************************/
  /****************************/
  // 使用defineExpose将组件中的数据交给外部
  defineExpose({name,age})
</script>

 3.11 TS 接口,泛型,自定义类型

TypeScript 提供了丰富的类型系统,帮助开发者在编码阶段就能捕获潜在的错误,增强代码的可读性和健壮性。以下是关于 TypeScript 中接口(Interfaces)、泛型(Generics)和自定义类型(Custom Types)的基本概念和使用方法。

一般在引入ts接口是都会创建一个和components文件夹同级的types文件夹

 

接口(Interfaces)

接口用于定义对象的形状(shape),即描述一个对象需要具备的属性和方法。接口可以用于类的实现,也可以用于变量、函数参数或返回值的类型注解。

在types文件夹下ts文件

export interface PersonInter {
    id:string;
    name:string;
    age:number;
}

如果增加一个可有可无的属性

export interface PersonInter {
    id:string;
    name:string;
    age:number;
    x?:number;
}
泛型(Generics)

泛型允许你在定义函数、接口或类的时候不预先指定具体的类型,而是在使用的时候指定。这增加了代码的重用性和灵活性。

import {type PersonInter, type Persons} from '@/types/index'

let person:PersonInter = {id:'asdgdsfg1gj01',name:'张三',age:18}
自定义类型(Custom Types)

自定义类型可以是类型别名(type aliases)或枚举(enums),它们提供了另一种方式来定义复杂的类型结构。

 同样是在types文件夹下的ts文件中,对前面定义的接口进行改造

export interface PersonInter {
    id:string;
    name:string;
    age:number;
}

// 一个自定义类型 两种写法任选其一
// export type Persons = Array<PersonInter> 
export type Persons = PersonInter[];

 3.12 props 父传子数据

父子组件之间传递数据主要通过props实现,这是一种单向数据流的方式,即数据从父组件流向子组件。

1. 父组件中准备数据

在父组件中,你需要准备要传递给子组件的数据。这个数据可以是任何有效的JavaScript数据类型,包括基本类型(如字符串、数字)和复杂类型(如对象、数组)。

从其他子组件传过来的数据

defineExpose

<!-- 父组件App.vue -->
<template>
  <Person ref="ren"/>
  <button @click="test">测试</button>
</template>

<script lang="ts" setup name="App">
  import Person from './components/Person.vue'
  import {ref} from 'vue'

  let ren = ref()

  function test(){
    console.log(ren.value.name)
    console.log(ren.value.age)
  }
</script>


<!-- 子组件Person.vue中要使用defineExpose暴露内容 -->
<script lang="ts" setup name="Person">
  import {ref,defineExpose} from 'vue'
	// 数据
  let name = ref('张三')
  let age = ref(18)
  /****************************/
  /****************************/
  // 使用defineExpose将组件中的数据交给外部
  defineExpose({name,age})
</script>
2. 子组件中声明props

在子组件中,你需要声明你期望从父组件接收的数据。这可以通过props选项完成,可以定义每个prop的名称、类型和是否必需。

这里可以根据 3.11 TS接口 来规范数据

// 定义一个接口,限制每个Person对象的格式
export interface PersonInter {
id:string,
name:string,
 age:number
}

// 定义一个自定义类型Persons
export type Persons = Array<PersonInter>

 父传子

<template>
    <!-- html -->
     <div class="app">
        <person a="哈哈哈" :list="personList"/> 
        <test/>
     </div>
</template>

<script lang="ts" setup>
    import Person from './components/Person.vue'
    import test from './components/text.vue'
    import {reactive} from 'vue'
    import {type Persons} from '@/types/index'
    let personList = reactive<Persons>([
        {id:'asudfinsafl01',name:'张三',age:18},
        {id:'asudfinsafl02',name:'李四',age:20},
        {id:'asudfinsafl03',name:'王五',age:22},
    ])

</script>

<style>
    /* css样式 */
    .app{
        background-color: #ddd;
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
    }
</style>

子接收

<template>
    <div class="person">
        <ul>
            <li v-for="person in list" :key="person.id">
                <b>{{ person.name }}</b> -- {{ person.age }}
            </li>
        </ul>
    </div>
</template>

<script setup lang="ts">
    import { withDefaults } from 'vue'
    import {type Persons} from '@/types'
    // 接收 a,b
    // defineProps(['a','list'])

    // 接收 a,b,同时将props保存起来
    /* let x = defineProps(['a','b'])
    console.log(x) */

    // 接收 list+限制类型
    // defineProps<{list:Persons}>()

    // 接收 list+限制类型+限制必要性+指定默认值
    let props = withDefaults(defineProps<{list?:Persons}>(),{
        list:()=>[
            {id:'asudfinsafl01',name:'张三',age:18},
            {id:'asudfinsafl02',name:'李四',age:20},
        ]
    })

</script>

<style scoped>
    .person{
        background-color: rgb(112, 96, 221);
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
    }
    button{
        margin: 0 5px;
    }
</style>

 3.13 生命周期

组件的生命周期:创建、挂载、更新、销毁

Vue2的生命周期

Vue 2 提供了以下生命周期钩子函数:

  1. beforeCreate(): 在组件创建之前调用。
  2. created(): 在组件创建后调用。
  3. beforeMount(): 在组件挂载之前调用。
  4. mounted(): 在组件挂载后调用。
  5. beforeUpdate(): 在组件更新之前调用。
  6. updated(): 在组件更新后调用。
  7. beforeDestroy(): 在组件销毁之前调用。
  8. destroyed(): 在组件销毁后调用。

每个钩子函数都有其特定的用途,可以帮助您在组件的生命周期中执行特定的任务。

  • beforeCreate(): 在组件创建之前调用,可以用来设置初始状态或执行必要的设置。
  • created(): 在组件创建后调用,可以用来执行必要的设置或初始化。
  • beforeMount(): 在组件挂载之前调用,可以用来执行必要的设置或初始化。
  • mounted(): 在组件挂载后调用,可以用来执行必要的设置或初始化。
  • beforeUpdate(): 在组件更新之前调用,可以用来执行必要的设置或初始化。
  • updated(): 在组件更新后调用,可以用来执行必要的设置或初始化。
  • beforeDestroy(): 在组件销毁之前调用,可以用来执行必要的清理或销毁。
  • destroyed(): 在组件销毁后调用,可以用来执行必要的清理或销毁。
Vue3的生命周期

Vue 3的Composition API通过setup()函数引入,它没有直接对应Vue 2中的生命周期钩子,但提供了类似功能的组合式API。

创建阶段:setup

挂载阶段:onBeforeMountonMounted

更新阶段:onBeforeUpdateonUpdated

卸载阶段:onBeforeUnmountonUnmounted

  1. setup(): 这是一个新的入口点,替代了Vue 2中的created()beforeCreate(),在这里可以初始化数据、计算属性、侦听器、副作用等。setup()在组件实例被创建之后,模板被渲染之前调用,且不能访问this

  2. onBeforeMount() / onMounted(): 分别对应Vue 2的beforeMountmountedonBeforeMount在挂载前调用,可以用于进行最后的准备工作;onMounted在组件被挂载到DOM后调用,此时可以访问DOM元素。

  3. onBeforeUpdate() / onUpdated(): 类似Vue 2的beforeUpdateupdated,分别在组件数据变化并即将重新渲染前和重新渲染后调用。

  4. onBeforeUnmount() / onUnmounted(): 对应Vue 2的beforeDestroydestroyed,分别在组件卸载前和卸载后调用,用于清理工作,如取消定时器、解绑事件监听器等。

  5. onErrorCaptured(): 类似Vue 2的errorCaptured,用于捕获子组件抛出的错误。

  6. onRenderTracked() / onRenderTriggered(): 这两个是Vue 3新增的,用于调试。onRenderTracked在组件渲染过程中跟踪依赖变化时调用,onRenderTriggered在组件因依赖变化而触发渲染时调用。

常用的钩子:onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)

示例代码

import { ref,onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted } from 'vue'

// 数据
let sum = ref(0)

// 方法
function changeSum(){
    sum.value += 1
    console.log(sum.value);
}
// 创建 
console.log('创建')

// 挂载前
onBeforeMount(()=>{
    console.log('挂载前');
})
// 挂载完毕
onMounted(()=>{
    console.log('挂载完毕');
})
// 更新前
onBeforeUpdate(()=>{
    console.log('更新前');
})
// 更新完毕
onUpdated(()=>{
    console.log('更新完毕');
})
// 卸载前
onBeforeUnmount(()=>{
    console.log('卸载前');
})
// 卸载完毕
onUnmounted(()=>{
    console.log('卸载完毕');
})

 3.14 自定义hook

  • 什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装,类似于vue2.x中的mixin

  • 自定义hook的优势:复用代码, 让setup中的逻辑更清楚易懂。

创建一个和Components同级的hooks文件夹来存放 hook文件

示例代码

  • useSum.ts 
    import {ref,onMounted, computed} from 'vue'
    
    export default function (){
        // 数据
        let sum = ref(0)
        let bigSum = computed(() => {
            return sum.value * 10
        })
        // 方法
        function changeSum(){
            sum.value += 1
        }
        // 钩子
        onMounted(() => {
            changeSum()
        })
        // 返回
        return {sum,changeSum,bigSum}
    }
  •  useDog.ts
    import {reactive, onMounted} from 'vue'
    import axios from 'axios'
    
    export default function (){
        // 数据
        let dogList = reactive([
            'https://images.dog.ceo/breeds/pembroke/n02113023_6826.jpg'
        ])
        // 方法
        /* function getDog(){
            axios.get('https://dog.ceo/api/breed/pembroke/images/random').then(ref => {
                dogList.push(ref.data.message)
            })
        } */
        async function getDog(){
            try{
                let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
                dogList.push(result.data.message)
            }catch(err){
                alert(err)
            }
        }
        // 钩子
        onMounted(() => {
            getDog()  // 挂载后执行
        })
        // 返回
        return {dogList,getDog}
    }
  • 组件中具体使用:
    <template>
        <div class="person">
            <h2>当前求和为:{{ sum }},放大十倍后:{{ bigSum }}</h2>
            <button @click="changeSum">点我sum+1</button>
            <hr>
            <img v-for="(dog,index) in dogList" :src="dog" :key="index"><br>
            <button @click="getDog">再来一只狗</button>
        </div>
    </template>
    
    <script setup lang="ts">
        import useSum from '@/hooks/useSum'
        import useDog from '@/hooks/useDog';
    
        const {sum,changeSum,bigSum} = useSum()
        const {dogList,getDog} = useDog()
    
    </script>
    
    <style scoped>
        .person{
            background-color: rgb(112, 96, 221);
            box-shadow: 0 0 10px;
            border-radius: 10px;
            padding: 20px;
        }
        button{
            margin: 0 5px;
        }
        img {
            height: 200px;
            margin-right: 10px;
        }
    </style>

4. Vue3进阶知识

4.1 路由

Vue 3 的路由系统是基于 Vue Router 4.x 的,以下是 Vue 3 中路由的基本使用方法:

安装 Vue Router

npm install vue-router

4.2 Vue Router

创建路由

首先,你需要定义你的路由。每个路由映射到一个组件。创建一个components同级的router文件夹。例如:


然后创建一个index.ts文件:

// 创建一个路由器,并暴露出去

// 第一步:引入createRouter
import {createRouter, createWebHistory} from 'vue-router'
import Home from '@/components/Home.vue'
import About from '@/components/About.vue'
import News from '@/components/News.vue'


// 第二步:创建路由器
const router = createRouter({
    history: createWebHistory(), // 路由模式
    routes: [       // 路由规则
        {
            path: '/home', //路径
            component: Home
        },
        {
            path: '/about',
            component: About
        },
        {
            path: '/news',
            component: News
        }
    ]
})

// 第三步:导出路由
export default router
集成Vue Router到Vue应用

在你的Vue应用的主要入口文件(通常是main.jsapp.js),你需要将创建的路由器实例注入到Vue应用中:

// 引入createApp用于创建应用
import { createApp } from "vue";
// 引入App根组件
import App from "./App.vue";
// 引入路由
import router from './router'

// 创建应用
const app = createApp(App);
// 使用路由器
app.use(router)
// 挂载整个应用到app容器中
app.mount("#app");

# 另外一种写法

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App).use(router).mount('#app');
使用路由

在组件内部,你可以使用<router-link>进行导航,以及使用<router-view>来显示路由对应的组件:

<template>
    <!-- html -->
     <div class="app">
        <h2 class="title">Vue路由测试</h2>
        <!-- 导航区 -->
         <div class="navigate">
            <RouterLink to="/home" active-class="active">首页</RouterLink>
            <RouterLink to="/news" active-class="active">新闻</RouterLink>
            <RouterLink to="/about" active-class="active">关于</RouterLink>
         </div>
        <!-- 测试区 -->
         <div class="main-content">
            <router-view/>
         </div>
        
     </div>
</template>

<script lang="ts" setup>
import { RouterLink } from 'vue-router';

    
    
</script>

<style>
    /* css样式 */
    .app{
        background-color: #ddd;
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
    }
    .title{
        text-align: center;
        word-spacing: 5px;
        margin: 30px 0;
        height: 70px;
        line-height: 70px;
        background-image: linear-gradient(45deg,gray,white);
        border-radius: 10px;
    }
    .navigate{
        display: flex;
        justify-content: space-around;
        margin: 0 100px;
    }
    .navigate a{
        text-decoration: none;
        color: white;
        font-size: 20px;
        font-weight: bold;
        padding: 10px;
        border-radius: 5px;
        background-color: gray;
    }
    .navigate a.active{
        background-color: yellowgreen;
        color: yellow;
    }
    .main-content{
        margin: 30px 100px;
        border: 1px solid #000;
        border-radius: 5px;
        padding: 20px;
    }
</style>

4.3 两个注意点

1、路由组件通常存放在 pages views 文件夹、一般组件通常存放 components 文件夹。

2、通过点击导航、视觉效果上“消失”了的路由组件,默认是被销毁掉的,需要的时候再去挂载

4.4 to的两种写法

<!-- 第一种:to的字符串写法 -->
<RouterLink to="/home" active-class="active">首页</RouterLink>

<!-- 第二种:to的对象写法 -->
<RouterLink :to="{path:'/home'}" active-class="active">首页</RouterLink>

4.5 路由器的工作模式

1、history 模式

优点:URL更加美观,不带有#,更接近传统的网站URL

缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有404错误。

const router = createRouter({
    history:createWebHistory(), // history模式
    /****/
})

2、hash 模式

优点:兼容性更好,因为不需要服务器端处理路径。

缺点:URL带有#不太美观,且在SEO优化方面相对较差.

4.6 命名路由

 作用:可以简化路由跳转及传参

给路由规则命名:

const router = createRouter({
    history: createWebHistory(), // 路由模式
    routes: [       // 路由规则
        {
            name: 'home',
            path: '/home', //路径
            component: Home
        },
        {
            name: 'about',
            path: '/about',
            component: About
        },
        {
            name: 'news',
            path: '/news',
            component: News,
            children: [
                {
                    name: 'detail',
                    path: 'detail',
                    component: Detail
                }
            ]
        }
    ]
})

跳转路由:

<!-- 第一种写法 -->
<!--简化前:需要写完整的路径(to的字符串写法) -->
<!-- <RouterLink :to="`/news/detail?id=${news.id}&title=${news.title}&content=${news.content}`">{{ news.title }}</RouterLink> -->

<!-- 第二种写法 -->
<!--简化后:直接通过名字跳转(to的对象写法配合name属性) -->
<router-link :to="{name:'guanyu'}">跳转</router-link>

4.7 嵌套路由

设置路由

接下来,定义你的路由。嵌套路由通常涉及定义一个父路由,该路由可以包含一个或多个子路由。下面是一个简单的示例,定义了一个news组件作为父路由,它有一个子路由:detail

// 创建一个路由器,并暴露出去

// 第一步:引入createRouter
import {createRouter, createWebHistory} from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
import News from '@/views/News.vue'
import Detail from '@/views/Detail.vue'

// 第二步:创建路由器
const router = createRouter({
    history: createWebHistory(), // 路由模式
    routes: [       // 路由规则
        {
            name: 'home',
            path: '/home', //路径
            component: Home
        },
        {
            name: 'about',
            path: '/about',
            component: About
        },
        {
            name: 'news',
            path: '/news',
            component: News,
            children: [
                {
                    name: 'detail',
                    path: 'detail',
                    component: Detail
                }
            ]
        }
    ]
})

// 第三步:导出路由
export default router
使用嵌套路由 

注意

记得要加完整路径

在你的主组件中,使用<router-view>来渲染父路由的组件,同时在父组件内部使用另一个<router-view>来渲染子路由的组件。

<router-link to="/news/detail">xxxx</router-link>
<!-- 或 -->
<router-link :to="{path:'/news/detail'}">xxxx</router-link>

 4.8 路由传参

query参数
  1. 传递参数
    <!-- 跳转并携带query参数(to的字符串写法)(我个人不喜欢这种写法) -->
    <router-link to="/news/detail?a=1&b=2&content=欢迎你">
    	跳转
    </router-link>
    				
    <!-- 跳转并携带query参数(to的对象写法) (我更喜欢这样的写法)-->
    <RouterLink 
      :to="{
        //name:'xiang', //用name也可以跳转
        path:'/news/detail',
        query:{
          id:news.id,
          title:news.title,
          content:news.content
        }
      }"
    >
      {{news.title}}
    </RouterLink>
  2. 接收参数
    <template>
        <div class="news-list">
            <li>编号:{{ query.id }}</li>
            <li>标题:{{ query.title }}</li>
            <li>内容:{{ query.content }}</li>
        </div>
    </template>
    
    <script setup lang="ts">
        import { toRefs } from 'vue';
        import { useRoute } from 'vue-router'
    
        let route = useRoute()
    
        let {query} = toRefs(route)
        
    
    </script>
params参数
  1. 传递参数
    <!-- 跳转并携带params参数(to的字符串写法) -->
    <RouterLink :to="`/news/detail/001/新闻001/内容001`">{{news.title}}</RouterLink>
    				
    <!-- 跳转并携带params参数(to的对象写法) -->
    <RouterLink 
      :to="{
        name:'xiang', //用name跳转
        params:{
          id:news.id,
          title:news.title,
          content:news.title
        }
      }"
    >
      {{news.title}}
    </RouterLink>
  2. 接收参数
    import {useRoute} from 'vue-router'
    const route = useRoute()
    // 打印params参数
    console.log(route.params)

 温馨提示

备注1:传递params参数时,若使用to的对象写法,必须使用name配置项,不能用path

备注2:传递params参数时,需要提前在规则中占位

4.9 路由的props配置

作用:让路由组件更方便的收到参数(可以将路由参数作为props传给组件)

props忘记的可以回头翻看(3.12)

children: [
    {
        name: 'detail',
        path: 'detail',
        component: Detail,

        // 第一种写法:将路由收到的所有params参数作为props传给路由组件
        // props: true

        // 第二种写法:函数写法,可以自己决定将什么作为props给路由组件
        props: (route)=>{
            return route.query
        }

        // 第三种写法:对象写法,可以自己决定将什么作为props给路由组件
        /* props:{
            a: '我是新闻列表的摘要',
            b: '我是新闻列表的标题',
            c: '我是新闻列表的作者'
        } */
    }
]

4.10 replace属性

  1. 作用:控制路由跳转时操作浏览器历史记录的模式。

  2. 浏览器的历史记录有两种写入方式:分别为pushreplace

    • push是追加历史记录(默认值)。

    • replace是替换当前记录。

  3. 开启replace模式:

    <RouterLink replace .......>News</RouterLink>

4.11 编程式导航

实际上这种写法用的更多一些

编程式路由导航是指在Vue应用中通过JavaScript代码来控制路由的导航,而不是通过HTML中的<router-link>标签。这种方式提供了更多的灵活性和控制能力,特别是在需要根据条件或逻辑来决定导航路径的情况下非常有用。

在Vue 3中,编程式路由导航可以通过Vue Router提供的方法来实现。下面是一些常用的方法:

1、使用router.push()

router.push()方法是最常用的编程式导航方法之一,用于导航到一个新的位置。它可以接受一个路由对象或一个相对路径作为参数。

实例

import { useRouter } from 'vue-router';

let router = useRouter()

router.push({
    name: 'detail',
    query:{
        id:news.id,
        title:news.title,
        content:news.content
    }
})

2、使用router.replace() 

router.replace()方法与router.push()类似,但不同之处在于它不会在浏览器的历史堆栈中添加新的记录,而是替换当前的历史记录。

实例

import { useRouter } from 'vue-router';

let router = useRouter()

router.push({
    name: 'detail',
    replace:{
        id:news.id,
        title:news.title,
        content:news.content
    }
})

 4.12 重定向

作用:将特定的路径,重新定向到已有路由。

实例

{
    path:'/',
    redirect:'/about'
}

5. Pinia

Pinia 是一个 Vue 专用的状态管理库,它提供了一种简洁、易于理解和使用的状态管理模式。Pinia 的设计目标是尽可能地保持简单,同时又足够强大以满足大多数状态管理的需求。在 Vue 3 中,Pinia 是一个非常受欢迎的选择,尤其是在那些需要状态管理但又不想引入像 Vuex 这样较重的解决方案的项目中。

Pinia 的特点

  1. 简单易用:Pinia 的 API 设计非常直观,学习曲线平缓。
  2. 类型安全:Pinia 支持 TypeScript,并且在开发过程中提供强大的类型推断。
  3. 可扩展性:Pinia 允许你轻松地扩展功能,如中间件、插件等。
  4. 模块化:Pinia 支持按模块划分状态,便于管理和维护。

5.1、安装 Pinia

首先,你需要安装 Pinia。可以通过 npm 进行安装:

npm install pinia

操作src/main.ts

// 引入createApp用于创建应用
import { createApp } from "vue";
// 引入App根组件
import App from "./App.vue";
// 引入路由
import router from './router'
// 引入pinia
import { createPinia } from 'pinia'

// 创建应用
const app = createApp(App);
// 创建pinia
const pinia = createPinia()
// 使用pinia
app.use(pinia)
// 使用路由器
app.use(router)
// 挂载整个应用到app容器中
app.mount("#app");

5.2、创建 Store

Pinia 中的 store 是通过定义一个函数并使用 defineStore 函数来创建的。下面是一个简单的示例:

创建一个 store 文件 counter.ts
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2
  }
});

在这个示例中,我们定义了一个名为 counter 的 store,它有一个状态 count,一个 action increment 用于增加 count 的值,以及一个 getter doubleCount 用于返回 count 的两倍。 

5.3、使用 Store

一旦你创建了 store,就可以在组件中使用它。Pinia 提供了 useStore 函数来访问 store。

在组件中使用 store
// Counter.vue

<template>
    <div class="text">
        <h2>数据计算</h2>
        <h3>当前数值:{{ counterStore.count }}</h3>
        <button @click="counterStore.increment">加</button>
    </div>
</template>

<script setup lang="ts">
    import {useCounterStore} from '@/store/counter'

    const counterStore = useCounterStore()

</script>

5.4、修改pinia数据

1、第一种修改方式,直接修改
countStore.sum = 666
2、第二种修改方式:批量修改
countStore.$patch({
  sum:999,
  school:'atguigu'
})
3、第三种修改方式:借助action修改(action中可以编写一些业务逻辑)
import {defineStore} from 'pinia'

export const useCountStore = defineStore('count',{
    // 真正存储数据的地方
    state:()=>{
        return {
            sum:6,
            school: 'sqm',
            address: '库库林'
        }
    },
    // actions里面放置的是一个一个的动作方法,用于响应组件的“动作”
    actions:{
        increment(value){
            this.sum += value
        }
    }
})

 组件中调用action即可

// 使用countStore
const countStore = useCountStore()

// 调用对应action
countStore.incrementOdd(n.value)

5.5 storeToRefs

  • 借助storeToRefsstore中的数据转为ref对象,方便在模板中使用。

  • 注意:pinia提供的storeToRefs只会将数据做转换,而VuetoRefs会转换store中数据。

import { ref,toRefs } from 'vue'
import {useCountStore} from "@/store/count"
import { storeToRefs } from 'pinia';

let countStore = useCountStore()
// storeToRefs只会关注store中定义的响应式数据,不会对方法进行ref包裹
const {sum,school,address} = storeToRefs(countStore)

5.6 getters

在 Vue 3 中使用 Pinia 定义 getters 是一个很好的方式来处理和计算状态数据。

  1. 概念:当state中的数据,需要经过处理后再使用时,可以使用getters配置。

  2. 追加getters配置。

// 引入defineStore用于创建store
import {defineStore} from 'pinia'

// 定义并暴露一个store
export const useCountStore = defineStore('count',{
  // 动作
  actions:{
    /************/
  },
  // 状态
  state(){
    return {
      sum:1,
      school:'atguigu'
    }
  },
  // 计算
  getters:{
    // 两种实现方式:个人比较喜欢第一种
    bigSum:(state):number => state.sum *10,
    upperSchool():string{
      return this. school.toUpperCase()
    }
  }
})

5.7 $subscribe 订阅

Pinia 提供了一个 $subscribe 方法,用于订阅 Store 的变化。当 Store 的状态发生变化时,你可以通过 $subscribe 来执行某些操作,比如记录状态的变化或者触发某些副作用。

<script setup lang="ts">
    import {useTalkStore} from '@/store/talk'
    import { storeToRefs } from 'pinia';

    const talkStore = useTalkStore()
    const {talkList} = storeToRefs(talkStore)
    talkStore.$subscribe((mutate, state) => {
        console.log('talkStore数据发生了变化', mutate,state);
        localStorage.setItem('talkList', JSON.stringify(state.talkList))
    })
    // 方法
    function getLoveTalk(){
        talkStore.getATalk()
    }
</script>

talkStore.$subscribe((mutate, state) => {...}): 订阅 Store 的变化。当 talkList 发生变化时,回调函数会被调用。

  • mutate 参数包含了变化的具体信息。
  • state 参数包含了 Store 的当前状态。
  • 在回调函数中,使用 localStorage.setItem 存储最新的 talkList 到本地存储。

 5.8 store组合式写法

import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'
import {reactive} from 'vue'

export const useTalkStore = defineStore('talk',()=>{
  // talkList就是state
  const talkList = reactive(
    JSON.parse(localStorage.getItem('talkList') as string) || []
  )

  // getATalk函数相当于action
  async function getATalk(){
    // 发请求,下面这行的写法是:连续解构赋值+重命名
    let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
    // 把请求回来的字符串,包装成一个对象
    let obj = {id:nanoid(),title}
    // 放到数组中
    talkList.unshift(obj)
  }
  return {talkList,getATalk}
})

6. 组件通信

Vue3组件通信和Vue2的区别:

  • 移出事件总线,使用mitt代替。

  • vuex换成了pinia

  • .sync优化到了v-model里面了。

  • $listeners所有的东西,合并到$attrs中了。

  • $children被砍掉了。

常见搭配形式:

6.1. 【props】

概述:props是使用频率最高的一种通信方式,常用与 :父 ↔ 子

  • 父传子:属性值是非函数

  • 子传父:属性值是函数

父组件

<template>
  <div class="father">
    <h3>父组件,</h3>
		<h4>我的车:{{ car }}</h4>
		<h4>儿子给的玩具:{{ toy }}</h4>
		<Child :car="car" :getToy="getToy"/>
  </div>
</template>

<script setup lang="ts" name="Father">
	import Child from './Child.vue'
	import { ref } from "vue";
	// 数据
	const car = ref('奔驰')
	const toy = ref()
	// 方法
	function getToy(value:string){
		toy.value = value
	}
</script>

子组件

<template>
  <div class="child">
    <h3>子组件</h3>
		<h4>我的玩具:{{ toy }}</h4>
		<h4>父给我的车:{{ car }}</h4>
		<button @click="getToy(toy)">玩具给父亲</button>
  </div>
</template>

<script setup lang="ts" name="Child">
	import { ref } from "vue";
	const toy = ref('奥特曼')
	
	defineProps(['car','getToy'])
</script>

6.2. 【自定义事件】

  1. 概述:自定义事件常用于:子 => 父。

  2. 注意区分好:原生事件、自定义事件。

  • 原生事件:

    • 事件名是特定的(clickmosueenter等等)

    • 事件对象$event: 是包含事件相关信息的对象(pageXpageYtargetkeyCode

  • 自定义事件:

    • 事件名是任意名称

    • 事件对象$event: 是调用emit时所提供的数据,可以是任意类型!!!

实例

<!--在父组件中,给子组件绑定自定义事件:-->
<Child @send-toy="toy = $event"/>
//子组件中,触发事件:

<button @click="emit('send-toy',atm)">把玩具交给父亲</button>
// 声明事件
const emit = defineEmits(['send-toy'])

6.3. 【mitt】

概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。

安装mitt

npm i mitt

 新建文件:src\utils\emitter.ts

// 引入mitt 
import mitt from "mitt";

// 创建emitter
const emitter = mitt()

/*
  // 绑定事件
  emitter.on('abc',(value)=>{
    console.log('abc事件被触发',value)
  })
  emitter.on('xyz',(value)=>{
    console.log('xyz事件被触发',value)
  })

  setInterval(() => {
    // 触发事件
    emitter.emit('abc',666)
    emitter.emit('xyz',777)
  }, 1000);

  setTimeout(() => {
    // 清理事件
    emitter.all.clear()
  }, 3000); 
*/

// 创建并暴露mitt
export default emitter

接收数据的组件中:绑定事件、同时在销毁前解绑事件:

import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

// 绑定事件
emitter.on('send-toy',(value)=>{
  console.log('send-toy事件被触发',value)
})

onUnmounted(()=>{
  // 解绑事件
  emitter.off('send-toy')
})

【第三步】:提供数据的组件,在合适的时候触发事件

import emitter from "@/utils/emitter";

function sendToy(){
  // 触发事件
  emitter.emit('send-toy',toy.value)
}

注意这个重要的内置关系,总线依赖着这个内置关系

6.4.【v-model】

  1. 概述:实现 父↔子 之间相互通信。

  2. 前序知识 —— v-model的本质

    <!-- 使用v-model指令 -->
    <input type="text" v-model="userName">
    
    <!-- v-model的本质是下面这行代码 -->
    <input 
      type="text" 
      :value="userName" 
      @input="userName =(<HTMLInputElement>$event.target).value"
    >
  3. 组件标签上的v-model的本质::moldeValueupdate:modelValue事件。
    <!-- 组件标签上使用v-model指令 -->
    <AtguiguInput v-model="userName"/>
    
    <!-- 组件标签上v-model的本质 -->
    <AtguiguInput :modelValue="userName" @update:model-value="userName = $event"/>
     AtguiguInput组件中:
    <template>
      <div class="box">
        <!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
    		<!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件-->
        <input 
           type="text" 
           :value="modelValue" 
           @input="emit('update:model-value',$event.target.value)"
        >
      </div>
    </template>
    
    <script setup lang="ts" name="AtguiguInput">
      // 接收props
      defineProps(['modelValue'])
      // 声明事件
      const emit = defineEmits(['update:model-value'])
    </script>
  4. 也可以更换value,例如改成abc
    <!-- 也可以更换value,例如改成abc-->
    <AtguiguInput v-model:abc="userName"/>
    
    <!-- 上面代码的本质如下 -->
    <AtguiguInput :abc="userName" @update:abc="userName = $event"/>

    AtguiguInput组件中:

    <template>
      <div class="box">
        <input 
           type="text" 
           :value="abc" 
           @input="emit('update:abc',$event.target.value)"
        >
      </div>
    </template>
    
    <script setup lang="ts" name="AtguiguInput">
      // 接收props
      defineProps(['abc'])
      // 声明事件
      const emit = defineEmits(['update:abc'])
    </script>
  5. 如果value可以更换,那么就可以在组件标签上多次使用v-model
    <AtguiguInput v-model:abc="userName" v-model:xyz="password"/>

6.5.【$attrs 】

  1. 概述:$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。

  2. 具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。

    注意:$attrs会自动排除props中声明的属性(可以认为声明过的 props 被子组件自己“消费”了)

父组件:

<template>
  <div class="father">
    <h3>父组件</h3>
		<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
  </div>
</template>

<script setup lang="ts" name="Father">
	import Child from './Child.vue'
	import { ref } from "vue";
	let a = ref(1)
	let b = ref(2)
	let c = ref(3)
	let d = ref(4)

	function updateA(value){
		a.value = value
	}
</script>

子组件:

<template>
	<div class="child">
		<h3>子组件</h3>
		<GrandChild v-bind="$attrs"/>
	</div>
</template>

<script setup lang="ts" name="Child">
	import GrandChild from './GrandChild.vue'
</script>

孙组件:

<template>
	<div class="grand-child">
		<h3>孙组件</h3>
		<h4>a:{{ a }}</h4>
		<h4>b:{{ b }}</h4>
		<h4>c:{{ c }}</h4>
		<h4>d:{{ d }}</h4>
		<h4>x:{{ x }}</h4>
		<h4>y:{{ y }}</h4>
		<button @click="updateA(666)">点我更新A</button>
	</div>
</template>

<script setup lang="ts" name="GrandChild">
	defineProps(['a','b','c','d','x','y','updateA'])
</script>

6.6. 【$refs、$parent】

  1. 概述:

    • $refs用于 :父→子。

    • $parent用于:子→父。

  2. 原理如下:

    属性 说明
    $refs 值为对象,包含所有被ref属性标识的DOM元素或组件实例。
    $parent 值为对象,当前组件的父组件实例对象。

案例

父组件

<template>
    <!-- html -->
     <div class="app">
        <h2>父组件:</h2>
        <h4>房产:{{ house }}</h4>
        <button @click="changeToy">修改Text的玩具</button>
        <button @click="changeComputer">修改Child2的电脑</button>
        <button @click="getAllChild($refs)">给孩子增加快乐</button>
        <Text ref="c1"/>
        <Text2 ref="c2"/>
     </div>
</template>

<script lang="ts" setup>
    import Text from './components/Text.vue';
    import Text2 from './components/Text2.vue';
    import { ref } from 'vue';
    let c1 = ref()
    let c2 = ref()
    // 数据
    let house = ref(4)
    // 方法
    function changeToy(){
        c1.value.toy = '小羊驼'
    }
    function changeComputer(){
        c2.value.computer = '华为'
    }
    function getAllChild(refs:any){
        for(let key in refs){
            refs[key].book += 3
        }
    }

    // 把数据交给外部
    defineExpose({house})
</script>

 子组件1

<template>
    <div class="text">
        <h2>子组件1</h2>
        <h4>玩具:{{ toy }}</h4>
        <h4>书籍:{{ book }}本</h4>
        <button @click="minusHouse($parent)">获得父亲一套房产</button>
    </div>
</template>

<script setup lang="ts">
    import {ref} from 'vue'
    // 数据
    let toy = ref('凹凸曼')
    let book = ref(3)
    // 方法
    function minusHouse(parent:any){
        parent.house--
    }
    // 把数据交给外部
    defineExpose({toy,book})
</script>

子组件2

<template>
    <div class="text2">
        <h2>子组件</h2>
        <h4>电脑:{{ computer }}</h4>
        <h4>书籍:{{ book }}本</h4>
    </div>
</template>

<script setup lang="ts">
    import {ref} from 'vue'
    // 数据
    let computer = ref('联想')
    let book = ref(6)
    // 接收
    
    // 把数据交给外部
    defineExpose({computer,book})
</script>

6.7. 【provide、inject】

  1. 概述:实现祖孙组件直接通信

  2. 具体使用:

    • 在祖先组件中通过provide配置向后代组件提供数据

    • 在后代组件中通过inject配置来声明接收数据

  3. 具体编码:

    【第一步】父组件中,使用provide提供数据

    <template>
      <div class="father">
        <h3>父组件</h3>
        <h4>资产:{{ money }}</h4>
        <h4>汽车:{{ car }}</h4>
        <button @click="money += 1">资产+1</button>
        <button @click="car.price += 1">汽车价格+1</button>
        <Child/>
      </div>
    </template>
    
    <script setup lang="ts" name="Father">
      import Child from './Child.vue'
      import { ref,reactive,provide } from "vue";
      // 数据
      let money = ref(100)
      let car = reactive({
        brand:'奔驰',
        price:100
      })
      // 用于更新money的方法
      function updateMoney(value:number){
        money.value += value
      }
      // 提供数据
      provide('moneyContext',{money,updateMoney})
      provide('car',car)
    </script>

    注意:子组件中不用编写任何东西,是不受到任何打扰的

  4. 【第二步】孙组件中使用inject配置项接受数据。
    <template>
      <div class="grand-child">
        <h3>我是孙组件</h3>
        <h4>资产:{{ money }}</h4>
        <h4>汽车:{{ car }}</h4>
        <button @click="updateMoney(6)">点我</button>
      </div>
    </template>
    
    <script setup lang="ts" name="GrandChild">
      import { inject } from 'vue';
      // 注入数据
     let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})
      let car = inject('car')
    </script>

6.8. 【pinia】

 参考之前pinia部分 5.Pinia

6.9. 【slot】

1. 默认插槽

父组件:

<template>
    <!-- html -->
     <div class="app">
        <h2>父组件</h2>
        <div class="content">
            <Text title="热门游戏列表">
                <ul>
                    <li v-for="item in games" :key="item.id">{{item.name}}</li>
                </ul>
            </Text>
            <Text title="随机男神头像">
                <img :src="imgUrl" alt="随机图片">
            </Text>
            <Text title="今日影视推荐">
                <video :src="videoUrl" controls></video>
            </Text>
        </div>
     </div>
</template>

<script lang="ts" setup>
    import { reactive, ref,onMounted } from 'vue';
    import Text from './components/Text.vue';
    import axios from 'axios';
    // 数据
    let games = reactive([
        {id:'asdffgadasf01',name:'英雄联盟'},
        {id:'asdffgadasf02',name:'王者荣耀'},
        {id:'asdffgadasf03',name:'CSGO2'},
        {id:'asdffgadasf04',name:'绝地求生'},
    ])
    let imgUrl = ref('')

    let videoUrl = 'https://img-baofun.zhhainiao.com/pcwallpaper_ugc/preview/d85a1490619cd94925ce25308834f044_preview.mp4'
    // 方法
    async function getImgUrl(){
        let {data:{imgurl}} = await axios.get('https://api.uomg.com/api/rand.avatar?sort=男&format=json')
        imgUrl.value = imgurl
    }
    // 钩子
    onMounted(()=>{
        getImgUrl()
    })
</script>

<style>
    /* css样式 */
    .app{
        background-color: #ddd;
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
    }
    .content{
        display: flex;
        justify-content: space-evenly;
    }

    img, video{
        width: 100%;
    }
</style>

 子组件:

<template>
    <div class="text">
        <h2>{{ title }}</h2>
        <!-- 默认插槽 -->
        <slot></slot>
    </div>
</template>

<script setup lang="ts">
    import {ref} from 'vue'
   
    // 数据

    // 接收数据
    defineProps(['title'])
</script>

<style scoped>
    .text {
        background-color: yellowgreen;
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
        width: 200px;
        height: 300px;
    }
    h2{
        background-color: orange;
        text-align: center;
        font-size: 20px;
        font-weight: 800;
    }
</style>

2. 具名插槽

如何使用具名插槽:

在子组件中定义具名插槽

  • 在子组件模板中使用 <template v-slot:name> 定义具名插槽。
  • name 是您为插槽指定的名字。

在父组件中使用具名插槽

  • 在父组件中使用 <slot name="name">...</slot>,其中 name 与子组件中定义的插槽名字相匹配。

实例

子组件

<template>
  <div class="child-component">
    <slot name="header"></slot>
    <p>Content goes here.</p>
    <slot name="footer"></slot>
  </div>
</template>

父组件

<template>
  <div class="parent-component">
    <ChildComponent>
      <template v-slot:header>
        <h1>Header Content</h1>
      </template>
      <template v-slot:footer>
        <footer>Footer Content</footer>
      </template>
    </ChildComponent>
  </div>
</template>

在这个例子中,ChildComponent 定义了两个具名插槽:headerfooter。在 ParentComponent 中,我们使用 <template v-slot:name> 来填充这些插槽。

更简洁的语法:

Vue 也支持更简洁的语法来使用具名插槽:

子组件

<template>
  <div class="child-component">
    <slot name="header"></slot>
    <p>Content goes here.</p>
    <slot name="footer"></slot>
  </div>
</template>

父组件

<template>
  <div class="parent-component">
    <ChildComponent>
      <template #header>
        <h1>Header Content</h1>
      </template>
      <template #footer>
        <footer>Footer Content</footer>
      </template>
    </ChildComponent>
  </div>
</template>

这里,#header #footer 是具名插槽的简写形式。

3. 作用域插槽

作用域插槽(Scoped Slots)是 Vue 中一种高级特性,它允许父组件访问子组件的数据,并在父组件中使用这些数据来渲染内容。这种特性使得组件之间的数据传递更加灵活,尤其是在需要根据子组件提供的数据动态生成内容的情况下非常有用。

如何使用作用域插槽:

  1. 在子组件中定义作用域插槽

    • 在子组件中使用 <slot> 标签,并为其添加 v-bind 属性来绑定子组件的数据。
    • 这样父组件就可以访问这些数据并在插槽中使用它们。
  2. 在父组件中使用作用域插槽

    • 在父组件中使用 <template v-slot:default="slotProps">...</template> 来定义作用域插槽。
    • slotProps 是一个对象,它包含了从子组件传递过来的数据。
    • 在模板中,您可以使用这些数据来渲染内容。

示例:

子组件

<template>
  <div class="child-component">
    <slot :item="item"></slot>
  </div>
</template>

父组件

<template>
  <div class="parent-component">
    <ChildComponent>
      <template #default="slotProps">
        <h1>{{ slotProps.item.title }}</h1>
        <p>{{ slotProps.item.description }}</p>
      </template>
    </ChildComponent>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

</script>

7. 其它 API

7.1.【shallowRef 与 shallowReactive 】

Vue 3 中,为了更好地管理响应式状态,引入了几个新的 API,包括 shallowRefshallowReactive。这两个函数都是用于创建响应式的引用类型,但它们之间有一些关键的区别。

shallowRef

shallowRef 创建一个响应式的引用对象,该对象只跟踪其 .value 属性的变化。当 .value 变化时,依赖于它的任何组件将会重新渲染。但是,如果 .value 的值本身是一个对象或数组,并且该对象或数组内部发生变化,则不会触发重新渲染。

使用示例:
import { shallowRef } from 'vue';

const user = shallowRef({ name: 'Alice', age: 30 });

// 当 user.value 改变时,依赖它的组件会更新
user.value = { name: 'Bob', age: 25 };

// 但是,如果 user.value 内部属性改变,则不会触发更新
user.value.name = 'Charlie'; // 不会触发更新

shallowReactive

shallowReactive 创建一个对象的响应式代理,但它只会使该对象的第一层属性成为响应式的。这意味着如果你修改对象的第一层属性,那么变化会被追踪。然而,如果对象包含其他嵌套的对象或数组,这些内部结构的变化不会被追踪。

使用示例:
import { shallowReactive } from 'vue';

const state = shallowReactive({
  user: { name: 'Alice', age: 30 },
  friends: [{ name: 'Bob' }]
});

// 修改第一层属性时,会触发更新
state.user = { name: 'Charlie', age: 25 };

// 但是,如果内部属性改变,则不会触发更新
state.user.name = 'David'; // 不会触发更新
state.friends[0].name = 'Eve'; // 也不会触发更新

总结

  • shallowRef 用于创建一个响应式的引用,只跟踪 .value 的变化。
  • shallowReactive 创建一个响应式代理,只使对象的第一层属性成为响应式的。

选择哪个?

  • 如果你需要一个简单的响应式引用,可以选择 shallowRef
  • 如果你需要一个响应式对象,但不关心对象内部深层次的响应性,可以使用 shallowReactive

这两种方法都比完全响应式的 refreactive 要节省性能开销,因为它们减少了需要追踪的依赖数量。这对于大型应用程序或者有复杂数据结构的应用程序来说是非常有用的。

7.2.【readonly 与 shallowReadonly】

在 Vue 3 中,为了更好地管理状态并提高性能,引入了 readonlyshallowReadonly 这两个实用函数。它们可以用来创建不可变的响应式对象,这对于防止意外修改状态以及优化性能非常有用。

readonly

readonly 创建一个只读的响应式代理。这意味着你不能直接修改通过 readonly 创建的对象的属性。如果尝试修改这些属性,将会抛出一个错误,并且不会发生实际的修改。但是,如果对象包含其他嵌套的对象或数组,你可以修改这些内部对象或数组的属性。

使用示例:
import { reactive, readonly } from 'vue';

const state = reactive({ user: { name: 'Alice', age: 30 }, friends: [{ name: 'Bob' }] });
const readOnlyState = readonly(state);

// 不能直接修改 readOnlyState 的属性
readOnlyState.user = { name: 'Charlie', age: 25 }; // 抛出错误

// 但是,可以修改内部对象的属性
readOnlyState.user.name = 'David'; // 不会抛出错误
readOnlyState.friends[0].name = 'Eve'; // 也不会抛出错误

shallowReadonly

shallowReadonly 类似于 readonly,但它只创建一个只读的响应式代理,且只对对象的第一层属性应用只读。这意味着如果对象包含其他嵌套的对象或数组,这些内部结构仍然是可写的。

使用示例:
import { reactive, shallowReadonly } from 'vue';

const state = reactive({ user: { name: 'Alice', age: 30 }, friends: [{ name: 'Bob' }] });
const shallowReadOnlyState = shallowReadonly(state);

// 不能直接修改 shallowReadOnlyState 的属性
shallowReadOnlyState.user = { name: 'Charlie', age: 25 }; // 抛出错误

// 但是,可以修改内部对象的属性
shallowReadOnlyState.user.name = 'David'; // 不会抛出错误
shallowReadOnlyState.friends[0].name = 'Eve'; // 也不会抛出错误

总结

  • readonly 创建一个只读的响应式代理,阻止直接修改对象及其所有嵌套对象的属性。
  • shallowReadonly 创建一个只读的响应式代理,只阻止直接修改对象的第一层属性,内部对象或数组仍然可写。

选择哪个?

  • 如果你需要一个完全不可变的响应式对象,可以选择 readonly
  • 如果你需要一个只读的响应式对象,但不关心内部对象的响应性和可写性,可以使用 shallowReadonly

这两种方法都可以帮助你在开发过程中避免意外的状态修改,从而提高代码的健壮性和可维护性。此外,它们还可以带来性能上的好处,因为只读对象不需要追踪内部属性的变化,这可以减少不必要的渲染和计算。

7.3.【toRaw 与 markRaw】

在 Vue 3 中,为了更好地管理和操作响应式数据,引入了 toRawmarkRaw 这两个实用函数。它们可以帮助您处理和优化与响应式数据相关的操作。

toRaw

toRaw 函数用于获取一个响应式对象的原始版本。这意味着它会返回对象的一个非响应式版本,这在某些场景下很有用,比如当你需要将响应式对象作为参数传递给不支持响应式数据的函数或库时。

使用示例:
import { reactive, toRaw } from 'vue';

const state = reactive({ name: 'Alice', age: 30 });

// 获取 state 的原始版本
const rawState = toRaw(state);

// 修改原始版本的属性不会触发响应
rawState.name = 'Bob'; // 修改 rawState.name 不会影响 state

// 但是,state 仍然是响应式的
state.name = 'Charlie'; // 修改 state.name 会导致依赖 state 的组件重新渲染

markRaw

markRaw 函数用于标记一个对象,使其永远都不会被转换成响应式的。这对于那些大型的对象或者外部库返回的对象非常有用,因为这些对象可能包含复杂的内部结构,转换为响应式可能会消耗大量的性能资源。

使用示例:
import { reactive, markRaw } from 'vue';

const largeObject = {
  // ...一个非常大的对象
};

// 标记 largeObject 为非响应式的
const markedLargeObject = markRaw(largeObject);

const state = reactive({ 
  largeData: markedLargeObject 
});

// 即使 state.largeData 是响应式的对象的一部分,它本身也不会变为响应式的
state.largeData.someProperty = 'new value'; // 不会触发任何响应式更新

总结

  • toRaw 用于获取响应式对象的原始版本,即一个非响应式的副本。
  • markRaw 用于标记一个对象,使其永远不会被转换成响应式的。

选择哪个?

  • 如果你需要暂时性地禁用一个响应式对象的响应性,可以选择 toRaw
  • 如果你需要永久性地禁用一个对象的响应性,可以使用 markRaw

这两种方法都可以帮助您更好地控制和优化应用程序中的响应式数据管理。使用 toRaw 可以在需要时临时禁用响应性,而使用 markRaw 可以避免不必要的性能开销。

7.4.【customRef】

customRef 是 Vue 3 中的一个实用工具,它允许您创建自定义的响应式引用类型。通过 customRef,您可以实现更复杂的逻辑,比如自定义值的读取和写入行为、延迟更新、缓存值等。

使用 customRef 的基本步骤:

  1. 定义一个类:继承自 VueReactivity/refImpl/RefBase 并实现 track 和 trigger 方法。
  2. 实现 track 方法:当值被读取时调用,用于追踪依赖。
  3. 实现 trigger 方法:当值被修改时调用,用于触发依赖的更新。
  4. 定义 get 和 set 方法:实现自定义的读取和写入逻辑。

作用:创建一个自定义的ref,并对其依赖项跟踪和更新触发进行逻辑控制。

实现防抖效果(useSumRef.ts):

import {customRef } from "vue";

export default function(initValue:string,delay:number){
  let msg = customRef((track,trigger)=>{
    let timer:number
    return {
      get(){
        track() // 告诉Vue数据msg很重要,要对msg持续关注,一旦变化就更新
        return initValue
      },
      set(value){
        clearTimeout(timer)
        timer = setTimeout(() => {
          initValue = value
          trigger() //通知Vue数据msg变化了
        }, delay);
      }
    }
  }) 
  return {msg}
}

总结

  • customRef 允许您创建自定义的响应式引用类型,实现更复杂的逻辑。
  • 定义类:继承自 RefBase 并实现 track 和 trigger 方法。
  • 实现逻辑:通过 get 和 set 方法自定义读取和写入行为。
  • 使用 customRef:通过 customRef 函数创建自定义引用。

通过 customRef,您可以实现许多有趣的功能,比如缓存值、防抖更新、懒加载等。这对于构建高性能和复杂的应用程序非常有用。

8. Vue3新组件

8.1. 【Teleport】

Teleport 是 Vue 3 中引入的一个非常有用的全局 API,它允许您将组件渲染到 DOM 中的任意位置,而不受组件树的限制。这在实现模态框、弹出菜单、工具提示等 UI 元素时非常有用,因为这些元素通常需要放置在页面的特定位置,而不仅仅是当前组件的子节点。

Teleport 的基本用法

HTML 结构

首先,在 HTML 中定义一个接收 Teleport 渲染内容的容器,通常位于 body 或者 html 标签内:

<div id="teleport-target"></div>
Vue 组件中使用 Teleport

在 Vue 组件中使用 Teleport 非常简单,只需要在模板中使用 <Teleport> 标签,并为其指定 to 属性,指向之前定义的接收容器。

<template>
  <button @click="showModal = true">Open Modal</button>

  <Teleport to="body">
    <Transition name="fade">
      <div v-if="showModal" class="modal">
        <h1>Modal Title</h1>
        <p>This is some content in the modal.</p>
        <button @click="showModal = false">Close Modal</button>
      </div>
    </Transition>
  </Teleport>
</template>

<script>
import { ref } from 'vue';

export default {
  setup() {
    const showModal = ref(false);

    return {
      showModal
    };
  }
};
</script>

<style scoped>
.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: white;
  padding: 20px;
  border: 1px solid #ccc;
  z-index: 1000;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

8.2. 【Suspense】

  • 等待异步组件时渲染一些额外内容,让应用有更好的用户体验

  • 使用步骤:

    • 异步引入组件

    • 使用Suspense包裹组件,并配置好defaultfallback

Suspense 的基本用法

异步组件

首先,你需要创建一个异步组件。在 Vue 3 中,你可以使用 defineAsyncComponent 函数来创建一个异步组件。这个函数接受一个工厂函数作为参数,该工厂函数会在需要的时候返回一个 Promise,该 Promise 解析后的值就是组件的定义。

import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() => import('./MyComponent.vue'));
使用 Suspense

接下来,在你的模板中使用 <Suspense> 组件包裹异步组件。<Suspense> 组件有两个特殊的插槽:#default#fallback#default 插槽用于放置异步组件,而 #fallback 插槽的内容会在异步组件加载完成之前显示。

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

特性

  • 默认和回退插槽Suspense 组件提供了两个特殊的插槽:#default#fallback#default 插槽用于放置需要加载的异步组件,#fallback 插槽则用于在组件加载完成前显示占位内容。

  • 错误处理:如果你的异步组件加载失败,你可以使用 error 事件来处理这种情况。

注意事项

  • 避免滥用:虽然 Suspense 是一个强大的功能,但是过度使用可能会导致应用结构变得复杂。尽量只在需要的地方使用它。

  • 性能考量Suspense 使得 Vue 能够更高效地管理异步组件的加载,但是在一些极端情况下,可能会对性能产生影响,特别是当多个异步组件同时加载时。

总结

Suspense 是 Vue 3 中用于处理异步组件加载状态的一个非常实用的功能。它可以帮助你优雅地处理组件加载中的状态,以及错误处理。通过使用 Suspense,你可以轻松地提升用户体验,特别是在那些依赖于异步数据的应用场景中。

8.3.【全局API转移到应用对象】

在 Vue 3 中,为了更好地组织和管理全局 API,一些原本作为全局函数提供的 API 被移动到了 Vue 应用实例 (app) 上。这样做有助于将全局 API 的作用域限定在一个具体的 Vue 应用中,从而避免全局命名冲突,并提供更清晰的 API 使用方式。

移动的全局 API

以下是一些从全局函数转移到 Vue 应用实例 (app) 上的 API:

  1. component

    • 用途:注册全局组件。
    • 以前的全局函数:Vue.component(name, component)
    • 新的 API:app.component(name, component)
  2. directive

    • 用途:注册全局指令。
    • 以前的全局函数:Vue.directive(name, directive)
    • 新的 API:app.directive(name, directive)
  3. config

    • 用途:访问和修改 Vue 的配置选项。
    • 以前的全局对象:Vue.config
    • 新的 API:app.config
  4. use

    • 用途:注册插件。
    • 以前的全局函数:Vue.use(plugin)
    • 新的 API:app.use(plugin)
  5. provideinject

    • 用途:提供和注入依赖。
    • 以前的全局函数:在组件内使用 provide 和 inject 选项。
    • 新的 API:在组件内使用 setup 函数中的 provide 和 inject 语法。
  6. mount

    • 用途:挂载 Vue 应用到 DOM。
    • 以前的全局函数:new Vue({...}).$mount(selector)
    • 新的 API:app.mount(selector)

示例

创建 Vue 应用实例
import { createApp } from 'vue';

const app = createApp({});
注册全局组件
app.component('MyComponent', {
  template: '<div>Hello from MyComponent!</div>'
});
注册全局指令
app.directive('focus', {
  mounted(el) {
    el.focus();
  }
});
注册插件
app.use(myPlugin);
配置选项
app.config.globalProperties.myGlobalMethod = function () {
  console.log('Called from global properties!');
};
挂载应用
app.mount('#app');

总结

  • 迁移原因:为了提供更清晰的作用域和避免全局命名冲突。
  • 新 API:现在这些 API 都可以通过 Vue 应用实例 (app) 访问。
  • 优势:更加模块化和易于维护。

通过这种方式,您可以更轻松地管理您的 Vue 应用,并且避免在项目中出现全局命名冲突的问题。这种方式也使得代码更加清晰和易于理解。

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐