vue3 + ts 快速入门(全)
文章目录
- 学习链接
- 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、 refs、parent、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最新标准,实现后台前端综合解决方案 imooc-admin源码
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. 新的特性
-
Composition API
(组合API
):-
setup
-
ref
与reactive
-
computed
与watch
…
-
-
新的内置组件:
-
Fragment
-
Teleport
-
Suspense
…
-
-
其他改变:
-
新的生命周期钩子
-
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
),能实现极速的服务启动。 - 对
TypeScript
、JSX
、CSS
等支持开箱即用(不用配置,直接就可以用)。 - 真正的按需编译,不再等待整个应用编译完成。
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
Vue2
的API
设计是Options
(配置)风格的。Vue3
的API
设计是Composition
(组合)风格的。
Options API 的弊端
Options
类型的 API
,数据、方法、计算属性等,是分散在:data
、methods
、computed
中的,若想新增或者修改一个需求,就需要分别修改:data
、methods
、computed
,不便于维护和复用。
Composition API 的优势
可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。
3.2. 拉开序幕的 setup
setup 概述
介绍
setup
是Vue3
中一个新的配置项,值是一个函数。- 它是
Composition API
“表演的舞台”,组件中所用到的:数据、方法、计算属性、监视…等等,均配置在setup
中。
特点如下:
setup
函数返回的对象中的内容,可直接在模板中使用。setup
中访问this
是undefined
。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
的配置(data
、methos
…)中可以访问到setup
中的属性、方法。- 但在
setup
中不能访问到Vue2
的配置(data
、methos
…)。 - 如果与
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>
扩展:上述代码,还需要编写一个不写
setup
的script
标签,去指定组件名字,比较麻烦,我们可以借助vite
中的插件简化
- 第一步:
npm i vite-plugin-vue-setup-extend -D
- 第二步:
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))}}
})
- 第三步:
<script setup lang="ts" name="Person">
3.3. ref 创建:基本类型的响应式数据
- **作用:**定义响应式变量。
- 语法:
let xxx = ref(初始值)
。 - **返回值:**一个
RefImpl
的实例对象,简称ref对象
或ref
,ref
对象的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中勾选
,它会在使用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
。 -
若需要一个响应式对象,层级不深,
ref
、reactive
都可以。 -
若需要一个响应式对象,且层级较深,推荐使用
reactive
。
3.7 toRefs 与 toRef
- 作用:将一个响应式对象中的每一个属性,转换为
ref
对象。 - 备注:
toRefs
与toRef
功能一致,但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
定义的对象中的属性,newValue
和oldValue
都是新值,因为它们是同一个对象。 -
若修改整个
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>
* 情况四
监视ref
或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: '爱玛' }}</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
都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch
:要明确指出监视的数据
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
的生命周期创建阶段:
beforeCreate
、created
挂载阶段:
beforeMount
、mounted
更新阶段:
beforeUpdate
、updated
销毁阶段:
beforeDestroy
、destroyed
-
Vue3
的生命周期创建阶段:
setup
(替代了之前vue2中的beforeCreate、created)挂载阶段:
onBeforeMount
、onMounted
更新阶段:
onBeforeUpdate
、onUpdated
卸载阶段:
onBeforeUnmount
、onUnmounted
(就对应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、路由组件通常存放在pages
或 views
文件夹,一般组件通常存放在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. 路由器工作模式
-
history
模式优点:
URL
更加美观,不带有#
,更接近传统的网站URL
。缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有
404
错误。const router = createRouter({history:createWebHistory(), //history模式/******/ })
-
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参数
- 定义路由规则,并定义路由路径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}]
})
- 传递参数
<!-- 跳转并携带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>
- 接收参数:
// 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属性
-
作用:控制路由跳转时操作浏览器历史记录的模式。
-
浏览器的历史记录有两种写入方式:分别为
push
和replace
:push
是追加历史记录(默认值)。replace
是替换当前记录。
-
开启
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 重定向
-
作用:将特定的路径,重新定向到已有路由。
-
具体编码:
{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 准备一个效果

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 存储+读取数据
-
Store
是一个保存:状态、业务逻辑 的实体,每个组件都可以读取、写入它。 -
它有三个概念:
state
、getter
、action
,相当于组件中的:data
、computed
和methods
。
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用法
- 借助
storeToRefs
将store
中的数据转为ref
对象,方便在模板中使用。 - 注意:
pinia
提供的storeToRefs
只会将数据做转换,而Vue
的toRefs
会转换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
)功能类似,可以实现任意组件间通信。
安装mitt
,npm 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
-
概述:
$attrs
用于实现**当前组件的父组件,向当前组件的子组件**通信(祖→孙)。 -
具体说明:
$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、 refs、parent、proxy
-
概述:
$refs
用于 :父→子。$parent
用于:子→父。
-
原理如下:
属性 说明 $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
-
概述:实现祖孙组件直接通信
-
具体使用:
- 在祖先组件中通过
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
-
作用:创建一个响应式数据,但只对顶层属性进行响应式处理。
-
用法:
let myVar = shallowRef(initialValue);
-
特点:只跟踪引用值的变化,不关心值内部的属性变化。
shallowReactive
-
作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的
-
用法:
const myObj = shallowReactive({ ... });
-
特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。
总结
通过使用 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
-
作用:用于创建一个对象的深只读副本。
-
用法:
const original = reactive({ ... }); const readOnlyCopy = readonly(original);
-
特点:
- 对象的所有嵌套属性都将变为只读。
- 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
-
应用场景:
- 创建不可变的状态快照。
- 保护全局状态或配置不被修改。
shallowReadonly
-
作用:与
readonly
类似,但只作用于对象的顶层属性。 -
用法:
const original = reactive({ ... }); const shallowReadOnlyCopy = shallowReadonly(original);
-
特点:
-
只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。
-
适用于只需保护对象顶层属性的场景。
-
示例
<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
-
作用:用于获取一个响应式对象的原始对象,
toRaw
返回的对象不再是响应式的,不会触发视图更新。 -
官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
-
何时使用? 在需要将响应式对象传递给非
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
包裹组件,并配置好default
与fallback
示例
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-if
和v-for
在同一个元素身上使用时的优先级发生了变化。 -
移除了
$on
、$off
和$once
实例方法。 -
移除了过滤器
filter
。 -
移除了
$children
实例propert
。
相关文章:

vue3 + ts 快速入门(全)
文章目录 学习链接1. Vue3简介1.1. 性能的提升1.2.源码的升级1.3. 拥抱TypeScript1.4. 新的特性 2. 创建Vue3工程2.1. 基于 vue-cli 创建2.2. 基于 vite 创建(推荐)vite介绍创建步骤项目结构安装插件项目结构总结 2.3. 一个简单的效果Person.vueApp.vue …...
vue2实现面包屑功能
目录 1. store/index.js 2. router/index.js 3. Header.vue 在Vue 2中实现面包屑导航是一种常见的前端实践,它可以帮助用户了解当前页面在网站结构中的位置,并快速导航到上一级或根目录。以下是使用Vue 2实现面包屑导航的基本步骤: 1. st…...
helm安装 AWS Load Balancer Controller
1、创建AmazonEKSLoadBalancerControllerRole角色 亚马逊文档 创建文档 2)、使用 eksctl 创建 IAM 角色 a、安装eksctl eksctl安装文档 使用以下命令下载并提取最新版本的 eksctl curl --silent --location "https://github.com/weaveworks/eksctl/releases/l…...

贪吃蛇大作战(C语言--实战项目)
朋友们!好久不见。经过一段时间的沉淀,我这篇文章来和大家分享贪吃蛇大作战这个游戏是怎么实现的。 (一).贪吃蛇背景了解及效果展示 首先相信贪吃蛇游戏绝对称的上是我们00后的童年,不仅是贪吃蛇还有俄罗斯⽅块&…...
谷歌确认:链接并不那么重要
谷歌的 Gary Illyes 在最近的一次搜索营销会议上证实,谷歌只需要很少的链接,这为出版商需要关注其他因素提供了越来越多的证据。Gary 在推特上证实了他确实说过这些话。 排名链接的背景 20 世纪 90 年代末,搜索引擎发现链接是验证网站权威性…...
python基础--修饰器
修饰器(语法糖) 在python中函数实际上就是一个对象 def outer(x):def inner(y):return x yreturn innerprint(outer(6)(5))def double(x):return x * 2 def triple(x):return x * 3def calc_number(func, x):print(func(x))calc_number(double, 3) calc_number(triple, 3)函…...

6. Z 字形变换
题目描述 给你一个字符串s和行数numRows,把s字符串按照z字形重新排列。 再从左往右进行读取,返回读取之后的字符串。 本题是找规律,但是没有找出来 解题思路 要想解出来该题,在进行z字变换的时候,我们把字符串的下…...

shell常用文件处理命令
1. 解压 1.1 tar 和 gz 文件 如果你有一个 .tar 文件,你可以使用以下命令来解压: tar -xvf your_file.tar在这个命令中,-x 表示解压缩,-v 表示详细输出(可选),-f 后面跟着要解压的文件名。 如果你的 .tar 文件同时被 gzip 压缩了(即 .tar.gz 文件),你可以使用以下…...

从Paint 3D入门glTF
Paint 3D Microsoft Paint 3D是微软的一款图像编辑软件,它是传统的Microsoft Paint程序的升级版。 这个新版本的Paint专注于三维设计和创作,使用户可以使用简单的工具创建和编辑三维模型。 Microsoft Paint 3D具有直观的界面和易于使用的工具࿰…...

数据库(MySQL)—— DQL语句(基本查询和条件查询)
数据库(MySQL)—— DQL语句(基本查询和条件查询) 什么是DQL语句基本查询查询多个字段字段设置别名去除重复记录 条件查询语法条件 我们今天进入MySQL的DQL语句的学习: 什么是DQL语句 MySQL中的DQL(Data Q…...
如何根据索引删除数组中的元素,并保证删除的正确性
使用 splice() 方法来删除这些索引处的数据 var array [1, 2, 3, 4, 5]; var indexesToDelete [1, 3]; // 需要删除的索引// 将需要删除的索引按照从大到小的顺序排序,以避免删除元素后索引发生变化 indexesToDelete.sort((a, b) > b - a);// 遍历需要删除的索…...

Shell编程规范与变量
目录 一、shell脚本概述 Shell脚本的概念 Shel脚本应用场景 1、shell的作用 2、shell编程规范 Shell脚本的编写 Shell脚本的运行 3、重定向与管道 交互式硬件设备 重定向操作 管道操作符号"|" 二、shell脚本变量 变量的作用 变量的类型 1、自定义变量…...

武汉星起航:策略升级,亚马逊平台销售额持续增长显实力
武汉星起航电子商务有限公司,一家致力于跨境电商领域的企业,于2023年10月30日在上海股权托管交易中心成功挂牌展示,这一里程碑事件标志着公司正式踏入资本市场,开启了新的发展篇章。公司董事长张振邦在接受【第一财经】采访时表示…...

循环链表 -- c语言实现
#pragma once // 带头双向循环链表增删查改实现 #include<stdlib.h> #include<stdio.h> #include<assert.h>typedef int LTDataType;typedef struct ListNode {LTDataType data;struct ListNode* next;struct ListNode* prev; }ListNode;//双链表申请一个新节…...

如何使git提交的时候忽略一些特殊文件?
认识.gitignore文件 在生成远程仓库的时候我们会看到这样一个选项: 这个.gitignore文件有啥用呢? .gotignore文件是Git版本控制系统中的一个特殊文件。用来指定哪些文件或者目录不被Git追踪或者提交到版本库中。也就意味着,如果我们有一些文…...

如何保证Redis双写一致性?
目录 数据不一致问题 数据库和缓存不一致解决方案 1. 先更新缓存,再更新数据 该方案数据不一致的原因 2. 先更新数据库,再更新缓存 3. 先删除缓存,再更新数据库 延时双删 4. 先更新数据库,再删除缓存 该方案数据不一致的…...

HarmonyOS实战开发-如何实现查询当前城市实时天气功能
先来看一下效果 本项目界面搭建基于ArkUI中TS扩展的声明式开发范式, 数据接口是和风(天气预报), 使用ArkUI自带的网络请求调用接口。 我想要实现的一个功能是,查询当前城市的实时天气, 目前已实现的功能…...

(三)JSP教程——JSP动作标签
JSP动作标签 用户可以使用JSP动作标签向当前输出流输出数据,进行页面定向,也可以通过动作标签使用、修改和创建对象。 <jsp:include>标签 <jsp:include>标签将同一个Web应用中静态或动态资源包含到当前页面中。资源可以是HTML、JSP页面和文…...

centos7安装真的Redmine-5.1.2+ruby-3.0.0
下载redmine-5.1.2.tar.gz,上传到/usr/local/目录下 cd /usr/local/ tar -zxf redmine-5.1.2.tar.gz cd redmine-5.1.2 cp config/database.yml.example config/database.yml 配置数据连接 #编辑配置文件 vi config/database.yml #修改后的内容如下 product…...

方法的重写
方法的重写 概念:子类继承父类之后,就拥有了符合权限的父类的属性和方法,但是当父类的方法不符合子类的要求的时候,子类也可以重新的书写自己想要的方法。所以,方法的重写,即子类继承父类的方法后…...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...

黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 
《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...
【LeetCode】3309. 连接二进制表示可形成的最大数值(递归|回溯|位运算)
LeetCode 3309. 连接二进制表示可形成的最大数值(中等) 题目描述解题思路Java代码 题目描述 题目链接:LeetCode 3309. 连接二进制表示可形成的最大数值(中等) 给你一个长度为 3 的整数数组 nums。 现以某种顺序 连接…...

Ubuntu Cursor升级成v1.0
0. 当前版本低 使用当前 Cursor v0.50时 GitHub Copilot Chat 打不开,快捷键也不好用,当看到 Cursor 升级后,还是蛮高兴的 1. 下载 Cursor 下载地址:https://www.cursor.com/cn/downloads 点击下载 Linux (x64) ,…...

AI语音助手的Python实现
引言 语音助手(如小爱同学、Siri)通过语音识别、自然语言处理(NLP)和语音合成技术,为用户提供直观、高效的交互体验。随着人工智能的普及,Python开发者可以利用开源库和AI模型,快速构建自定义语音助手。本文由浅入深,详细介绍如何使用Python开发AI语音助手,涵盖基础功…...
Windows 下端口占用排查与释放全攻略
Windows 下端口占用排查与释放全攻略 在开发和运维过程中,经常会遇到端口被占用的问题(如 8080、3306 等常用端口)。本文将详细介绍如何通过命令行和图形化界面快速定位并释放被占用的端口,帮助你高效解决此类问题。 一、准…...