文章目录
- 学习链接
- 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 + 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
。