vue3 + ts 快速入门(全)

devtools/2024/12/22 11:01:16/

文章目录

  • 学习链接
  • 1. Vue3简介
    • 1.1. 性能的提升
    • 1.2.源码的升级
    • 1.3. 拥抱TypeScript
    • 1.4. 新的特性
  • 2. 创建Vue3工程
    • 2.1. 基于 vue-cli 创建
    • 2.2. 基于 vite 创建(推荐)
      • vite介绍
      • 创建步骤
      • 项目结构
        • 安装插件
        • 项目结构
        • 总结
    • 2.3. 一个简单的效果
      • Person.vue
      • App.vue
  • 3. Vue3核心语法
    • 3.1. OptionsAPI 与 CompositionAPI
      • Options API 的弊端
      • Composition API 的优势
    • 3.2. 拉开序幕的 setup
      • setup 概述
      • setup 的返回值
      • setup 与 Options API 的关系
      • setup 语法糖
    • 3.3. ref 创建:基本类型的响应式数据
    • 3.4. reactive 创建:对象类型的响应式数据
    • 3.5 ref 创建:对象类型的响应式数据
    • 3.6. ref 对比 reactive
      • 宏观角度
      • 区别
      • 使用原则
    • 3.7 toRefs 与 toRef
      • 现象
      • toRefs&toRef的使用
    • 3.8 computed
    • 3.9 watch
      • 作用
      • 特点
      • 场景
        • * 情况一
        • * 情况二
          • 示例1
          • 示例2
        • * 情况三
        • * 情况四
          • 没有监视的代码
          • 监视reactive定义的对象类型中的某个基本属性
          • 监视reactive定义的对象类型中的某个对象属性
        • * 情况五
    • 3.10 watchEffect
    • 3.11. 标签的 ref 属性
      • 用在普通DOM标签上
      • 用在组件标签上(defineExpose)
    • 3.12 回顾TS
      • main.ts
      • App.vue
      • index.ts
      • Person.vue
    • 3.13 props(defineProps)
      • App.vue
      • index.ts
      • Person.vue
    • 3.14 生命周期
      • App.vue
      • Person.vue
    • 3.15 自定义hooks
      • 未使用hooks前
        • App.vue
        • Person.vue
      • 使用hooks
        • App.vue
        • Person.vue
        • hooks/useSum.ts
        • hooks/useDog.ts
  • 4.路由
    • 4.1 路由的基本理解
    • 4.2 基本切换效果
      • 安装vue-router
      • 配置路由规则router/index.ts
      • 使用router路由管理器main.ts
      • 路由展示区App.vue
      • 路由组件
        • Home.vue
        • New.vue
        • About.vue
      • 路由切换效果图
    • 4.3. 两个注意点
      • About.vue
    • 4.4. 路由器工作模式
    • 4.5. to的两种写法
    • 4.6. 命名路由
    • 4.7 嵌套路由
      • main.ts
      • router/index.ts
      • App.vue
      • News.vue
      • Detail.vue
      • 效果
    • 4.8 路由传参
      • query参数
      • params参数
    • 4.9 路由的props配置
    • 4.10 replace属性
      • 示例
    • 4.11 编程式导航
      • 示例
    • 4.12 重定向
      • 示例
  • 5. pinia
    • 5.1 准备一个效果
      • main.ts
      • App.vue
      • Count.vue
      • LoveTalk.vue
    • 5.2 搭建 pinia 环境
      • 使用步骤
    • 5.3 存储+读取数据
      • store/count.ts
      • store/loveTalk.ts
      • Count.vue
      • LoveTalk.vue
      • App.vue
      • main.ts
    • 5.4 修改数据(三种方式)
      • 第一种方式
        • count.ts
        • Count.vue
      • 第二种方式
        • count.ts
        • Count.vue
      • 第三种方式
        • count.ts
        • Count.vue
    • 5.5 storeToRefs用法
      • LoveTalk.ts
      • LoveTask.vue
      • count.ts
      • Count.vue
    • 5.6 getters用法
      • count.ts
      • Count.vue
    • 5.7 $subscribe的使用
      • loveTalk.ts
      • LoveTalk.vue
    • 5.8 store组合式写法
      • loveTalk.js
      • LoveTalk.vue
  • 6. 组件通信
    • 6.1 props
      • Father.vue
      • Child.vue
    • 6.2 自定义事件
      • Father.vue
      • Child.vue
    • 6.3 mitt
      • emitter.ts
      • Father.vue
      • Child1.vue
      • Child2.vue
    • 6.4 v-model
      • Father.vue
      • AtguiguInput.vue
    • 6.5 $attrs
      • Father.vue
      • Child.vue
      • GrandChild.vue
    • 6.6 r e f s 、 refs、 refsparent、proxy
      • Father.vue
      • Child1.vue
      • Child2.vue
    • 6.7 provide、inject
      • Father.vue
      • Child.vue
      • GrandChild.vue
    • 6.8 pinia
    • 6.9 slot插槽
      • 1. 默认插槽
        • Father.vue
        • Category.vue
      • 2. 具名插槽
        • Father.vue
        • Category.vue
      • 3. 作用域插槽
        • Father.vue
        • Category.vue
  • 7. 其它 API
    • 7.1 shallowRef 与 shallowReactive
      • shallowRef
      • shallowReactive
      • 示例
    • 7.2 readonly 与 shallowReadonly
      • readonly
      • shallowReadonly
      • 示例
    • 7.3 toRaw 与 markRaw
      • toRaw
      • markRaw
      • 示例
    • 7.4 customRef
      • 示例
        • App.vue
        • useMsgRef.ts
  • 8. Vue3新组件
    • 8.1 Teleport传送门
      • 示例
        • App.vue
        • Modal.vue
    • 8.2 Suspense
      • 示例
        • App.vue
        • Child.vue
    • 8.3 全局API转移到应用对象
      • 示例
    • 8.4 其他

学习链接

尚硅谷Vue3入门到实战,最新版vue3+TypeScript前端开发教程

Vue3+Vite4+Pinia+ElementPlus从0-1 web项目搭建

Vue3.2后台管理系统

深入Vue3+TypeScript技术栈 coderwhy

尚硅谷Vue项目实战硅谷甄选,vue3项目+TypeScript前端项目一套通关

Vue3 + vite + Ts + pinia + 实战 + 源码 + electron - 百万播放量哦

1. Vue3简介

  • 2020年9月18日,Vue.js发布版3.0版本,代号:One Piece

  • 经历了:4800+次提交、40+个RFC、600+次PR、300+贡献者

  • 官方发版地址:Release v3.0.0 One Piece · vuejs/core

  • 截止2023年10月,最新的公开版本为:3.3.4

在这里插入图片描述

1.1. 性能的提升

  • 打包大小减少41%

  • 初次渲染快55%, 更新渲染快133%

  • 内存减少54%

1.2.源码的升级

  • 使用Proxy代替defineProperty实现响应式。

  • 重写虚拟DOM的实现和Tree-Shaking

1.3. 拥抱TypeScript

  • Vue3可以更好的支持TypeScript

1.4. 新的特性

  1. Composition API(组合API):

    • setup

    • refreactive

    • computedwatch

  2. 新的内置组件:

    • Fragment

    • Teleport

    • Suspense

  3. 其他改变:

    • 新的生命周期钩子

    • data 选项应始终被声明为一个函数

    • 移除keyCode支持作为 v-on 的修饰符

2. 创建Vue3工程

2.1. 基于 vue-cli 创建

点击查看 Vue-Cli 官方文档,(基于vue-cli创建,其实就是基于webpack来创建vue项目)

备注:目前vue-cli已处于维护模式,官方推荐基于 Vite 创建项目。

## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version## 安装或者升级你的@vue/cli 
npm install -g @vue/cli## 执行创建命令
vue create vue_test##  随后选择3.x
##  Choose a version of Vue.js that you want to start the project with (Use arrow keys)
##  > 3.x
##    2.x## 启动
cd vue_test
npm run serve

2.2. 基于 vite 创建(推荐)

vite介绍

vite 是新一代前端构建工具,官网地址:https://vitejs.cn,vite的优势如下:

  • 轻量快速的热重载(HMR),能实现极速的服务启动。
  • TypeScriptJSXCSS 等支持开箱即用(不用配置,直接就可以用)。
  • 真正的按需编译,不再等待整个应用编译完成。
  • webpack构建 与 vite构建对比图如下:
    webpack构建 vite构建

创建步骤

具体操作如下(点击查看官方文档)

## 1.创建命令(基于vite创建vue3项目,前提是需要安装nodejs环境)
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

构建过程如下:

在这里插入图片描述

访问vue3项目如下:

在这里插入图片描述

项目结构

安装插件

安装官方推荐的vscode插件:

在这里插入图片描述

在这里插入图片描述

项目结构

在这里插入图片描述

index.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><link rel="icon" href="/favicon.ico"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Vite App</title></head><body><div id="app"></div><script type="module" src="/src/main.ts"></script></body>
</html>

main.ts

import './assets/main.css'// 引入createApp用于创建应用
import { createApp } from 'vue'// 引入App根组件
import App from './App.vue'createApp(App).mount('#app')

App.vue

<!-- 自己动手编写的一个App组件 -->
<template><div class="app"><h1>你好啊!</h1></div>
</template><script lang="ts"> // 添加lang="ts", 里面写ts或js都可以export default {name:'App' //组件名}</script><style>.app {background-color: #ddd;box-shadow: 0 0 10px;border-radius: 10px;padding: 20px;}
</style>
总结
  • Vite 项目中,index.html 是项目的入口文件,在项目最外层。
  • 加载index.html后,Vite 解析 <script type="module" src="xxx"> 指向的JavaScript
  • Vue3在main.ts中是通过 createApp 函数创建一个应用实例。

2.3. 一个简单的效果

Vue3向下兼容Vue2语法,且Vue3中的模板中可以没有根标签

Person.vue

<template><div class="person"><h2>姓名:{{name}}</h2><h2>年龄:{{age}}</h2><button @click="changeName">修改名字</button><button @click="changeAge">年龄+1</button><button @click="showTel">点我查看联系方式</button></div>
</template><script lang="ts">export default {name:'App',data() {return {name:'张三',age:18,tel:'13888888888'}},methods:{changeName(){this.name = 'zhang-san'},changeAge(){this.age += 1},showTel(){alert(this.tel)}},}
</script>

App.vue

<template><div class="app"><h1>你好啊!</h1><Person/></div>
</template><script lang="ts">import Person from './components/Person.vue'export default {name:'App', //组件名components:{Person} //注册组件}
</script><style>.app {background-color: #ddd;box-shadow: 0 0 10px;border-radius: 10px;padding: 20px;}
</style>

3. Vue3核心语法

3.1. OptionsAPI 与 CompositionAPI

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

Options API 的弊端

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

1.gif2.gif

Composition API 的优势

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

3.gif4.gif

3.2. 拉开序幕的 setup

setup 概述

介绍

  • setupVue3中一个新的配置项,值是一个函数。
  • 它是 Composition API “表演的舞台,组件中所用到的:数据、方法、计算属性、监视…等等,均配置在setup中。

特点如下:

  • setup函数返回的对象中的内容,可直接在模板中使用。
  • setup中访问thisundefined
  • setup函数会在beforeCreate之前调用,它是“领先”所有钩子执行的。
<template><div class="person"><h2>姓名:{{name}}</h2><h2>年龄:{{age}}</h2><button @click="changeName">修改名字</button><button @click="changeAge">年龄+1</button><button @click="showTel">点我查看联系方式</button></div>
</template><script lang="ts">export default {name:'Person',// 生命周期函数beforeCreate(){console.log('beforeCreate')},setup(){// 先打印的setup..., 再打印的beforeCreate, 说明了setup函数与beforeCreate生命周期函数的执行顺序console.log('setup ...')// 【setup函数中的this是undefined】console.log(this); // undefined// 数据,原来写在data中【注意:此时的name、age、tel数据都不是响应式数据】//                   (不是响应式的意思是:当这些数据变化,并不会触发dom更新,//                                     模板中应用这些变量的地方没有重新渲染)let name = '张三'let age = 18let tel = '13888888888'// 方法,原来写在methods中function changeName(){name = 'zhang-san'        // 注意:此时这么修改name页面是不变化的console.log(name)         // (name确实改了,但name不是响应式的)}function changeAge(){age += 1                  // 注意:此时这么修改age页面是不变化的console.log(age)          // (age确实改了,但age不是响应式的)}function showTel(){alert(tel)}// 返回一个对象,对象中的内容,模板中可以直接使用(将数据、方法交出去,模板中才可以使用这些交出去的数据、方法)return {name,age,tel,changeName,changeAge,showTel}}}
</script>

setup 的返回值

  • 若返回一个对象:则对象中的:属性、方法等,在模板中均可以直接使用**(重点关注)。**
  • 若返回一个函数:则可以直接指定 自定义渲染的内容,代码如下:
<template><div class="person">我特么一点都不重要了</div>
</template><script lang="ts">export default {name:'Person',setup(){// setup的返回值也可以是一个渲染函数// (模板什么的都不重要了,直接在页面上渲染成:你好啊!这几个字)// return ()=>'哈哈'}}
</script><style scoped>.person {background-color: skyblue;box-shadow: 0 0 10px;border-radius: 10px;padding: 20px;}button {margin: 0 5px;}
</style>

setup 与 Options API 的关系

  • Vue2 的配置(datamethos…)中可以访问到 setup中的属性、方法。
  • 但在setup不能访问到Vue2的配置(datamethos…)。
  • 如果与Vue2冲突,则setup优先。
<template><div class="person"><h2>姓名:{{name}}</h2><h2>年龄:{{age}}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="showTel">查看联系方式</button><hr><h2>测试1:{{a}}</h2><h2>测试2:{{c}}</h2><h2>测试3:{{d}}</h2><button @click="b">测试</button></div>
</template><script lang="ts">export default {name:'Person',beforeCreate(){console.log('beforeCreate')},data(){return {a:100,// 在data配置项中, 可以使用this.name来使用setup中交出的数据, 因为setup执行时机更早。// 但是在setup中不能使用在data中定义的数据c:this.name, d:900,age:90}},methods:{b(){console.log('b')}},// setup可以与data、methods等配置项同时存在setup(){// 数据,原来是写在data中的,此时的name、age、tel都不是响应式的数据let name = '张三'let age = 18let tel = '13888888888'// 方法function changeName() {name = 'zhang-san' // 注意:这样修改name,页面是没有变化的console.log(name)  // name确实改了,但name不是响应式的}function changeAge() {age += 1           // 注意:这样修改age,页面是没有变化的console.log(age)   // age确实改了,但age不是响应式的}function showTel() {alert(tel)}// 将数据、方法交出去,模板中才可以使用return {name,age,tel,changeName,changeAge,showTel}// setup的返回值也可以是一个渲染函数// return ()=>'哈哈'}}
</script><style scoped>.person {background-color: skyblue;box-shadow: 0 0 10px;border-radius: 10px;padding: 20px;}button {margin: 0 5px;}
</style>

setup 语法糖

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

<template><div class="person"><h2>姓名:{{name}}</h2><h2>年龄:{{age}}</h2><button @click="changName">修改名字</button><button @click="changAge">年龄+1</button><button @click="showTel">点我查看联系方式</button></div></template><!-- 专门单个弄个script标签, 特地来配置组件的名字 -->
<script lang="ts">export default {name:'Person',}
</script><!-- 下面的写法是setup语法糖 -->
<!-- 1. 相当于写了setup函数; 2. 相当于自动把其中定义的变量交出去(包括里面引入的其它组件也会交出去, 可以在模板中使用引入的组件))-->
<script setup lang="ts">console.log(this) // undefined// 数据(注意:此时的name、age、tel都不是响应式数据)let name = '张三'let age = 18let tel = '13888888888'// 方法function changName(){name = '李四'//注意:此时这么修改name页面是不变化的}function changAge(){console.log(age)age += 1 //注意:此时这么修改age页面是不变化的}function showTel(){alert(tel)}
</script>

扩展:上述代码,还需要编写一个不写setupscript标签,去指定组件名字,比较麻烦,我们可以借助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))}}
})
  1. 第三步:<script setup lang="ts" name="Person">

3.3. ref 创建:基本类型的响应式数据

  • **作用:**定义响应式变量。
  • 语法:let xxx = ref(初始值)
  • **返回值:**一个RefImpl的实例对象,简称ref对象refref对象的value属性是响应式的
  • 注意点:
    • JS中操作数据需要:xxx.value,但模板中不需要.value,直接使用即可。
    • 对于let name = ref('张三')来说,name不是响应式的,name.value是响应式的。
<template><div class="person"><!-- 模板中直接使用, 不需要.value --><h2>姓名:{{name}}</h2><h2>年龄:{{age}}</h2><h2>电话:{{tel}}</h2><button @click="changeName">修改名字</button><button @click="changeAge">年龄+1</button><button @click="showTel">点我查看联系方式</button></div>
</template><!-- 使用了setup语法糖, 会自动将定义的变量和方法交出去, 以供给模板使用 -->
<script setup lang="ts" name="Person">// 引入vue中的ref函数import { ref } from 'vue'// name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的。//(所谓的响应式指的是, 对数据的改变后, 能够让模板中使用该数据的地方得到重新渲染更新)// ref是1个函数, 向这个ref函数中传入参数, 返回的是1个RefImpl的实例对象let name = ref('张三')let age = ref(18)// tel就是一个普通的字符串,不是响应式的let tel = '13888888888'function changeName(){// JS中操作ref对象时候需要.valuename.value = '李四' // 页面得到刷新console.log(name.value)// 注意:name不是响应式的,name.value是响应式的,所以如下代码并不会引起页面的更新。// name = ref('zhang-san')}function changeAge(){// JS中操作ref对象时候需要.valueage.value += 1      // 页面得到刷新console.log(age.value)}function showTel(){// tel是普通数据      tel += '1'          // tel的确改了, 但页面并未刷新alert(tel)}
</script>

3.4. reactive 创建:对象类型的响应式数据

  • 作用:定义一个响应式对象(基本类型不要用它,要用ref,否则报错)
  • 语法:let 响应式对象= reactive(源对象)
  • **返回值:**一个Proxy的实例对象,简称:响应式对象。
  • 注意点:reactive定义的响应式数据是“深层次”的。
<template>
<div class="person"><h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2><h2>游戏列表:</h2><ul><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul><h2>测试:{{ obj.a.b.c.d }}</h2><button @click="changeCarPrice">修改汽车价格</button><button @click="changeFirstGame">修改第一游戏</button><button @click="test">测试</button></div>
</template><script lang="ts" setup name="Person">import { reactive } from 'vue'// 定义数据// reactive是1个函数, 向这个reactive函数中传入参数(传入对象或数组), 返回的是1个Proxy的实例对象//(Proxy是原生Js就有的函数)// reactive函数中传入对象let car = reactive({ brand: '奔驰', price: 100 }) console.log('car', car); // car Proxy {brand: '奔驰', price: 100}// reactive函数传入数组let games = reactive([  { id: 'ahsgdyfa01', name: '英雄联盟' },{ id: 'ahsgdyfa02', name: '王者荣耀' },{ id: 'ahsgdyfa03', name: '原神' }])// reactive定义的响应式数据是 深层次 的let obj = reactive({a: {b: {c: {d: 666}}}})// 修改对象中的属性(修改使用reactive包裹对象后返回的对象)function changeCarPrice() {car.price += 10}// 修改数组中的对象的属性(修改使用reactive包裹数组后返回的对象)function changeFirstGame() {games[0].name = '流星蝴蝶剑'}function test() {obj.a.b.c.d = 999}
</script>

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

  • 其实ref接收的数据可以是:基本类型对象类型
  • ref接收的是对象类型,内部其实也是调用了reactive函数。
<template><div class="person"><h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2><h2>游戏列表:</h2><ul><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul><h2>测试:{{ obj.a.b.c.d }}</h2><button @click="changeCarPrice">修改汽车价格</button><button @click="changeFirstGame">修改第一游戏</button><button @click="test">测试</button></div>
</template><script lang="ts" setup name="Person">import { ref,reactive } from 'vue'// 使用ref定义对象类型响应式数据let car = ref({ brand: '奔驰', price: 100 })// 使用reactive定义对象类型响应式数据let car2 = reactive({brand: '奔驰', price: 100})// reactive只能用来定义对象类型的响应式数据// let name = reactive('zhangsan') // 错误, value cannot be made reactive: zhangsan// 使用ref定义对象(数组)类型响应式数据let games = ref([{ id: 'ahsgdyfa01', name: '英雄联盟' },{ id: 'ahsgdyfa02', name: '王者荣耀' },{ id: 'ahsgdyfa03', name: '原神' }])// 使用ref定义对象类型响应式数据也是深层次的let obj = ref({a: {b: {c: {d: 666}}}})// 若ref接收的是对象类型,内部其实也是使用的reactive函数console.log(car)       // RefImpl {__v_isShallow: false, dep: undefined, //          __v_isRef: true, _rawValue: {…}, _value: Proxy}console.log(car.value) // Proxy {brand: '奔驰', price: 100}console.log(car2)      // Proxy {brand: '奔驰', price: 100}function changeCarPrice() {// 使用ref函数定义的响应式数据, 在js操作时, 需要带上.value, 才能碰到内部的Proxy对象car.value.price += 10console.log(car.value.price);}function changeFirstGame() {// 使用ref函数定义的响应式数据, 在js操作时, 需要带上.value, 才能碰到内部的Proxy对象games.value[0].name = '流星蝴蝶剑'console.log(games.value);   // Proxy {0: {…}, 1: {…}, 2: {…}}}function test() {// 使用ref函数定义的响应式数据, 在js操作时, 需要带上.value, 才能碰到内部的Proxy对象obj.value.a.b.c.d = 999}</script>

3.6. ref 对比 reactive

宏观角度

  • ref可以定义:基本类型、对象类型的响应式数据

  • reactive只能定义:对象类型的响应式数据

区别

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

    可以在齿轮->设置->扩展->volar中勾选自动补充value ,它会在使用ref创建的变量时,自动添加上.value

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

    <template><div class="person"><h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2><button @click="changeBrand">改品牌</button><button @click="changePrice">改价格</button><button @click="changeCar">改car</button></div>
    </template><script lang="ts" setup name="Person">import { ref,reactive } from 'vue'let car = reactive({brand:'奔驰', price:100})function changeBrand() {// 正常修改car的brand, 并且是响应式car.brand = '宝马'
    }function changePrice() {// 正常修改car的price, 并且是响应式car.price += 10
    }function changeCar() {// 错误做法1// 不可以直接给reactive重新分配一个新对象,这会让car直接失去响应式// car = {brand:'奥托', price:10}// 错误做法2// 这样也不行, 因为模板中用的car是上面定义的响应式对象, // 现在car指向的是1个新的响应式对象, 而模板中压根就没有使用这个新的响应式对象// car =  reactive({brand:'奥托', price:10})// 正确做法(car仍然是响应式的)// API介绍: Object.assign(obj1, obj2, obj3, ..), //         将obj2中的每一组属性和值设置到obj1中, 然后obj3的每一组属性和值设置到obj1中Object.assign(car, {brand:'奥托', price:10})
    }</script>
    
    <template><div class="person"><h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2><button @click="changeBrand">改品牌</button><button @click="changePrice">改价格</button><button @click="changeCar">改car</button></div>
    </template><script lang="ts" setup name="Person">import { ref,reactive } from 'vue'let car = ref({brand:'奔驰', price:100})function changeBrand() {// 正常修改car的brand, 并且是响应式car.value.brand = '宝马'
    }function changePrice() {// 正常修改car的price, 并且是响应式car.value.price += 10
    }function changeCar() {// 错误做法1// 不能直接给car换了个ref, 因为模板中压根就没有使用这个新的RefImpl对象// car = ref({brand:'奥托', price:10})// 正确做法1(car仍然是响应式的)// API介绍: Object.assign(obj1, obj2, obj3, ..), 将obj2中的每一组属性和值设置到obj1中, //         然后obj3的每一组属性和值设置到obj1中// Object.assign(car.value, {brand:'奥托', price:10})// 正确做法2//(这里相比于对car使用reactive定义而言, 使用ref定义则可以直接给car.value整体赋值// 原因在于car.value获取的是Proxy响应式对象, 凡是对Proxy响应式对象的操作都可以被拦截到)car.value = {brand:'奥托', price:10}}</script>
    

使用原则

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

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

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

3.7 toRefs 与 toRef

  • 作用:将一个响应式对象中的每一个属性,转换为ref对象。
  • 备注:toRefstoRef功能一致,但toRefs可以批量转换。

现象

对响应式对象直接结构赋值,得到的数据不是响应式的

<template><div class="person"><h2>姓名:{{ person.name }} {{ name }}</h2><h2>年龄:{{ person.age }}  {{ age }}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button></div>
</template><script lang="ts" setup name="Person2">import { ref, reactive, toRefs, toRef } from 'vue'// 数据let person = reactive({ name: '张三', age: 18 })console.log(person);                // Proxy {name: '张三', age: 18}// 这里的解构赋值其实就等价于: let name = person.name; let age = person.age;// 只是记录了此时person.name、person.age的值, 仅此而已// 因此, 此处使用结构赋值语法获取的name和age都不是响应式的let {name, age } = personconsole.log(name, age);              // 张三 18// 方法function changeName() {name += '~'console.log(name, person.name);  // 变化的是name, 而person.name仍然未修改}function changeAge() {age += 1console.log(age, person.age);    // 变化的是age, 而person.age仍然未修改}</script>

toRefs&toRef的使用

通过toRefs将person对象中的所有属性都批量取出, 且依然保持响应式的能力

<template>
<div class="person"><h2>姓名:{{ person.name }}</h2><h2>年龄:{{ person.age }}</h2><h2>性别:{{ person.gender }}  {{ gender }}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="changeGender">修改性别</button><button @click="changeGender2">修改性别2</button></div>
</template><script lang="ts" setup name="Person">import { ref, reactive, toRefs, toRef } from 'vue'// 数据let person = reactive({ name: '张三', age: 18, gender: '男' })// 通过toRefs将person对象中的所有属性都批量取出, 且依然保持响应式的能力//(使用toRefs从person这个响应式对象中,解构出name、age, 且name和age依然是响应式的,//  name和gender的值是ref类型, 其value值指向的是person.name和person.age,//  对name.value和对age.value的修改将会修改person.name和person.age, 并且会页面渲染刷新)let { name, age } = toRefs(person)console.log(name.value, name);     // '张三' ObjectRefImpl {_object: Proxy, _key: 'name', //                 _defaultValue: undefined, __v_isRef: true}console.log(age.value, age.value); // 18 ObjectRefImpl {_object: Proxy, _key: 'age', //                   _defaultValue: undefined, __v_isRef: true}console.log(toRefs(person));       // {name: ObjectRefImpl, age: ObjectRefImpl, //  gender: ObjectRefImpl}// 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力let gender = toRef(person, 'gender')console.log(gender, gender.value); // ObjectRefImpl {_object: Proxy, _key: 'gender', //               _defaultValue: undefined, __v_isRef: true} '男'// 方法function changeName() {// 此处修改name.value, 将会修改person.name, 并且页面会刷新person.name的值name.value += '~'console.log(name.value, person.name);}function changeAge() {// 此处修改age.value, 将会修改person.age, 并且页面会刷新person.age的值age.value += 1console.log(age.value, person.age);}function changeGender() {// 此处修改gender.value, 将会修改person.age, 并且页面会刷新person.gender的值gender.value = '女'console.log(gender.value, person.gender);}function changeGender2() {// 此处对person.gender的修改, 将会修改上面的let gender = toRef(person, 'gender')// 并且页面会刷新person.gender和gender的值person.gender = '男'console.log(gender.value, person.gender);}
</script>

3.8 computed

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

<template><div class="person">姓:<input type="text" v-model="firstName"> <br>名:<input type="text" v-model="lastName"> <br>全名:<span>{{ fullName }}</span> <br><button @click="changeFullName">全名改为: li-si</button></div>
</template><script setup lang="ts" name="App">// 引入computed计算属性函数
import { ref, computed } from 'vue'let firstName = ref('zhang')
let lastName = ref('san')// 计算属性——只读取,不修改
/* 
// 1. 使用时, 在computed中传入1个函数。在模板中, 直接使用计算属性即可。
// 2. 当计算属性依赖的数据只要发生变化, 它就会重新计算, 如果页面中有使用到该计算属性, 那么就会重新渲染模板 
// 3. 只会计算1次, 后面会使用缓存, 而方法是没有缓存的
let fullName = computed(()=>{return firstName.value + '-' + lastName.value
}) 
console.log(fullName); // ComputedRefImpl {dep: undefined, __v_isRef: true, //                  __v_isReadonly: true, effect: ReactiveEffect, _setter: ƒ, …}*/// 计算属性——既读取又修改
let fullName = computed({// 读取get() {// 当firstName或lastName变化时, 计算属性会重新计算, 并刷新页面渲染return firstName.value + '-' + lastName.value},// 修改// 当修改计算属性时(或者说给计算属性赋值时, 注意要.value), 此方法会被调用set(val) {console.log('有人修改了fullName', val)firstName.value = val.split('-')[0]lastName.value = val.split('-')[1]}
})function changeFullName() {// 修改fullName计算属性(会触发计算属性中set方法的调用)fullName.value = 'li-si'
}
</script>

3.9 watch

作用

监视数据的变化(和Vue2中的watch作用一致)

特点

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

  • ref定义的数据。

  • reactive定义的数据。

  • 函数返回一个值(getter函数,所谓的getter函数就是能返回一个值的函数)。

  • 一个包含上述内容的数组。

场景

我们在Vue3中使用watch的时候,通常会遇到以下几种情况:

* 情况一

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

<template><div class="person"><h1>情况一:监视【ref】定义的【基本类型】数据</h1><h2>当前求和为:{{ sum }}</h2><button @click="changeSum">点我sum+1</button></div>
</template><script lang="ts" setup name="Person">// 引入watch监视函数
import { ref, watch } from 'vue'// 数据
let sum = ref(0)// 方法
function changeSum() {sum.value += 1
}// 监视,情况一:监视【ref】定义的【基本类型】数据
//(注意:这里监视写的是sum, 而不是sum.value哦)
const stopWatch = watch(sum, (newValue, oldValue) => {console.log('sum变化了', newValue, oldValue) // 注意: 这里也没带.value哦if (newValue >= 10) {// 解除监视(即: 当调用此方法后, 不会再监视sum的变化了, 也就是当sum变化时, 当前的监视函数不再执行了)stopWatch()}
})</script><style scoped>
...
</style>
* 情况二

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

注意:

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

  • 若修改整个ref定义的对象,newValue 是新值, oldValue 是旧值,因为不是同一个对象了。

示例1
<template><div class="person"><h1>情况二:监视【ref】定义的【对象类型】数据</h1><h2>姓名:{{ person.name }}</h2><h2>年龄:{{ person.age }}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="changePerson">修改整个人</button></div>
</template><script lang="ts" setup name="Person">import { ref, watch } from 'vue'// 数据
let person = ref({name: '张三',age: 18
})// 方法
function changeName() {person.value.name += '~'   // 当修改person.value.name时, 监视函数未被触发
}function changeAge() {person.value.age += 1      // 当修改person.value.age时, 监视函数也未被触发
}function changePerson() {person.value = { name: '李四', age: 90 }  // 当整体修改person.value时, 此时监视函数被触发
}                                            // (因为监视的是对象的地址值, 所以这里每次修改都会触发监视函数)/* 监视,情况一:监视【ref】定义的【对象类型】数据,监视的是对象的地址值。watch的第一个参数是:被监视的数据watch的第二个参数是:监视的回调
*/
watch(person, (newValue, oldValue) => {console.log('person变化了', newValue, oldValue)// 一直调用changePerson方法, 控制台如下输出// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '张三', age: 18}// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}// ...
})</script><style scoped>
...
</style>
示例2
<template><div class="person"><h1>情况二:监视【ref】定义的【对象类型】数据</h1><h2>姓名:{{ person.name }}</h2><h2>年龄:{{ person.age }}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="changePerson">修改整个人</button></div>
</template><script lang="ts" setup name="Person">import { ref, watch } from 'vue'// 数据
let person = ref({name: '张三',age: 18
})// 方法
function changeName() {person.value.name += '~'// 因为开启了深度监视, 当修改person.value.name时, 监视函数被触发
}                         //(但由于原对象并未修改, 所以监视函数中输出的newVal和oldVal是一样的)// 每次调用changeName都修改, 变化如下:// person变化了 Proxy {name: '张三~', age: 18} Proxy {name: '张三~', age: 18}// person变化了 Proxy {name: '张三~~', age: 18} Proxy {name: '张三~~', age: 18}// person变化了 Proxy {name: '张三~~', age: 18} Proxy {name: '张三~~', age: 18}// ...
function changeAge() {person.value.age += 1   // 因为开启了深度监视, 当修改person.value.name时, 监视函数被触发
}                           //(但由于原对象并未修改, 所以监视函数中输出的newVal和oldVal是一样的)// 每次调用changeName都修改, 变化如下:// person变化了 Proxy {name: '张三', age: 19} Proxy {name: '张三', age: 19}// person变化了 Proxy {name: '张三', age: 20} Proxy {name: '张三', age: 20}// person变化了 Proxy {name: '张三', age: 21} Proxy {name: '张三', age: 21}// ...function changePerson() {person.value = { name: '李四', age: 90 }// 当整体修改person.value时, 监视函数被触发//(但由于原对象都改了, 所以监视函数中输出的newVal和oldVal是不一样的)// 每次调用changeName都修改, 变化如下:
}                           // person变化了 Proxy {name: '李四', age: 90} Proxy {name: '张三', age: 18}// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}// person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}// ...
/* 监视,情况二:监视【ref】定义的【对象类型】数据,监视的是对象的地址值,若想监视对象内部属性的变化,需要手动开启深度监视watch的第一个参数是:被监视的数据watch的第二个参数是:监视的回调watch的第三个参数是:配置对象(deep、immediate等等) 
*/
watch(person, (newValue, oldValue) => {console.log('person变化了', newValue, oldValue)
}, { deep: true, immediate: true })</script><style scoped>
.person {background-color: skyblue;box-shadow: 0 0 10px;border-radius: 10px;padding: 20px;
}button {margin: 0 5px;
}li {font-size: 20px;
}
</style>
* 情况三

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

<template>
<div class="person"><h1>情况三:监视【reactive】定义的【对象类型】数据</h1><h2>姓名:{{ person.name }}</h2><h2>年龄:{{ person.age }}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="changePerson">修改整个人</button><hr><h2>测试:{{obj.a.b.c}}</h2><button @click="test">修改obj.a.b.c</button></div>
</template><script lang="ts" setup name="Person">import {reactive,watch} from 'vue'// 数据let person = reactive({name:'张三',age:18})let obj = reactive({a:{b:{c:666}}})// 方法function changeName(){person.name += '~'// 每次调用changeName都修改, 变化如下:// person变化了 Proxy {name: '张三~', age: 18} Proxy {name: '张三~', age: 18}// person变化了 Proxy {name: '张三~~', age: 18} Proxy {name: '张三~~', age: 18}// person变化了 Proxy {name: '张三~~~', age: 18} Proxy {name: '张三~~~', age: 18}// ...//(如上结果, //  1. 证明监视到了person的name //  2. oldVal和newVal是一样的输出, 是因为虽然监测到person的变化, 但oldVal和newVal是同一对象, 从这来说并未改变)}function changeAge(){person.age += 1// 每次调用changeAge都修改, 变化如下:// person变化了 Proxy {name: '张三', age: 19} Proxy {name: '张三', age: 19}// person变化了 Proxy {name: '张三', age: 20} Proxy {name: '张三', age: 20}// person变化了 Proxy {name: '张三', age: 21} Proxy {name: '张三', age: 21}// ...//(如上结果, //  1. 证明监视到了person的age//  2. oldVal和newVal是一样的输出, 是因为虽然监测到person的变化, 但oldVal和newVal是同一对象, 从这来说并未改变)}function changePerson(){// 此处注意: 使用reactive函数定义的数据, 不能直接替换, 可以如下方式对person中的属性做批量修改 Object.assign(person,{name:'李四',age:80})// 多次调用changePerson, 仅有1次监视到到修改, 变化如下:// person变化了 Proxy {name: '李四', age: 80} Proxy {name: '李四', age: 80}//(如上结果, //  1. 证明监视到了person的name和age的改变//  2. oldVal和newVal是一样的输出, 是因为虽然监测到person的变化, 但oldVal和newVal仍是同一对象, 从这来说并未改变)}function test(){obj.a.b.c = 888// 此处证明watch监控reactive定义的对象类型数据, 默认是开启了深度监视的}// 监视,情况三:监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的(隐式创建了深层次的监听, 无法关闭)watch(person,(newValue,oldValue)=>{console.log('person变化了',newValue,oldValue)})watch(obj,(newValue,oldValue)=>{console.log('Obj变化了',newValue,oldValue)})</script><style scoped>
...
</style>
* 情况四

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

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

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

没有监视的代码
<template>
<div class="person"><h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1><h2>姓名:{{ person.name }}</h2><h2>年龄:{{ person.age }}</h2><h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="changeC1">修改第一台车</button><button @click="changeC2">修改第二台车</button><button @click="changeCar">修改整个车</button></div>
</template><script lang="ts" setup name="Person">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是使用reactive定义的, 所以person整体不能改(改是可以改, 但是不再响应式了, //                                                         所以说不能整体直接改), //          但是person里面的car属性可以改, 因此可以如下改person.car = { c1: '雅迪', c2: '爱玛' }}</script><style scoped>...
</style>
监视reactive定义的对象类型中的某个基本属性
<template><div class="person"><h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1><h2>姓名:{{ person.name }}</h2><h2>年龄:{{ person.age }}</h2><h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="changeC1">修改第一台车</button><button @click="changeC2">修改第二台车</button><button @click="changeCar">修改整个车</button></div>
</template><script lang="ts" setup name="Person">import { reactive, watch } from 'vue'// 数据let person = reactive({name: '张三',age: 18,car: {c1: '奔驰',c2: '宝马'}})// 方法function changeName() {person.name += '~'// 一直调用changeName方法, 控制台如下输出// person.name变化了 张三~ 张三// person.name变化了 张三~~ 张三~// person.name变化了 张三~~~ 张三~~// ...}function changeAge() {person.age += 1}function changeC1() {person.car.c1 = '奥迪'}function changeC2() {person.car.c2 = '大众'}function changeCar() {// 注意此处: 因为person是使用reactive定义的, 所以person整体不能改(改是可以改, 但是不再响应式了, //                                                         所以说不能整体直接改), //          但是person里面的car属性可以改, 因此可以如下改person.car = { c1: '雅迪', c2: '爱玛' }}// 监视,情况四:监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式(不能直接写person.name哦)//(如下监视, 将会只监视person的name属性的变化, //  当person的name属性发生变化时, 将会触发监听函数执行, 其它属性变化不会触发监听函数的执行)watch(()=> person.name,(newValue,oldValue)=>{console.log('person.name变化了',newValue,oldValue)}) // 错误写法, 因为person的name属性是基本类型, 所以不能直接写为第1个参数, 应该要用函数包一下/*watch(person.name,(newValue,oldValue)=>{console.log('person.name变化了',newValue,oldValue)})*/// 监视person的car属性中的c1属性//(当调用changeC1方法时, 此处能够监测到person.car.c1的改变;//  多次调用changeC1方法, 此处只监测到了1次, 因为后面都没改person.car.c1的值;//  当调用changeCar方法, 此处能够监测到person.car.c1的改变;//  多次调用changeCar方法, 此处只监测到了1次, 因为后面都没改person.car.c1的值;)watch(()=> person.car.c1,(newValue,oldValue)=>{console.log('person.car.c1变化了',newValue,oldValue)})// 错误写法, 因为person的car.c1属性是基本类型, 所以不能直接写为第1个参数, 应该要用函数包一下/*watch(person.car.c1,(newValue,oldValue)=>{console.log('person.car.c1变化了',newValue,oldValue)})*/</script><style scoped>
...
</style>
监视reactive定义的对象类型中的某个对象属性
<template><div class="person"><h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1><h2>姓名:{{ person.name }}</h2><h2>年龄:{{ person.age }}</h2><h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="changeC1">修改第一台车</button><button @click="changeC2">修改第二台车</button><button @click="changeCar">修改整个车</button></div>
</template><script lang="ts" setup name="Person">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是使用reactive定义的, 所以person整体不能改(改是可以改, 但是不再响应式了, //                                                             所以说不能整体直接改), //          但是person里面的car属性可以改, 因此可以如下改person.car = { c1: '雅迪', c2: '爱玛' }}// 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数// 建议写成函数的形式// 当调用changeC1或changeC2方法时, 会触发此处的监测函数执行// 当调用changeCar方法时, 会触发此处的监测函数执行// 【最佳实践】(函数式来开启对person.car的地址值的监测, 然后deep:true开启对该对象的深度监视)watch(() => person.car, (newValue, oldValue) => {console.log('person.car变化了', newValue, oldValue)}, { deep: true })// 如果写成下面这样, 监测的其实是person.car的地址值, 只有在person.car整体改变时, 才会触发此处的监测函数执行//  当调用changeC1或changeC2方法时, 不会触发此处的监测函数执行/* watch(() => person.car, (newValue, oldValue) => {console.log('person.car变化了', newValue, oldValue)}) */// 如果写成下面这样(直接写的做法), 那么当调用changeCar方法时, 不会触发此处的监测函数执行// 当调用changeC1或changeC2方法时, 会触发此处的监测函数执行//(因为person.car是person中的对象类型属性, 因此这里可以直接写)/* watch(person.car, (newValue, oldValue) => {console.log('person.car变化了', newValue, oldValue)}) */</script><style scoped>...
</style>
* 情况五

监视上述的多个数据

<template>
<div class="person"><h1>情况五:监视上述的多个数据</h1><h2>姓名:{{ person.name }}</h2><h2>年龄:{{ person.age }}</h2><h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="changeC1">修改第一台车</button><button @click="changeC2">修改第二台车</button><button @click="changeCar">修改整个车</button></div>
</template><script lang="ts" setup name="Person">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:'爱玛'}}// 监视,情况五:监视上述的多个数据//(person.name是基本类型, 所以要写成函数式; person.car是对象类型, 所以可以直接写;// 这里的newVal和oldVal都是数组, 跟监视的2个源相对应; // deep开启深度监视, 不止可以监视地址值, 还包括内部属性的变化;)watch([()=>person.name, person.car],(newValue, oldValue)=>{console.log('person.car变化了',newValue,oldValue)},{deep:true})</script><style scoped>
...
</style>

3.10 watchEffect

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

watch对比watchEffect

  1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同

  2. watch:要明确指出监视的数据

  3. watchEffect:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。

<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、heightwatch([temp,height],(value)=>{// 从value中获取最新的temp值、height值const [newTemp,newHeight] = value// 室温达到50℃,或水位达到20cm,立刻联系服务器if(newTemp >= 50 || newHeight >= 20){console.log('联系服务器')}})// 用watchEffect实现,不用明确的指出要监视变量// 1. 它会从监听函数中自动分析需要监视的数据 (而watch则需要指定需要监视的数据)// 2. 一上来就会执行1次函数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>

3.11. 标签的 ref 属性

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

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

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

用在普通DOM标签上

<template><div class="person"><!-- ref标记在普通DOM标签上 --><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()  // 使用ref来获取对应的节点, 其中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>

用在组件标签上(defineExpose)

defineExpose它属于宏函数,不需要引入

<!-- 父组件App.vue -->
<template><!-- ref标记在组件标签上 --><Person ref="ren"/><button @click="test">测试</button></template><script lang="ts" setup name="App">// 在setUp中不需要注册Person组件, 直接使用即可import Person from './components/Person.vue'import {ref} from 'vue'// 变量名需要与ref标记的值相同let ren = ref()function test(){// 需要子组件通过defineExpose暴露出来的属性或方法, 父组件才可以在这里访问到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.12 回顾TS

main.ts

// 引入createApp用于创建应用
import { createApp } from 'vue'// 引入App根组件
import App from './App.vue'createApp(App).mount('#app')

App.vue

<template><Person/>
</template><script lang="ts" setup name="App">import Person from '@/components/Person.vue'
</script>

index.ts

在src下创建types文件夹,并在这个文件夹中创建如下index.ts文件。

在其中定义接口和自定义泛型

// 定义一个接口,用于限制person对象的具体属性
export interface PersonInter {id: string,name: string,age: number,x?: number /* x是可选属性, 该类型中可以有该属性, 也可以无该属性 */
}// 一个自定义类型
// export type Persons = Array<PersonInter>
export type Persons = PersonInter[] // 与上面等价

Person.vue

注意把vetur这个插件给禁掉, 否则,老是有飘红。就开启本篇中上述的推荐的插件即可。

<template><div class="person"></div>
</template><script lang="ts" setup name="Person">// 引入接口 或 自定义类型 的时候, 需要在前面加上type; import { type PersonInter, type Persons } from '@/types'// 定义1个变量, 它要符合PersonInter接口let person: PersonInter = {id: 'a01', name: 'john', age:60}// 定义1个数组, 首先它是个数组, 并且里面元素类型都是符合PersonInter接口的(如果里面有属性名写错会有飘红提示)let personList: Array<PersonInter> = [{id: 'a01', name: 'john', age:60}]// 定义1个数组, 它符合 Persons 自定义类型(如果里面有属性名写错会有飘红提示)let personList2: Persons = [{id: 'a01', name: 'john', age:60}]</script><style scoped></style>

3.13 props(defineProps)

defineProps它属于宏函数,不需要引入

App.vue

<template><!-- Person子组件定义了list属性, 并且限定为Persons类型 --><Person :list="personList" />
</template><script lang="ts" setup name="App">import Person from '@/components/Person.vue'import {reactive} from 'vue'import {type Persons} from '@/types'let personList = reactive<Persons>([{ id: 'asudfysafd01', name: '张三', age: 18 },{ id: 'asudfysafd02', name: '李四', age: 20 },{ id: 'asudfysaf)d03', name: '王五', age: 22 }])</script>

index.ts

// 定义一个接口,用于限制person对象的具体属性
export interface PersonInter {id: string,name: string,age: number,x?: number /* x是可选属性, 该类型中可以有该属性, 也可以无该属性 */
}// 一个自定义类型
// export type Persons = Array<PersonInter>
export type Persons = PersonInter[] // 与上面等价

Person.vue

<template><div class="person"><ul><!-- 在模板中直接使用list, 不需要加props.list --><li v-for="p in list" :key="p.id">{{p.name}} -- {{p.age}}</li></ul></div>
</template><script lang="ts" setup name="Person">import {reactive, withDefaults} from 'vue'// 引入接口 或 自定义类型 的时候, 需要在前面加上type; import { type PersonInter, type Persons } from '@/types'// 不推荐的写法, 但可用let personList:Persons = reactive([{id: 'a01', name: 'john', age:60}])// 推荐的写法, 意为: personList2这个变量须符合 Persons 类型的规范let personList2 = reactive<Persons>([{id: 'a01', name: 'john', age:60}])// 推荐的写法, 意为: personList3这个变量须符合 PersonInter[] 类型的规范let personList3 = reactive<PersonInter[]>([{id: 'a01', name: 'john', age:60}])// 只接收// 定义接收父组件传过来的a属性, 并赋值给props以便于访问。并且defineProps只能使用1次/* let props = defineProps(['a', 'b'])// 在js代码中使用props.a来访问父组件传过来的a属性对应的值, 在模板中直接使用a来访问父组件传过来的a属性对应的值console.log(props.a); */    // 接收 + 限制类型 + 限制必要性// (list2可不传; list必须传, 并且必须是Persons类型的)/* let props = defineProps<{list:Persons, list2?:Persons}>()console.log(props.list);  */// 接收 + 限制类型 + 限制必要性 + 指定默认值// (list属性可不传, 如果没有传的话, 就是用下面默认定义的数据)const props = withDefaults(defineProps<{list?: Persons}>(),{list: () => [{id:'A001',name:'张三',age:18}]})console.log(props.list);</script><style scoped></style>

3.14 生命周期

  • 概念:Vue组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子

  • 规律:

    生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。

  • Vue2的生命周期

    创建阶段:beforeCreatecreated

    挂载阶段:beforeMountmounted

    更新阶段:beforeUpdateupdated

    销毁阶段:beforeDestroydestroyed

  • Vue3的生命周期

    创建阶段:setup(替代了之前vue2中的beforeCreate、created)

    挂载阶段:onBeforeMountonMounted

    更新阶段:onBeforeUpdateonUpdated

    卸载阶段:onBeforeUnmountonUnmounted(就对应vue2中的销毁阶段)

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

App.vue

<template><Person v-if="isShow"/>
</template><script lang="ts" setup name="App">import Person from './components/Person.vue'import {ref,onMounted} from 'vue'let isShow = ref(true)// 挂载完毕(先子组件挂载完毕, 再父挂载完毕)onMounted(()=>{console.log('父---挂载完毕')})</script>

Person.vue

<template><div class="person"><h2>当前求和为:{{ sum }}</h2><button @click="add">点我sum+1</button></div>
</template><script lang="ts" setup name="Person">import {ref,onBeforeMount, onMounted,onBeforeUpdate, onUpdated,onBeforeUnmount, onUnmounted } from 'vue'// 数据let sum = ref(0)// 方法function add(){sum.value += 1}// 创建(替代了之前vue2中的beforeCreate、created)console.log('创建')// 挂载前(这里面传入的函数由vue3帮我们调用, 这里只是将这个函数注册进去)onBeforeMount(()=>{// console.log('挂载前')})// 挂载完毕onMounted(()=>{console.log('子---挂载完毕')})// 更新前onBeforeUpdate(()=>{// console.log('更新前')})// 更新完毕onUpdated(()=>{// console.log('更新完毕')})// 卸载前onBeforeUnmount(()=>{// console.log('卸载前')})// 卸载完毕onUnmounted(()=>{// console.log('卸载完毕')})
</script>

3.15 自定义hooks

未使用hooks前

App.vue
<template><Person />
</template><script lang="ts" setup name="App">
import Person from './components/Person.vue'
</script>
Person.vue
<template><div class="person"><h2>当前求和为:{{ sum }},放大10倍后:{{ bigSum }}</h2><button @click="add">点我sum+1</button><hr><img v-for="(dog, index) in dogList" :src="dog" :key="index"><button @click="getDog">再来一只小狗</button></div>
</template><script lang="ts" setup name="Person">import { ref, reactive, onMounted, computed } from 'vue'import axios from 'axios'// ---- 求和// 数据let sum = ref(0)let bigSum = computed(() => {return sum.value * 10})// 方法function add() {sum.value += 1}// 钩子onMounted(() => {add()})// --- 发起请求获取图片// 数据let dogList = reactive(['https://images.dog.ceo/breeds/pembroke/n02113023_4373.jpg'])// 方法async function getDog() {try {let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')dogList.push(result.data.message)} catch (error) {alert(error)}}// 钩子onMounted(() => {getDog()})</script><style scoped></style>

使用hooks

vue3本身就推荐使用组合式api,但是如果各种功能都放到setup里面,显得就有点乱了,所以,使用hooks将单独的功能所使用的各种数据、方法等抽离出去,当需要某个功能时,再引入进来。

hooks中不仅可以定义数据,还可以使用声明周期钩子函数,还可以写计算属性。

App.vue
<template><Person />
</template><script lang="ts" setup name="App">
import Person from './components/Person.vue'
</script>
Person.vue
<template><div class="person"><h2>当前求和为:{{ sum }},放大10倍后:{{ bigSum }}</h2><button @click="add">点我sum+1</button><hr><img v-for="(dog,index) in dogList" :src="dog" :key="index"><br><button @click="getDog">再来一只小狗</button></div>
</template><script lang="ts" setup name="Person">import useSum from '@/hooks/useSum'import useDog from '@/hooks/useDog'// 调用函数获得数据const {sum,add,bigSum} = useSum()// 调用函数获得数据const {dogList,getDog} = useDog()</script><style scoped></style>
hooks/useSum.ts
import { ref ,onMounted,computed} from 'vue'// 暴露此函数(默认暴露)
export default function () {// 数据let sum = ref(0)// 这里面也可以写计算属性的哦let bigSum = computed(()=>{return sum.value * 10})// 方法function add() {sum.value += 1}// 钩子(hooks这里面也能写钩子的哦)onMounted(()=>{add()})// 给外部提供东西(要把东西放出去,让外界使用)return {sum,add,bigSum}
}
hooks/useDog.ts
import {reactive,onMounted} from 'vue'
import axios from 'axios'export default function (){// 数据let dogList = reactive(['https://images.dog.ceo/breeds/pembroke/n02113023_4373.jpg'])// 方法async function getDog(){try {let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')dogList.push(result.data.message)} catch (error) {alert(error)}}// 钩子(hooks这里面也能写钩子的哦)onMounted(()=>{getDog()})// 向外部提供东西return {dogList,getDog}
}

4.路由

4.1 路由的基本理解

在这里插入图片描述

当路由变化,路由器会监听到此变化,就会根据路由规则找到对应的组件,将这个组件展示在路由出口

在这里插入图片描述

4.2 基本切换效果

安装vue-router

# 现在查看package.json,发现安装的版本是【"vue-router": "^4.3.2"】
# 路由器是用来管理路由的, 并且当路径变化时, 根据路由规则将对应的组件 展示在路由出口处
npm install vue-router

配置路由规则router/index.ts

// 创建一个路由器,并暴露出去// 第一步:引入createRouter
import {createRouter,createWebHistory} from 'vue-router'
// 引入一个一个可能要呈现组件
import Home from '@/components/Home.vue'
import News from '@/components/News.vue'
import About from '@/components/About.vue'// 第二步:创建路由器
const router = createRouter({history:createWebHistory(), //路由器的工作模式(稍后讲解)routes:[ //一个一个的路由规则{path:'/home',component:Home},{path:'/news',component:News},{path:'/about',component:About},]
})// 暴露出去router
export default router

使用router路由管理器main.ts

// 引入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')

路由展示区App.vue

<template><div class="app"><h2 class="title">Vue路由测试</h2><!-- 导航区, 使用<router-link>标签来切换路由路径 --><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><!-- 展示区 , 使用<Router-view>标签作为路由出口 --><div class="main-content"><RouterView></RouterView></div></div>
</template><script lang="ts" setup name="App">import {RouterView,RouterLink} from 'vue-router'</script><style>/* App */.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;box-shadow: 0 0 2px;font-size: 30px;}.navigate {display: flex;justify-content: space-around;margin: 0 100px;}.navigate a {display: block;text-align: center;width: 90px;height: 40px;line-height: 40px;border-radius: 10px;background-color: gray;text-decoration: none;color: white;font-size: 18px;letter-spacing: 5px;}.navigate a.active {background-color: #64967E;color: #ffc268;font-weight: 900;text-shadow: 0 0 1px black;font-family: 微软雅黑;}.main-content {margin: 0 auto;margin-top: 30px;border-radius: 10px;width: 90%;height: 400px;border: 1px solid;}
</style>

路由组件

Home.vue
<template><div class="home"><img src="http://www.atguigu.com/images/index_new/logo.png" alt=""></div>
</template><script setup lang="ts" name="Home"></script><style scoped>.home {display: flex;justify-content: center;align-items: center;height: 100%;}
</style>
New.vue
<template><div class="news"><ul><li><a href="#">新闻001</a></li><li><a href="#">新闻002</a></li><li><a href="#">新闻003</a></li><li><a href="#">新闻004</a></li></ul></div>
</template><script setup lang="ts" name="News"></script><style scoped>
/* 新闻 */
.news {padding: 0 20px;display: flex;justify-content: space-between;height: 100%;
}
.news ul {margin-top: 30px;list-style: none;padding-left: 10px;
}
.news li>a {font-size: 18px;line-height: 40px;text-decoration: none;color: #64967E;text-shadow: 0 0 1px rgb(0, 84, 0);
}
.news-content {width: 70%;height: 90%;border: 1px solid;margin-top: 20px;border-radius: 10px;
}
</style>
About.vue
<template><div class="about"><h2>大家好,欢迎来到尚硅谷直播间</h2></div>
</template><script setup lang="ts" name="About"></script><style scoped>
.about {display: flex;justify-content: center;align-items: center;height: 100%;color: rgb(85, 84, 84);font-size: 18px;
}
</style>

路由切换效果图

在这里插入图片描述

4.3. 两个注意点

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

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

About.vue

当通过切换路由路径的方式而控制About.vue组件的显示和隐藏时,会分别执行onMounted 和 onUnmounted 中定义的函数

<template><div class="about"><h2>大家好,欢迎来到尚硅谷直播间</h2></div></template><script setup lang="ts" name="About">import {onMounted,onUnmounted} from 'vue'// 挂载时执行的函数onMounted(()=>{console.log('About组件挂载了')})// 卸载时执行的函数onUnmounted(()=>{console.log('About组件卸载了')})
</script><style scoped>.about {display: flex;justify-content: center;align-items: center;height: 100%;color: rgb(85, 84, 84);font-size: 18px;}
</style>

4.4. 路由器工作模式

  1. history模式

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

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

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

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

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

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

4.5. to的两种写法

<!-- 第一种:to的字符串写法 -->
<router-link active-class="active" to="/home">主页</router-link><!-- 第二种:to的对象写法 -->
<router-link active-class="active" :to="{path:'/home'}">Home</router-link>

4.6. 命名路由

作用:可以简化路由跳转及传参(后面就讲)。

给路由规则命名:

// 创建一个路由器,并暴露出去// 第一步:引入createRouter
import {createRouter,createWebHistory,createWebHashHistory} from 'vue-router'
// 引入一个一个可能要呈现组件
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'
import About from '@/pages/About.vue'// 第二步:创建路由器
const router = createRouter({history:createWebHashHistory(), //路由器的工作模式(稍后讲解)routes:[ //一个一个的路由规则{name:'zhuye',path:'/home',component:Home},{name:'xinwen',path:'/news',component:News},{name:'guanyu',path:'/about',component:About},]
})// 暴露出去router
export default router

跳转路由:

<template><div class="app"><Header/><!-- 导航区 --><div class="navigate"><!--简化前:需要写完整的路径(to的字符串写法) --><RouterLink to="/home" active-class="active">首页</RouterLink><!--简化后:直接通过路由规则中定义的路由的名字(route的name属性)跳转(to的对象写法配合name属性) --><RouterLink :to="{name:'xinwen'}" active-class="active">新闻</RouterLink><RouterLink :to="{path:'/about'}" active-class="active">关于</RouterLink></div><!-- 展示区 --><div class="main-content"><RouterView></RouterView></div></div>
</template><script lang="ts" setup name="App">import {RouterView,RouterLink} from 'vue-router'import Header from './components/Header.vue'</script>

4.7 嵌套路由

main.ts

// 引入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')

router/index.ts

当访问/news/detail时,先根据路由规则匹配到News组件,这个News组件应该要展示在App.vue中的路由出口处,然后匹配到子级路由找到Detail.vue,然后将Detail.vue组件展示在News组件的路由出口处。

// 创建一个路由器,并暴露出去// 第一步:引入createRouter
import {createRouter,createWebHistory,createWebHashHistory} from 'vue-router'
// 引入一个一个可能要呈现组件
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'
import About from '@/pages/About.vue'
import Detail from '@/pages/Detail.vue'// 第二步:创建路由器
const router = createRouter({history:createWebHistory(), //路由器的工作模式(稍后讲解)routes:[ //一个一个的路由规则{name:'zhuye',path:'/home',component:Home},{name:'xinwen',path:'/news',component:News,children:[{path:'detail',component:Detail}]},{name:'guanyu',path:'/about',component:About},]
})// 暴露出去router
export default router

App.vue

在App.vue中有1个路由出口(一级路由出口)

<template><div class="app"><Header/><!-- 导航区 --><div class="navigate"><RouterLink to="/home" active-class="active">首页</RouterLink><RouterLink :to="{name:'xinwen'}" active-class="active">新闻</RouterLink><RouterLink :to="{path:'/about'}" active-class="active">关于</RouterLink></div><!-- 展示区 --><div class="main-content"><RouterView></RouterView></div></div>
</template><script lang="ts" setup name="App">import {RouterView,RouterLink} from 'vue-router'import Header from './components/Header.vue'</script><style>/* App */.navigate {display: flex;justify-content: space-around;margin: 0 100px;}.navigate a {display: block;text-align: center;width: 90px;height: 40px;line-height: 40px;border-radius: 10px;background-color: gray;text-decoration: none;color: white;font-size: 18px;letter-spacing: 5px;}.navigate a.active {background-color: #64967E;color: #ffc268;font-weight: 900;text-shadow: 0 0 1px black;font-family: 微软雅黑;}.main-content {margin: 0 auto;margin-top: 30px;border-radius: 10px;width: 90%;height: 400px;border: 1px solid;}
</style>

News.vue

在News.vue中有1个子级路由出口

<template><div class="news"><!-- 导航区 --><ul><li v-for="news in newsList" :key="news.id"><RouterLink to="/news/detail">{{news.title}}</RouterLink></li></ul><!-- 展示区 --><div class="news-content"><RouterView></RouterView></div></div>
</template><script setup lang="ts" name="News">import {reactive} from 'vue'import {RouterView,RouterLink} from 'vue-router'const newsList = reactive([{id:'asfdtrfay01',title:'很好的抗癌食物',content:'西蓝花'},{id:'asfdtrfay02',title:'如何一夜暴富',content:'学IT'},{id:'asfdtrfay03',title:'震惊,万万没想到',content:'明天是周一'},{id:'asfdtrfay04',title:'好消息!好消息!',content:'快过年了'}])</script><style scoped>
/* 新闻 */
.news {padding: 0 20px;display: flex;justify-content: space-between;height: 100%;
}
.news ul {margin-top: 30px;list-style: none;padding-left: 10px;
}
.news li>a {font-size: 18px;line-height: 40px;text-decoration: none;color: #64967E;text-shadow: 0 0 1px rgb(0, 84, 0);
}
.news-content {width: 70%;height: 90%;border: 1px solid;margin-top: 20px;border-radius: 10px;
}
</style>

Detail.vue

<template><ul class="news-list"><li>编号:xxx</li><li>标题:xxx</li><li>内容:xxx</li></ul>
</template><script setup lang="ts" name="About"></script><style scoped>.news-list {list-style: none;padding-left: 20px;}.news-list>li {line-height: 30px;}
</style>

效果

可以看到在App.vue中有1个路由出口,在News.vue中也有1个路由出口

在这里插入图片描述

4.8 路由传参

query参数

1.定义路由规则

const router = createRouter({history:createWebHistory(), //路由器的工作模式(稍后讲解)routes:[ //一个一个的路由规则{name:'zhuye',path:'/home',component:Home},{name:'xinwen',path:'/news',component:News,children:[{name:'xiang',path:'detail',component:Detail}]},{name:'guanyu',path:'/about',component:About}]
})

2.传递参数

<!-- 跳转并携带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>

3.接收参数:

import {useRoute} from 'vue-router'
import {toRefs} from 'vue' const route = useRoute()// 从1个响应式对象直接解构属性(route是响应式对象),会丢失响应式
// 然后试图在模板中使用此query, 发现点击不同的新闻时数据没有变化, 因为在解构时这里已经丢失了响应式了
// 应该使用toRefs
// const {query} = route // 应该如下使用toRefs
// 然后在模板中使用, 发现点击不同的新闻时, 数据有了变化
const {query} = toRefs(route)// 打印query参数
console.log(route.query)

params参数

  1. 定义路由规则,并定义路由路径params参数
const router = createRouter({history:createWebHistory(), //路由器的工作模式(稍后讲解)routes:[ //一个一个的路由规则{name:'zhuye',path:'/home',component:Home},{name:'xinwen',path:'/news',component:News,children:[{name:'xiang',// 添加路径参数来占位path:'detail/:id/:title/:content?', // 这里加个问号的意思是可传可不传, 否则必须传component:Detail}]},{name:'guanyu',path:'/about',component:About}]
})
  1. 传递参数
<!-- 跳转并携带params参数(to的字符串写法) -->
<RouterLink :to="`/news/detail/001/新闻001/内容001`">{{news.title}}</RouterLink><!-- 跳转并携带params参数(to的对象写法) -->
<RouterLink :to="{name:'xiang', // 用name跳转, 注意这里不能用path, 并且下面的params的属性对应的值不能是对象或数组params:{id:news.id,title:news.title,content:news.title}}"
>{{news.title}}
</RouterLink>
  1. 接收参数:
// useRoute是hooks钩子
import {useRoute} from 'vue-router'const route = useRoute()// 从1个响应式对象直接解构属性(route是响应式对象),会丢失响应式
// 然后试图在模板中使用此query, 发现点击不同的新闻时数据没有变化, 因为在解构时这里已经丢失了响应式了
// 应该使用toRefs
// const {params} = route // 应该如下使用toRefs
// 然后在模板中使用, 发现点击不同的新闻时, 数据有了变化
const {params} = toRefs(route)// 打印params参数
console.log(route.params)

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

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

4.9 路由的props配置

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

{name:'xiang',path:'detail/:id/:title/:content',component:Detail,// 第一种写法:将路由收到的【所有params参数】作为props传给路由组件// props的布尔值写法,作用:把收到了每一组params参数,作为props传给Detail组件,// (类似于: <Detail :id='xx' :title='xx' :content='xx' />)// 这样在Detail组件中通过defineProps(['id','title','content'])声明属性, // 然后在模板中直接使用id,title,content就可以访问这些属性了// props:true// 第二种写法:函数写法,可以自己决定将什么作为props给路由组件// props的函数写法,作用:把返回的对象中每一组key-value作为props传给Detail组件// 这里的形参可以不叫route, 换成其它任何名字都代表路由对象// 这样在Detail组件中通过defineProps(['k'])声明属性, // 然后在模板中直接使用k就可以访问k属性对应的值了, route.query中的属性也是一样props(route){return {...route.query, k:'v'}}// 第三种写法:对象写法,可以自己决定将什么作为props给路由组件// props的对象写法,作用:把对象中的每一组key-value作为props传给Detail组件// props:{a:1,b:2,c:3}, // 以上写法请注意, 都是在指定Detail作为路由组件展示在路由出口时, 给该【路由组件】传递的props, // 注意与直接使用<Detail/>标签的形式的【一般组件】区别开来
}

4.10 replace属性

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

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

    • push是追加历史记录(默认值)。
    • replace是替换当前记录。
  3. 开启replace模式:

    <RouterLink replace to='/news/detail/1'>News</RouterLink>
    

示例

<template><div class="app"><Header/><!-- 导航区 --><div class="navigate"><RouterLink to="/home" active-class="active">首页</RouterLink><RouterLink replace :to="{name:'xinwen'}" active-class="active">新闻</RouterLink><RouterLink replace :to="{path:'/about'}" active-class="active">关于</RouterLink></div><!-- 展示区 --><div class="main-content"><RouterView></RouterView></div></div>
</template>

4.11 编程式导航

路由组件的两个重要的属性:$route$router变成了两个hooks

import {useRoute,useRouter} from 'vue-router'const route = useRoute()
const router = useRouter()console.log(route.query)
console.log(route.parmas)// <RouterLink to=''/>标签中的to属性能怎么写, 那么router.push(..)中的参数就能怎么写
console.log(router.push) 
console.log(router.replace)

示例

<template><div class="news"><!-- 导航区 --><ul><li v-for="news in newsList" :key="news.id"><button @click="showNewsDetail(news)">查看新闻</button><RouterLink :to="{name:'xiang',query:{id:news.id,title:news.title,content:news.content}}">{{news.title}}</RouterLink></li></ul><!-- 展示区 --><div class="news-content"><RouterView></RouterView></div></div>
</template><script setup lang="ts" name="News">import {reactive} from 'vue'import {RouterView,RouterLink,useRouter} from 'vue-router'const newsList = reactive([{id:'asfdtrfay01',title:'很好的抗癌食物',content:'西蓝花'},{id:'asfdtrfay02',title:'如何一夜暴富',content:'学IT'},{id:'asfdtrfay03',title:'震惊,万万没想到',content:'明天是周一'},{id:'asfdtrfay04',title:'好消息!好消息!',content:'快过年了'}])const router = useRouter()interface NewsInter {id:string,title:string,content:string}function showNewsDetail(news:NewsInter){router.replace({name:'xiang',query:{id:news.id,title:news.title,content:news.content}})}</script><style scoped>
/* 新闻 */
.news {padding: 0 20px;display: flex;justify-content: space-between;height: 100%;
}
.news ul {margin-top: 30px;/* list-style: none; */padding-left: 10px;
}
.news li::marker {color: #64967E;
}
.news li>a {font-size: 18px;line-height: 40px;text-decoration: none;color: #64967E;text-shadow: 0 0 1px rgb(0, 84, 0);
}
.news-content {width: 70%;height: 90%;border: 1px solid;margin-top: 20px;border-radius: 10px;
}
</style>

4.12 重定向

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

  2. 具体编码:

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

示例

// 创建一个路由器,并暴露出去// 第一步:引入createRouter
import {createRouter,createWebHistory,createWebHashHistory} from 'vue-router'// 引入一个一个可能要呈现组件
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'
import About from '@/pages/About.vue'
import Detail from '@/pages/Detail.vue'// 第二步:创建路由器
const router = createRouter({history:createWebHistory(), //路由器的工作模式(稍后讲解)routes:[ //一个一个的路由规则{name:'zhuye',path:'/home',component:Home},{name:'xinwen',path:'/news',component:News,children:[{name:'xiang',path:'detail',component:Detail,props(route){return route.query}}]},{name:'guanyu',path:'/about',component:About},{path:'/',// 使用重定向, 当用户访问/时, 跳转到/home// 即: 让指定的路径重新定位到另一个路径redirect:'/home'}]
})// 暴露出去router
export default router

5. pinia

5.1 准备一个效果

pinia_example

main.ts

// 引入createApp用于创建应用
import {createApp} from 'vue'// 引入App根组件
import App from './App.vue'// 创建一个应用
const app = createApp(App)// 挂载整个应用到app容器中
app.mount('#app')

App.vue

<template><Count/><br><LoveTalk/>
</template><script setup lang="ts" name="App">import Count from './components/Count.vue'import LoveTalk from './components/LoveTalk.vue'
</script>

Count.vue

<template><div class="count"><h2>当前求和为:{{ sum }}</h2><!-- 如果不写.number, 那么绑定所获取的值是字符串 --><!-- 当然也可以这样使用v-bind来绑定, 如: <option :value="1">1</option> --><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref } from "vue";// 数据let sum = ref(1) // 当前求和let n = ref(1) // 用户选择的数字// 方法function add(){sum.value += n.value}function minus(){sum.value -= n.value}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

LoveTalk.vue

<template><div class="talk"><button @click="getLoveTalk">获取一句土味情话</button><ul><li v-for="talk in talkList" :key="talk.id">{{talk.title}}</li></ul></div>
</template><script setup lang="ts" name="LoveTalk">import {reactive} from 'vue'import axios from "axios";import {nanoid} from 'nanoid'// 数据let talkList = reactive([{id:'ftrfasdf01',title:'今天你有点怪,哪里怪?怪好看的!'},{id:'ftrfasdf02',title:'草莓、蓝莓、蔓越莓,今天想我了没?'},{id:'ftrfasdf03',title:'心里给你留了一块地,我的死心塌地'}])// 方法async function getLoveTalk(){// 发请求,下面这行的写法是:连续解构赋值+重命名let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')// 把请求回来的字符串,包装成一个对象let obj = {id:nanoid(),title}// 放到数组中talkList.unshift(obj)}
</script><style scoped>.talk {background-color: orange;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}
</style>

5.2 搭建 pinia 环境

使用步骤

第一步:npm install pinia(此处安装的版本是:“pinia”: “^2.1.7”,)

第二步:操作src/main.ts

import { createApp } from 'vue'import App from './App.vue'/* 引入createPinia,用于创建pinia */
import { createPinia } from 'pinia'/* 创建pinia */
const pinia = createPinia()const app = createApp(App)/* 使用插件 */
app.use(pinia)app.mount('#app')

此时开发者工具中已经有了pinia选项

5.3 存储+读取数据

  1. Store是一个保存:状态业务逻辑 的实体,每个组件都可以读取写入它。

  2. 它有三个概念:stategetteraction,相当于组件中的: datacomputedmethods

store/count.ts

import { defineStore } from 'pinia'// defineStore返回的值的命名 格式为: use{文件名}Store
export const useCountStore = defineStore('count', /* 建议这里的名字与文件名保持一直, 首字母小写 */{// 真正存储数据的地方state() { // 这个只能写成1个函数return {sum: 6}}
})

store/loveTalk.ts

import {defineStore} from 'pinia'export const useTalkStore = defineStore('talk',{// 真正存储数据的地方state(){return {talkList:[{id:'ftrfasdf01',title:'今天你有点怪,哪里怪?怪好看的!'},{id:'ftrfasdf02',title:'草莓、蓝莓、蔓越莓,今天想我了没?'},{id:'ftrfasdf03',title:'心里给你留了一块地,我的死心塌地'}]}}
})

Count.vue

<template><div class="count"><!-- 直接使用countStore --><h2>当前求和为:{{ countStore.sum }}</h2><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref, reactive } from "vue";import { useCountStore } from '@/store/count'const countStore = useCountStore()// 以下两种方式都可以拿到state中的数据// console.log('@@@',countStore.sum) // 注意: 这里后面不要写.value哦, 因为会自动拆包// console.log('@@@',countStore.$state.sum) // 也可以通过$state拿到sum/*   let obj = reactive({a:1,b:2,c:ref(3)})let x = ref(9)console.log(obj.a)console.log(obj.b)console.log(obj.c) // 注意, 这里最后面就不用.value了*/// 数据let n = ref(1) // 用户选择的数字// 方法function add() {}function minus() {}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

LoveTalk.vue

<template><div class="talk"><button @click="getLoveTalk">获取一句土味情话</button><ul><li v-for="talk in talkStore.talkList" :key="talk.id">{{talk.title}}</li></ul></div>
</template><script setup lang="ts" name="LoveTalk">import {reactive} from 'vue'import axios from "axios";import {nanoid} from 'nanoid'import {useTalkStore} from '@/store/loveTalk'const talkStore = useTalkStore()// 方法async function getLoveTalk(){// 发请求,下面这行的写法是:连续解构赋值+重命名// let {data:{content:title}} = await //                        axios.get('https://api.uomg.com/api/rand.qinghua?format=json')// 把请求回来的字符串,包装成一个对象// let obj = {id:nanoid(),title}// 放到数组中// talkList.unshift(obj)}
</script><style scoped>.talk {background-color: orange;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}
</style>

App.vue

<template><Count/><br><LoveTalk/>
</template><script setup lang="ts" name="App">import Count from './components/Count.vue'import LoveTalk from './components/LoveTalk.vue'
</script>

main.ts

import {createApp} from 'vue'
import App from './App.vue'
// 第一步:引入pinia
import {createPinia} from 'pinia'const app = createApp(App)
// 第二步:创建pinia
const pinia = createPinia()
// 第三步:安装pinia
app.use(pinia)
app.mount('#app')

5.4 修改数据(三种方式)

第一种方式

count.ts
import {defineStore} from 'pinia'export const useCountStore = defineStore('count',{// 真正存储数据的地方state(){return {sum:6,school:'atguigu',address:'宏福科技园'}}
})
Count.vue
<template><div class="count"><h2>当前求和为:{{ countStore.sum }}</h2><button @click="add"></button></div>
</template><script setup lang="ts" name="Count">import { ref, reactive } from "vue";// 引入useCountStoreimport { useCountStore } from '@/store/count'// 使用useCountStore,得到一个专门保存count相关的storeconst countStore = useCountStore()// 数据let n = ref(1) // 用户选择的数字// 方法function add() {// 第一种修改方式, 直接拿到countStore去改, 注意: 这和vuex不同, vuex是不能直接修改的countStore.sum += 1countStore.school = '尚硅谷'countStore.address = '北京'}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

第二种方式

count.ts
import {defineStore} from 'pinia'export const useCountStore = defineStore('count',{// 真正存储数据的地方state(){return {sum:6,school:'atguigu',address:'宏福科技园'}}
})
Count.vue
<template><div class="count"><h2>当前求和为:{{ countStore.sum }}</h2><h3>欢迎来到:{{ countStore.school }},坐落于:{{ countStore.address }}</h3><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref,reactive } from "vue";// 引入useCountStoreimport {useCountStore} from '@/store/count'// 使用useCountStore,得到一个专门保存count相关的storeconst countStore = useCountStore()// 数据let n = ref(1) // 用户选择的数字// 方法function add(){// 第二种修改方式(如果很多数据都要统一一次性发生变化,推荐使用$patch)countStore.$patch({sum:888,school:'尚硅谷',address:'北京'})}function minus(){}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

第三种方式

count.ts
import {defineStore} from 'pinia'export const useCountStore = defineStore('count',{// actions里面放置的是一个一个的方法,用于响应组件中的“动作”// (使用actions的意义在于可以将对组件共享数据统一操作的逻辑抽取放到这里)actions:{increment(value){ // value是调用方传过来的值console.log('increment被调用了',value)if( this.sum < 10){// 修改数据(this是当前的store)this.sum += value}}},// 真正存储数据的地方state(){return {sum:6,school:'atguigu',address:'宏福科技园'}}
})
Count.vue
<template><div class="count"><h2>当前求和为:{{ countStore.sum }}</h2><h3>欢迎来到:{{ countStore.school }},坐落于:{{ countStore.address }}</h3><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref,reactive } from "vue";// 引入useCountStoreimport {useCountStore} from '@/store/count'// 使用useCountStore,得到一个专门保存count相关的storeconst countStore = useCountStore()// 数据let n = ref(1) // 用户选择的数字// 方法function add(){// 第三种修改方式(直接调用count.ts中定义的actions方法)const result = countStore.increment(n.value)console.log('result', result); // result undefined}function minus(){}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

5.5 storeToRefs用法

  • 借助storeToRefsstore中的数据转为ref对象,方便在模板中使用。
  • 注意:pinia提供的storeToRefs只会将数据做转换,而VuetoRefs会转换store中数据(虽然能实现功能,单不建议使用哦)。

LoveTalk.ts

import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'export const useTalkStore = defineStore('talk',{actions:{async getATalk(){// 发请求,下面这行的写法是:连续解构赋值+重命名let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')// 把请求回来的字符串,包装成一个对象let obj = {id:nanoid(),title}// 放到数组中this.talkList.unshift(obj)}},// 真正存储数据的地方state(){return {talkList:[{id:'ftrfasdf01',title:'今天你有点怪,哪里怪?怪好看的!'},{id:'ftrfasdf02',title:'草莓、蓝莓、蔓越莓,今天想我了没?'},{id:'ftrfasdf03',title:'心里给你留了一块地,我的死心塌地'}]}}
})

LoveTask.vue

<template><div class="talk"><button @click="getLoveTalk">获取一句土味情话</button><ul><li v-for="talk in talkList" :key="talk.id">{{talk.title}}</li></ul></div>
</template><script setup lang="ts" name="LoveTalk">import {useTalkStore} from '@/store/loveTalk'import { storeToRefs } from "pinia";const talkStore = useTalkStore()// 这里如果直接这样解构写: const {talkList} = taskStore; 那么此时这里的talkList就已经丢失了响应式// 这里虽然也可以写:  const {talkList} = toRefs(taskStore); 虽然可以维持talkList的响应式, 但代价过大,//                 (toRefs会把talkStore中的全部数据包括函数,state啥的都给包了一遍)// 所以最好使用storeToRefs, 因为storeToRefs只会关注sotre中数据,不会对方法进行ref包裹const {talkList} = storeToRefs(talkStore)// 方法function getLoveTalk(){talkStore.getATalk()}
</script><style scoped>.talk {background-color: orange;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}
</style>

count.ts

import {defineStore} from 'pinia'export const useCountStore = defineStore('count',{// actions里面放置的是一个一个的方法,用于响应组件中的“动作”actions:{increment(value:number){console.log('increment被调用了',value)if( this.sum < 10){// 修改数据(this是当前的store)this.sum += value}}},// 真正存储数据的地方state(){return {sum:1,school:'atguigu',address:'宏福科技园'}}
})

Count.vue

<template><div class="count"><h2>当前求和为:{{ sum }}</h2><h3>欢迎来到:{{ school }},坐落于:{{ address }}</h3><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref,reactive,toRefs } from "vue";import {storeToRefs} from 'pinia'// 引入useCountStoreimport {useCountStore} from '@/store/count'// 使用useCountStore,得到一个专门保存count相关的storeconst countStore = useCountStore()// storeToRefs只会关注sotre中数据,不会对方法进行ref包裹const {sum,school,address} = storeToRefs(countStore)// console.log('!!!!!',storeToRefs(countStore))// 数据let n = ref(1) // 用户选择的数字// 方法function add(){countStore.increment(n.value)}function minus(){countStore.sum -= n.value}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

5.6 getters用法

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

count.ts

import {defineStore} from 'pinia'export const useCountStore = defineStore('count',{// actions里面放置的是一个一个的方法,用于响应组件中的“动作”actions:{increment(value:number){console.log('increment被调用了',value)if( this.sum < 10){// 修改数据(this是当前的store)this.sum += value}}},// 真正存储数据的地方state(){return {sum:3,school:'atguigu',address:'宏福科技园'}},getters:{bigSum:state => state.sum * 10,upperSchool():string{return this.school.toUpperCase()}}
})

Count.vue

<template><div class="count"><h2>当前求和为:{{ sum }},放大10倍后:{{ bigSum }}</h2><h3>欢迎来到:{{ school }},坐落于:{{ address }},大写:{{ upperSchool }}</h3><select v-model.number="n"><option value="1">1</option><option value="2">2</option><option value="3">3</option></select><button @click="add"></button><button @click="minus"></button></div>
</template><script setup lang="ts" name="Count">import { ref,reactive,toRefs } from "vue";import {storeToRefs} from 'pinia'// 引入useCountStoreimport {useCountStore} from '@/store/count'// 使用useCountStore,得到一个专门保存count相关的storeconst countStore = useCountStore()// storeToRefs只会关注sotre中数据,不会对方法进行ref包裹, 并且同时维持解构属性结果的响应式// (可以直接解构出state和getters中定义的数据)const {sum,school,address,bigSum,upperSchool} = storeToRefs(countStore)// console.log('!!!!!',storeToRefs(countStore))// 数据let n = ref(1) // 用户选择的数字// 方法function add(){countStore.increment(n.value)}function minus(){countStore.sum -= n.value}
</script><style scoped>.count {background-color: skyblue;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}select,button {margin: 0 5px;height: 25px;}
</style>

5.7 $subscribe的使用

通过 store 的 $subscribe() 方法侦听 state 及其变化

loveTalk.ts

import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'export const useTalkStore = defineStore('talk',{actions:{async getATalk(){// 发请求,下面这行的写法是:连续解构赋值+重命名let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')// 把请求回来的字符串,包装成一个对象let obj = {id:nanoid(),title}// 放到数组中this.talkList.unshift(obj)}},// 真正存储数据的地方state(){return {talkList:JSON.parse(localStorage.getItem('talkList') as string) || []}}
})

LoveTalk.vue

<template><div class="talk"><button @click="getLoveTalk">获取一句土味情话</button><ul><li v-for="talk in talkList" :key="talk.id">{{ talk.title }}</li></ul></div>
</template><script setup lang="ts" name="LoveTalk">import { useTalkStore } from '@/store/loveTalk'import { storeToRefs } from "pinia";const talkStore = useTalkStore()const { talkList } = storeToRefs(talkStore)talkStore.$subscribe((mutate, state) => {// 注意: 箭头函数中没有thisconsole.log('talkStore里面保存的数据发生了变化', mutate, state)// 实现页面刷新时, 这里的talkList不丢失, 因为在loveTalk.ts中会取localStorage中读取talkList数据localStorage.setItem('talkList', JSON.stringify(state.talkList))})// 方法function getLoveTalk() {talkStore.getATalk()}
</script><style scoped>.talk {background-color: orange;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}
</style>

5.8 store组合式写法

loveTalk.js

import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'/* export const useTalkStore = defineStore('talk',{actions:{async getATalk(){// 发请求,下面这行的写法是:连续解构赋值+重命名let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')// 把请求回来的字符串,包装成一个对象let obj = {id:nanoid(),title}// 放到数组中this.talkList.unshift(obj)}},// 真正存储数据的地方state(){return {talkList:JSON.parse(localStorage.getItem('talkList') as string) || []}}
})*/import {reactive} from 'vue'
export const useTalkStore = defineStore('talk',()=>{// talkList就是stateconst talkList = reactive(JSON.parse(localStorage.getItem('talkList') as string) || [])// getATalk函数相当于actionasync 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}
})

LoveTalk.vue

<template><div class="talk"><button @click="getLoveTalk">获取一句土味情话</button><ul><li v-for="talk in talkList" :key="talk.id">{{talk.title}}</li></ul></div>
</template><script setup lang="ts" name="LoveTalk">import {useTalkStore} from '@/store/loveTalk'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><style scoped>.talk {background-color: orange;padding: 10px;border-radius: 10px;box-shadow: 0 0 10px;}
</style>

6. 组件通信

6.1 props

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

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

这种不适合父子孙中父给孙组件传递数据,或者兄弟组件也可以找到同1个父组件来实现兄弟组件通信

Father.vue

<template><div class="father"><h3>父组件</h3><h4>汽车:{{ car }}</h4><h4 v-show="toy">子给的玩具:{{ toy }}</h4><Child :car="car" :sendToy="getToy" /></div>
</template><script setup lang="ts" name="Father">import Child from './Child.vue'import { ref } from 'vue'// 数据let car = ref('奔驰')let toy = ref('')// 方法function getToy(value: string) {toy.value = value}</script><style scoped>.father {background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}
</style>

Child.vue

<template><div class="child"><h3>子组件</h3><h4>玩具:{{ toy }}</h4><h4>父给的车:{{ car }}</h4><button @click="sendToy(toy)">把玩具给父亲</button></div>
</template><script setup lang="ts" name="Child">import { ref } from 'vue'// 数据let toy = ref('奥特曼')// 声明接收propsdefineProps(['car', 'sendToy'])</script><style scoped>.child {background-color: skyblue;padding: 10px;box-shadow: 0 0 10px black;border-radius: 10px;}
</style>

6.2 自定义事件

Father.vue

<template><div class="father"><h3>父组件</h3><h4 v-show="toy">子给的玩具:{{ toy }}</h4><!-- 给子组件Child绑定事件 --><Child @send-toy="saveToy" /></div>
</template><script setup lang="ts" name="Father">import Child from './Child.vue'import { ref } from "vue";// 数据let toy = ref('')// 用于保存传递过来的玩具function saveToy(value: string,e:any) {console.log('saveToy', value, e)toy.value = value}</script><style scoped>.father {background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}.father button {margin-right: 5px;}
</style>

Child.vue

<template><div class="child"><h3>子组件</h3><h4>玩具:{{ toy }}</h4><!-- 在模板中可以使用$event来代表事件对象 --><button @click="emit('send-toy', toy, $event)">测试</button></div>
</template><script setup lang="ts" name="Child">import { ref } from "vue";// 数据let toy = ref('奥特曼')// 声明事件const emit = defineEmits(['send-toy'])</script><style scoped>.child {margin-top: 10px;background-color: rgb(76, 209, 76);padding: 10px;box-shadow: 0 0 10px black;border-radius: 10px;}
</style>

6.3 mitt

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

安装mittnpm install mitt,版本是:“mitt”: “^3.0.1”

emitter.ts

// 引入mitt
import mitt from 'mitt'// 调用mitt得到emitter,emitter能:绑定事件、触发事件
const emitter = mitt()/* 
// 绑定事件
emitter.on('test1',()=>{console.log('test1被调用了')
})
emitter.on('test2',()=>{console.log('test2被调用了')
})// 触发事件
setInterval(() => {emitter.emit('test1')emitter.emit('test2')
}, 1000);setTimeout(() => {// emitter.off('test1')// emitter.off('test2')emitter.all.clear()
}, 3000); 
*/// 暴露emitter
export default emitter

Father.vue

<template><div class="father"><h3>父组件</h3><Child1/><Child2/></div>
</template><script setup lang="ts" name="Father">import Child1 from './Child1.vue'import Child2 from './Child2.vue'
</script><style scoped>.father{background-color:rgb(165, 164, 164);padding: 20px;border-radius: 10px;}.father button{margin-left: 5px;}
</style>

Child1.vue

<template><div class="child1"><h3>子组件1</h3><h4>玩具:{{ toy }}</h4><button @click="emitter.emit('send-toy',toy)">玩具给弟弟</button></div>
</template><script setup lang="ts" name="Child1">import {ref} from 'vue'import emitter from '@/utils/emitter';// 数据let toy = ref('奥特曼')
</script><style scoped>.child1{margin-top: 50px;background-color: skyblue;padding: 10px;box-shadow: 0 0 10px black;border-radius: 10px;}.child1 button{margin-right: 10px;}
</style>

Child2.vue

<template><div class="child2"><h3>子组件2</h3><h4>电脑:{{ computer }}</h4><h4>哥哥给的玩具:{{ toy }}</h4></div>
</template><script setup lang="ts" name="Child2">import { ref, onUnmounted } from 'vue'import emitter from '@/utils/emitter';// 数据let computer = ref('联想')let toy = ref('')// 给emitter绑定send-toy事件emitter.on('send-toy', (value: any) => {toy.value = value})// 在组件卸载时解绑send-toy事件onUnmounted(() => {emitter.off('send-toy')})
</script><style scoped>.child2 {margin-top: 50px;background-color: orange;padding: 10px;box-shadow: 0 0 10px black;border-radius: 10px;}
</style>

6.4 v-model

Father.vue

<template><div class="father"><h3>父组件</h3><h4>{{ username }}</h4><h4>{{ password }}</h4><!-- v-model用在html标签上 --><!-- <input type="text" v-model="username"> --><!-- <input type="text" :value="username" @input="username = (<HTMLInputElement>$event.target).value"> --><!-- v-model用在组件标签上 --><!-- <AtguiguInput v-model="username"/> --><!-- 上面这行等价于下面这行 --><!-- $event到底是啥? 啥时候能.target对于原生事件, $event就是事件对象 ===> 能.target对于自定义事件, $event就是触发事件时, 所传递的数据 ===> 不能.target--><!-- <AtguiguInput :modelValue="username" @update:modelValue="username = $event"/> --><!-- 修改modelValue --><AtguiguInput v-model:ming="username" v-model:mima="password"/></div>
</template><script setup lang="ts" name="Father">import { ref } from "vue";import AtguiguInput from './AtguiguInput.vue'// 数据let username = ref('zhansgan')let password = ref('123456')
</script><style scoped>.father {padding: 20px;background-color: rgb(165, 164, 164);border-radius: 10px;}
</style>

AtguiguInput.vue

<template><input type="text" :value="ming"@input="emit('update:ming',(<HTMLInputElement>$event.target).value)"><br><input type="text" :value="mima"@input="emit('update:mima',(<HTMLInputElement>$event.target).value)">
</template><script setup lang="ts" name="AtguiguInput">defineProps(['ming','mima'])const emit = defineEmits(['update:ming','update:mima'])</script><style scoped>input {border: 2px solid black;background-image: linear-gradient(45deg,red,yellow,green);height: 30px;font-size: 20px;color: white;}
</style>

6.5 $attrs

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

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

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

    (就是父组件给子组件通过标签的属性方式传递给子组件,子组件使用props的方式只接收了部分属性,其它没有接收的属性可以通过子组件的$attrs来访问)

Father.vue

<template><div class="father"><h3>父组件</h3><h4>a:{{a}}</h4><h4>b:{{b}}</h4><h4>c:{{c}}</h4><h4>d:{{d}}</h4><!-- v-bind="{x:100,y:200}就等价:  :x=100 :y=200 --><Child :a="a" :b="b" :c="c" :d="d" :e="e" 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)let e = ref(5)function updateA(value:number){a.value += value}
</script><style scoped>.father{background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}
</style>

Child.vue

<template><div class="child"><h3>子组件</h3><h4>{{ e }}</h4><!-- Father组件传给Child组件的属性,但是Father组件没有使用props接收的属性,就存在$attrs中 --><h4>{{ $attrs }}</h4><!-- Father组件传给Child组件的属性,但是Father组件没有使用props接收的属性,全部传递给GrandChild组件--><GrandChild v-bind="$attrs"/></div>
</template><script setup lang="ts" name="Child">import GrandChild from './GrandChild.vue'defineProps(['e'])
</script><style scoped>.child{margin-top: 20px;background-color: skyblue;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

GrandChild.vue

<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><!-- Father组件通过Child组件的v-bind="$attr"将函数传给GrandChild组件,这样GrandChild组件就可以通过此函数传递数据给Father组件了 --><button @click="updateA(6)">点我将爷爷那的a更新</button></div>
</template><script setup lang="ts" name="GrandChild">// 接收Father组件传递过来并由Child组件通过v-bind="$attr"中转过来的属性defineProps(['a','b','c','d','x','y','updateA'])
</script><style scoped>.grand-child{margin-top: 20px;background-color: orange;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

6.6 r e f s 、 refs、 refsparent、proxy

  1. 概述:

    • $refs用于 :父→子。
    • $parent用于:子→父。
  2. 原理如下:

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

Father.vue

<template><div class="father"><h3>父组件</h3><h4>房产:{{ house }}</h4><button @click="changeToy">修改Child1的玩具</button><button @click="changeComputer">修改Child2的电脑</button><!-- 在模板中可以直接使用$refs --><button @click="getAllChild($refs)">让所有孩子的书变多</button><button @click="getAllChild2()">让c1孩子的书变多2</button><button @click="getAllChild3()">让c1孩子的书变多3</button><Child1 ref="c1"/><Child2 ref="c2"/></div>
</template><script setup lang="ts" name="Father">import Child1 from './Child1.vue'import Child2 from './Child2.vue'import { ref,reactive } from "vue";import { getCurrentInstance } from 'vue';const proxy = getCurrentInstance()let c1 = ref()let c2 = ref()// 注意点:当访问obj.c的时候,底层会自动读取value属性,因为c是在obj这个响应式对象中的/* let obj = reactive({a:1,b:2,c:ref(3)})let x = ref(4)console.log(obj.a)console.log(obj.b)console.log(obj.c)console.log(x) */// 数据let house = ref(4)// 方法function changeToy(){// 必须要Child1组件通过defineExpose将toy属性暴露出来, 这样Father组件才能访问到并修改此toy属性c1.value.toy = '小猪佩奇'}function changeComputer(){c2.value.computer = '华为'}function getAllChild(refs:{[key:string]:any}){console.log(refs)for (let key in refs){// 这里不需要refs[key].value.book += 3, 是因为refs本身就是个响应式对象, 它会自动解包refs[key].book += 3}}function getAllChild2(){// 使用getCurrentInstance来访问感觉更加方便console.log(proxy);console.log(proxy.refs);   // {c1: Proxy(Object), c2: Proxy(Object)}console.log(proxy.parent); // {uid: 0, vnode: {…}, type: {…}, parent: null, //                                 appContext: {…}, …}console.log(proxy.attrs);  // {__vInternal: 1}proxy.refs.c1.book += 2}function getAllChild3(){// console.log($refs);      // 注意, 在vue3的setup语法糖中不能直接访问到$refs// console.log(this.$refs); // 注意, 在vue3的setup语法糖中不能直接访问到$refsconsole.log(this.proxy);    // 这个等价于getCurrentInstance()返回的值console.log(this.proxy == proxy);    // trueconsole.log(this.c1);       // 这里可以直接访问到ref='c1'标识的组件this.c1.book += 2}// 向外部提供数据defineExpose({house})</script><style scoped>.father {background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}.father button {margin-bottom: 10px;margin-left: 10px;}
</style>

Child1.vue

<template><div class="child1"><h3>子组件1</h3><h4>玩具:{{ toy }}</h4><h4>书籍:{{ book }} 本</h4><button @click="minusHouse($parent)">干掉父亲的一套房产</button><button @click="minusHouse2()">干掉父亲的一套房产2</button><button @click="minusHouse3()">干掉父亲的一套房产3</button></div></template><script setup lang="ts" name="Child1">import { ref,getCurrentInstance } from "vue";const proxy = getCurrentInstance()// 数据let toy = ref('奥特曼')let book = ref(3)// 方法function minusHouse(parent:any){// 需要Father组件通过defineExpose将house属性暴露出来, 这里才可以访问到parent.house -= 1}function minusHouse2(){// 需要Father组件通过defineExpose将house属性暴露出来, 这里才可以访问到console.log(proxy);console.log(proxy.parent);console.log(proxy.parent.exposed);proxy.parent.exposed.house.value -= 1}function minusHouse3(){// 需要Father组件通过defineExpose将house属性暴露出来, 这里才可以访问到console.log(this); // Proxy(Object) {proxy: {…}, minusHouse: ƒ, minusHouse2: ƒ, …console.log(this.parent); // undefinedconsole.log(this.proxy);  // 这个等价于getCurrentInstance()返回的值console.log(this.proxy == proxy); // true}// 把数据交给外部defineExpose({toy,book})</script><style scoped>.child1{margin-top: 20px;background-color: skyblue;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

Child2.vue

<template><div class="child2"><h3>子组件2</h3><h4>电脑:{{ computer }}</h4><h4>书籍:{{ book }} 本</h4></div>
</template><script setup lang="ts" name="Child2">import { ref } from "vue";// 数据let computer = ref('联想')let book = ref(6)// 把数据交给外部defineExpose({ computer, book })</script><style scoped>.child2 {margin-top: 20px;background-color: orange;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

6.7 provide、inject

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

  2. 具体使用:

    • 在祖先组件中通过provide配置向后代组件提供数据
    • 在后代组件中通过inject配置来声明接收数据

Father.vue

<template><div class="father"><h3>父组件</h3><h4>银子:{{ money }}万元</h4><h4>车子:一辆{{car.brand}}车,价值{{car.price}}万元</h4><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})function updateMoney(value:number){money.value -= value}// 向后代提供数据provide('moneyContext',{money,updateMoney})// (注意数据的后面不要.value, 否则不具备响应式)provide('car',car)</script><style scoped>.father {background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}
</style>

Child.vue

<template><div class="child"><h3>我是子组件</h3><GrandChild/></div>
</template><script setup lang="ts" name="Child">import GrandChild from './GrandChild.vue'
</script><style scoped>.child {margin-top: 20px;background-color: skyblue;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

GrandChild.vue

<template><div class="grand-child"><h3>我是孙组件</h3><h4>银子:{{ money }}</h4><h4>车子:一辆{{car.brand}}车,价值{{car.price}}万元</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:(param:number)=>{}})// 第二个参数的含义是: 如果没有提供car, 那么就把第二个参数作为默认值(这样可以避免使用car时模板中红色波浪线)let car = inject('car',{brand:'未知',price:0})</script><style scoped>.grand-child{background-color: orange;padding: 20px;border-radius: 10px;box-shadow: 0 0 10px black;}
</style>

6.8 pinia

直接参考pinia章节即可。

6.9 slot插槽

1. 默认插槽

在这里插入图片描述

Father.vue
<template><div class="father"><h3>父组件</h3><div class="content"><Category title="热门游戏列表"><ul><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul></Category><Category title="今日美食城市"><img :src="imgUrl" alt=""></Category><Category title="今日影视推荐"><video :src="videoUrl" controls></video></Category></div></div>
</template><script setup lang="ts" name="Father">import Category from './Category.vue'import { ref,reactive } from "vue";let games = reactive([{id:'asgytdfats01',name:'英雄联盟'},{id:'asgytdfats02',name:'王者农药'},{id:'asgytdfats03',name:'红色警戒'},{id:'asgytdfats04',name:'斗罗大陆'}])let imgUrl = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg')let videoUrl = ref('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4')</script><style scoped>.father {background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}.content {display: flex;justify-content: space-evenly;}img,video {width: 100%;}
</style>
Category.vue
<template><div class="category"><h2>{{title}}</h2><!-- 1. 如果父组件在使用当前组件时, 父组件标签中没有传入内容, 那么这里就显示“默认内容” 2. 如果这里这里写多个slot, 那么父组件标签中传入的内容就会在每个slot地方都展示一遍3. 其实, 这里省略了name属性, 它的默认值为default, 即这里相当于: <slot name="default">默认内容</slot>--><slot>默认内容</slot><!-- 这里同样会再展示一遍 --><slot name="default">默认内容</slot></div></template><script setup lang="ts" name="Category">defineProps(['title'])</script><style scoped>.category {background-color: skyblue;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;width: 200px;height: 300px;}h2 {background-color: orange;text-align: center;font-size: 20px;font-weight: 800;}
</style>

2. 具名插槽

Father.vue
<template><div class="father"><h3>父组件</h3><div class="content"><Category><!-- v-slot只能用在组件标签上 或者 <template>标签中 --><template v-slot:s2><ul><!-- Category标签中的内容可以直接使用Father组件中的数据 --><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul></template><template v-slot:s1><h2>热门游戏列表</h2></template></Category><!-- 还可以直接把v-slot直接写在组件上, 它将会把内部的所有内容都塞到s2的插槽中 --><Category v-slot:s2><ul><!-- Category标签中的内容可以直接使用Father组件中的数据 --><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul></Category><Category><template v-slot:s2><img :src="imgUrl" alt=""></template><template v-slot:s1><h2>今日美食城市</h2></template></Category><!-- 简写写法 --><Category><template #s2><!-- Category标签中的内容可以直接使用Father组件中的数据 --><video video :src="videoUrl" controls></video></template><template #s1><h2>今日影视推荐</h2></template></Category></div></div>
</template><script setup lang="ts" name="Father">import Category from './Category.vue'import { ref,reactive } from "vue";let games = reactive([{id:'asgytdfats01',name:'英雄联盟'},{id:'asgytdfats02',name:'王者农药'},{id:'asgytdfats03',name:'红色警戒'},{id:'asgytdfats04',name:'斗罗大陆'}])let imgUrl = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg')let videoUrl = ref('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4')</script><style scoped>.father {background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}.content {display: flex;justify-content: space-evenly;}img,video {width: 100%;}h2 {background-color: orange;text-align: center;font-size: 20px;font-weight: 800;}
</style>
Category.vue
<template><div class="category"><slot name="s1">默认内容1</slot><slot name="s2">默认内容2</slot></div>
</template><script setup lang="ts" name="Category"></script><style scoped>.category {background-color: skyblue;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;width: 200px;height: 300px;}
</style>

3. 作用域插槽

理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在News组件中,但使用数据所遍历出来的结构由App组件决定)

Father.vue
<template><div class="father"><h3>父组件</h3><div class="content"><Game><!-- 这里的params可以拿到所有子组件中传给<slot>插槽标签的所有属性和对应的值 --><!-- 形成的效果就是: 结构是由父组件决定的, 而数据的提供者是子组件(至于子组件的这个数据哪来的就不用管了, 反正就是有); 或者换句话说: 父组件通过插槽的方式“直接”访问到了子组件通过插槽传递的数据;--><!-- 这里默认其实是:  v-slot:default="params"--><template v-slot="params"><ul><li v-for="y in params.youxi" :key="y.id">{{ y.name }}</li></ul></template></Game><Game><template v-slot="params"><ol><li v-for="item in params.youxi" :key="item.id">{{ item.name }}</li></ol></template></Game><Game><template #default="{youxi}"><h3 v-for="g in youxi" :key="g.id">{{ g.name }}</h3></template></Game></div></div>
</template><script setup lang="ts" name="Father">import Game from './Game.vue'
</script><style scoped>.father {background-color: rgb(165, 164, 164);padding: 20px;border-radius: 10px;}.content {display: flex;justify-content: space-evenly;}img,video {width: 100%;}
</style>
Category.vue
<template><div class="game"><h2>游戏列表</h2><!-- 给插槽提供数据 --><slot :youxi="games" x="哈哈" y="你好"></slot></div>
</template><script setup lang="ts" name="Game">import {reactive} from 'vue'let games = reactive([{id:'asgytdfats01',name:'英雄联盟'},{id:'asgytdfats02',name:'王者农药'},{id:'asgytdfats03',name:'红色警戒'},{id:'asgytdfats04',name:'斗罗大陆'}])</script><style scoped>.game {width: 200px;height: 300px;background-color: skyblue;border-radius: 10px;box-shadow: 0 0 10px;}h2 {background-color: orange;text-align: center;font-size: 20px;font-weight: 800;}
</style>

7. 其它 API

7.1 shallowRef 与 shallowReactive

shallowRef

  1. 作用:创建一个响应式数据,但只对顶层属性进行响应式处理。

  2. 用法:

    let myVar = shallowRef(initialValue);
    
  3. 特点:只跟踪引用值的变化,不关心值内部的属性变化。

shallowReactive

  1. 作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的

  2. 用法:

    const myObj = shallowReactive({ ... });
    
  3. 特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。

总结

通过使用 shallowRef()shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。

示例

<template><div class="app"><h2>求和为:{{ sum }}</h2><h2>名字为:{{ person.name }}</h2><h2>年龄为:{{ person.age }}</h2><h2>汽车为:{{ car }}</h2><button @click="changeSum">sum+1</button><button @click="changeName">修改名字</button><button @click="changeAge">修改年龄</button><button @click="changePerson">修改整个人</button><span>|</span><button @click="changeBrand">修改品牌</button><button @click="changeColor">修改颜色</button><button @click="changeEngine">修改发动机</button></div>
</template><script setup lang="ts" name="App">import { ref, reactive, shallowRef, shallowReactive } from 'vue'let sum = shallowRef(0)let person = shallowRef({name: '张三',age: 18})/* 如果使用ref来定义sum和person, 那么下面的方法被调用时, 数据都会发生改变, 并且都会有响应式;但因为使用shallowRef定义, 因此只有第1层修改才会数据发生改变, 具有响应式, (第1层指的是xxx.value, 不能再点下去了, 否则就不是第1层了)*/function changeSum() {sum.value += 1                       // 数据发生改变, 有响应式}function changeName() {person.value.name = '李四'           // 数据未发生改变}function changeAge() {person.value.age += 1                // 数据未发生改变}function changePerson() {person.value = { name: 'tony', age: 100 } // 数据发生改变, 有响应式}/* ****************** *//* 如果使用reactive来定义car, 那么下面的方法被调用时, 数据都会发生改变, 并且都会有响应式;但因为使用shallowReactive定义, 因此只有第1层修改才会数据发生改变, 具有响应式, (第1层指的是brand和options, 不能再点下去了, 否则就不是第1层了)*/let car = shallowReactive({brand: '奔驰',options: {color: '红色',engine: 'V8'}})function changeBrand() {car.brand = '宝马'}function changeColor() {car.options.color = '紫色'}function changeEngine() {car.options.engine = 'V12'}</script><style scoped>.app {background-color: #ddd;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;}button {margin: 0 5px;}
</style>

7.2 readonly 与 shallowReadonly

readonly

  1. 作用:用于创建一个对象的深只读副本。

  2. 用法:

    const original = reactive({ ... });
    const readOnlyCopy = readonly(original);
    
  3. 特点:

    • 对象的所有嵌套属性都将变为只读。
    • 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
  4. 应用场景:

    • 创建不可变的状态快照。
    • 保护全局状态或配置不被修改。

shallowReadonly

  1. 作用:与 readonly 类似,但只作用于对象的顶层属性。

  2. 用法:

    const original = reactive({ ... });
    const shallowReadOnlyCopy = shallowReadonly(original);
    
  3. 特点:

    • 只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。

    • 适用于只需保护对象顶层属性的场景。

示例

<template><div class="app"><h2>当前sum1为:{{ sum1 }}</h2><h2>当前sum2为:{{ sum2 }}</h2><button @click="changeSum1">点我sum1+1</button><button @click="changeSum2">点我sum2+1</button><!-- ******************* --><h2>当前car1为:{{ car1 }}</h2><h2>当前car2为:{{ car2 }}</h2><button @click="changeBrand2">修改品牌(car2)</button><button @click="changeColor2">修改颜色(car2)</button><button @click="changePrice2">修改价格(car2)</button></div>
</template><script setup lang="ts" name="App">import { ref, reactive, readonly, shallowReadonly } from "vue";let sum1 = ref(0)// 这里要传入1个响应式对象, 注意不要.value// 当sum1数据发生变化的时候, sum2也会发生变化, 但不能直接改sum2, 因为sum2只读,// (这样就可以达到一种保护数据的目的)let sum2 = readonly(sum1)function changeSum1() {sum1.value += 1}function changeSum2() {sum2.value += 1 // sum2是不能修改的}/******************/let car1 = reactive({brand: '奔驰',options: {color: '红色',price: 100}})// 这里要传入1个响应式对象// 当car1数据发生变化的时候, car2也会发生变化, // 但不能直接改car2的第一层属性, 因为这里使用的是shallowReadOnly, 意味着car2的第一层属性都只读,// 这里也可以使用readOnly, 这就意味着car2的任何属性都不能改了// (这样就可以达到一种保护数据的目的)let car2 = shallowReadonly(car1)function changeBrand2() {car2.brand = '宝马'}function changeColor2() {// 由于car2是对car1使用了shallowReadOnly, 因此这里是允许改的car2.options.color = '绿色'}function changePrice2() {car2.options.price += 10}
</script><style scoped>.app {background-color: #ddd;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;}button {margin: 0 5px;}
</style>

7.3 toRaw 与 markRaw

toRaw

  1. 作用:用于获取一个响应式对象的原始对象toRaw 返回的对象不再是响应式的,不会触发视图更新

  2. 官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。

  3. 何时使用? 在需要将响应式对象传递给非 Vue 的库或外部系统时,使用 toRaw 可以确保它们收到的是普通对象

markRaw

作用:标记一个对象,使其永远不会变成响应式的

例如使用mockjs时,为了防止误把mockjs变为响应式对象,可以使用 markRaw 去标记mockjs

示例

<template><div class="app"><h2>姓名:{{ person.name }}</h2><h2>年龄:{{ person.age }}</h2><button @click="person.age += 1">修改年龄</button>{{ rawPerson }}<!-- 这里修改rawPerson不会影响到person的数据的变化, 并且由于rawPerson不是响应式数据, 因此上面的{{ rawPerson }}也不会变化 --><button @click="rawPerson.age += 1">修改年龄rawPerson</button><hr><h2>{{ car2 }}</h2><button @click="car2.price += 10">点我价格+10</button></div>
</template><script setup lang="ts" name="App">import { reactive,toRaw,markRaw } from "vue";import mockjs from 'mockjs'/* toRaw */let person = reactive({name:'tony',age:18})// 用于获取一个响应式对象的原始对象let rawPerson = toRaw(person)console.log('响应式对象',person)  // Proxy(Object) {name: 'tony', age: 18}console.log('原始对象',rawPerson) // {name: 'tony', age: 18}console.log('------------------------');/* markRaw */// 如果这里没加markRaw, 那么这里的这个car就可以作为响应式对象的源头// 加上了markRaw之后, 就意味着car永远不能作为响应式对象的源头, 只能是1个原始的对象, 不能做成1个响应式对象let car = markRaw({brand:'奔驰',price:100})let car2 = reactive(car) // 这里的car2不是响应式的了// 从输出看, 其实就是加了个标记__v_skip: true, 当遇到这个标记时, 就不对这个对象做响应式处理console.log(car)  // {brand: '奔驰', price: 100, __v_skip: true}console.log(car2) // {brand: '奔驰', price: 100, __v_skip: true}// 例如使用mockjs时,为了防止误把mockjs变为响应式对象,可以使用 markRaw 去标记mockjslet mockJs = markRaw(mockjs)</script><style scoped>.app {background-color: #ddd;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;}button {margin:0 5px;}
</style>

7.4 customRef

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

示例

App.vue
<template><div class="app"><h2>{{ msg }}</h2><input type="text" v-model="msg"></div>
</template><script setup lang="ts" name="App">import {ref} from 'vue'import useMsgRef from './useMsgRef'// 使用Vue提供的默认ref定义响应式数据,数据一变,页面就更新//                               (这是vue给我们提供的功能, 也是承诺)// let msg = ref('你好')// 使用useMsgRef来定义一个响应式数据且有延迟效果let {msg} = useMsgRef('你好',1000)</script><style scoped>.app {background-color: #ddd;border-radius: 10px;box-shadow: 0 0 10px;padding: 10px;}button {margin:0 5px;}
</style>
useMsgRef.ts
import { customRef } from "vue";export default function (initValue: string, delay: number) {// 使用Vue提供的customRef定义响应式数据let timer: number// track(跟踪)、trigger(触发)let msg = customRef((track, trigger) => {return {// get何时调用?—— msg被读取时get() {track() // 告诉Vue数据msg很重要,你要对msg进行持续关注,一旦msg变化就去更新console.log('get');return initValue},// set何时调用?—— msg被修改时set(value) {console.log('set');clearTimeout(timer)timer = setTimeout(() => {initValue = valuetrigger() // 通知Vue一下数据msg变化了}, delay);}}})return { msg }
}

8. Vue3新组件

8.1 Teleport传送门

什么是Teleport?—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。

示例

这个示例有个奇怪的地方(css还有这种操作的),给outer加上filter之后,fixed定位就变成相对于父元素定位了,而不是body定位,这时,使用teleport可以解决这个问题,因为它把dom都传送走了,当然,teleport不仅可以适用于这种情况,也可用于其它场景。

App.vue
<template><div class="outer"><h2>我是App组件</h2><img src="http://www.atguigu.com/images/index_new/logo.png" alt=""><br><!-- 遮罩 --><Modal/></div>
</template><script setup lang="ts" name="App">import Modal from "./Modal.vue";
</script><style>.outer{background-color: #ddd;border-radius: 10px;padding: 5px;box-shadow: 0 0 10px;width: 400px;height: 400px;filter: saturate(200%);}img {width: 270px;}
</style>
Modal.vue
<template><button @click="isShow = true">展示弹窗</button><!-- 数据用的还是当前组件的, 但渲染的地方被传送到了body那里;to这里写的是选择器哦;--><teleport to='body'><div class="modal" v-show="isShow"><h2>我是弹窗的标题</h2><p>我是弹窗的内容</p><button @click="isShow = false">关闭弹窗</button></div></teleport></template><script setup lang="ts" name="Modal">import {ref} from 'vue'let isShow = ref(false)</script><style scoped>.modal {width: 200px;height: 150px;background-color: skyblue;border-radius: 10px;padding: 5px;box-shadow: 0 0 5px;text-align: center;position: fixed;left: 50%;top: 20px;margin-left: -100px;}
</style>

8.2 Suspense

  • 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
  • 使用步骤:
    • 异步引入组件
    • 使用Suspense包裹组件,并配置好defaultfallback

示例

App.vue
<template><div class="app"><h2>我是App组件</h2><Child/><Suspense><template v-slot:default><Child/></template><!-- 当组件未加载完成时, 显示的临时内容 --><template v-slot:fallback><h2>加载中......</h2></template></Suspense></div>
</template><script setup lang="ts" name="App">import {Suspense} from 'vue'import Child from './Child.vue'
</script><style>.app {background-color: #ddd;border-radius: 10px;padding: 10px;box-shadow: 0 0 10px;}
</style>
Child.vue
<template><div class="child"><h2>我是Child组件</h2><h3>当前求和为:{{ sum }}</h3></div>
</template><script setup lang="ts">import {ref} from 'vue'import axios from 'axios'let sum = ref(0);// 当下面多了这行请求数据的异步代码时, Child组件将不会展示出来(setup顶层最外面有async),// 需要父组件在使用时, 借助Suspense组件才能展示Child组件let {data:{content}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')console.log('content',content)/*   // 使用这种方式, 可以不借助Suspense组件也能展示Child组件let content = (async function() {let {data:{content}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')return content})(); */</script><style scoped>.child {background-color: skyblue;border-radius: 10px;padding: 10px;box-shadow: 0 0 10px;}
</style>

8.3 全局API转移到应用对象

  • app.component
  • app.config
  • app.directive
  • app.mount
  • app.unmount
  • app.use

示例

import {createApp} from 'vue'
import App from './App.vue'
import Hello from './Hello.vue'// 创建应用
const app = createApp(App)// 全局注册组件, 然后所有的地方都可以使用Hello这个组件了
app.component('Hello',Hello)// 全局挂载
// 类似于vue2的Vue.prototype.x=99, 然后所有的组件中都可以使用x了
app.config.globalProperties.x = 99// 解决全局挂载x的时候, ts报错的问题
declare module 'vue' {interface ComponentCustomProperties {x:number}
}// 全局注册指令, 然后所有的组件中都可以使用v-beauty了, 如: <h1 v-beauty="sum">好开心</h1>
app.directive('beauty',(element,{value})=>{element.innerText += valueelement.style.color = 'green'element.style.backgroundColor = 'yellow'
})// 挂载应用
app.mount('#app')// 卸载应用
setTimeout(() => {app.unmount()
}, 2000);

8.4 其他

  • 过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from

  • keyCode 作为 v-on 修饰符的支持。

  • v-model 指令在组件上的使用已经被重新设计,替换掉了 v-bind.sync。

  • v-ifv-for 在同一个元素身上使用时的优先级发生了变化。

  • 移除了$on$off$once 实例方法。

  • 移除了过滤器 filter

  • 移除了$children 实例 propert


http://www.ppmy.cn/devtools/31495.html

相关文章

Java_JVM_JVMs

JVM 官方文档说明文档目录 官方文档 JVM Specification 说明 以Java SE 17为标准 文档目录 2&#xff1a;JVM 结构 class文件数据类型 基本数据类型引用数据类型 运行时数据区 栈帧 其他内容 对象的表示浮点数运算特殊方法 初始化方法【实例、类】多态方法 3&#xff…

「 网络安全常用术语解读 」SBOM主流格式CycloneDX详解

CycloneDX是软件供应链的现代标准。CycloneDX物料清单&#xff08;BOM&#xff09;可以表示软件、硬件、服务和其他类型资产的全栈库存。该规范由OWASP基金会发起并领导&#xff0c;由Ecma International标准化&#xff0c;并得到全球信息安全界的支持&#xff0c;如今CycloneD…

MYSQL从入门到精通(二)

1、MYSQL高级概述 【1】架构概述 【2】索引优化 【3】查询截取 【4】mysql锁机制 【5】主从复制 2、MYSQL概述 【1】mysql内核 【2】sql优化工程师 【3】mysql服务器的优化 【4】各种参数常量设定 【5】查询语句优化 【6】主从复制 【7】软硬件升级 【8】容灾百分 【9】sql编…

C语言排序

各种排序&#xff1a; 我这里是统一排成升序 各个排序的时间复杂度&#xff1a; 冒泡排序&#xff1a;O(N ^ 2)(适应性不好(即很难在中途完成排序))插入排序&#xff1a;O(N ^ 2)(在该数组接近有序时&#xff0c;时间复杂度很小)(适应性好(即容易在中途完成排序))选择排序&a…

基于Springboot的CSGO赛事管理系统

采用技术 基于Springboot的CSGO赛事管理系统的设计与实现~ 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBootMyBatis 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 页面展示效果 系统功能 首页 赛事信息 赛事通知 参赛队注册页面 管理…

esp32-cam 1. 出厂固件编译与测试

0. 环境 - ubuntu18 - esp32-cam - usb转ttl ch340 硬件连接 esp32-camch340板子U0RTXDU0TRXDGNDGND5V5V 1. 安装依赖 sudo apt-get install vim sudo apt install git sudo apt-get install git wget flex bison gperf python python-pip python-setuptools python-serial p…

LeetCode 题目 95:从递归到动态规划实现 不同的二叉搜索树 II

本文探讨了多种生成所有可能二叉搜索树的算法&#xff0c;包括递归分治法、动态规划、记忆化递归&#xff0c;详解每种方法的实现及优劣势。 题目描述 给定一个整数 n&#xff0c;生成所有由 1 到 n 为节点所组成的二叉搜索树 (BST)。 输入格式 n&#xff1a;表示生成树的节…

特斯拉PIXCELL矩阵大灯擎耀远程控制技术照亮未来智能之光

在科技的浪潮中&#xff0c;特斯拉这个名字如同一道闪电&#xff0c;照亮了新能源汽车的天空。而在这片星空中&#xff0c;特斯拉PIXCELL矩阵大灯则如同一颗璀璨的星辰&#xff0c;以其独特的创新技术和卓越的性能&#xff0c;为驾驶者提供了前所未有的照明体验。矩阵大灯技术如…