Vue3学习
- 1. Vue3
- 新的特性
- 2. 创建Vue3工程
- 2.1 基于 vue-cli 创建
- 项目文件说明
- 2.2 基于 vite 创建
- 具体操作
- 项目文件说明
- 2.3 简单案例(vite)
- 3. Vue3核心语法
- 3.1 OptionsAPI 与 CompositionAPI
- Options API 弊端
- Composition API 优势
- ⭐3.2 setup
- 小案例
- setup返回值
- setup 与 Options API 的关系
- setup语法糖
- 3.2 ref
- ref 创建:基本类型的响应式数据
- ref 创建:对象类型的响应式数据
- 3.3 reactive
- reactive 创建:对象类型的响应式数据
- 3.4 ref 对比 reactive
- 区别
- 使用原则
- 3.5 toRefs 与 toRef
- 3.6 computed
- 3.7 watch
- 情况一
- 情况二
- 情况三
- 情况四
- 情况五
- 3.8 watchEffect
- 3.9 标签中的ref属性
- 回顾TS
- vue文件路径 @
- 3.10 props使用
- reactive和泛型一起用
- 示例
- 3.11 生命周期
- 3.12 自定义hooks
1. Vue3
新的特性
-
Composition API
(组合API
):-
setup
-
ref
与reactive
-
computed
与watch
…
-
-
新的内置组件:
-
Fragment
-
Teleport
-
Suspense
…
-
-
其他改变:
-
新的生命周期钩子
-
data
选项应始终被声明为一个函数 -
移除
keyCode
支持作为v-on
的修饰符…
-
2. 创建Vue3工程
2.1 基于 vue-cli 创建
点击查看官方文档
备注:目前
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
具体步骤:
① 项目名字不可以大写英文
②
③ 进入文件夹运行
项目文件说明
- /src/main.js 入口文件
import { createApp } from 'vue'
/* 这里引入的不再是vue构造函数了,在vue2中 import Vue from 'vue'
vue3 引入的是一个名为 creatApp 的工厂函数
区别是 vue 构造函数需要通过new调用,工厂函数可以直接调用creatApp
*/
import App from './App.vue'
// 直接调用 createApp函数
// 把外壳组件 App 传递进去
// #app是页面上容器的 id
createApp(App).mount('#app')
/*
不简写:
const app = createApp(App) 创建应用实例对象app 类似vue2中的vm,但app更轻
app.mount('#app') 挂载
*/
/*
vue2写法:
new Vue({
el: '#app', //容器
})
或者
new Vue({
render:(h)=>h(App)
}).$mount('#app)
或者
const vm = new Vue({
render:(h)=>h(App)
})
vm.$mount('#app)
*/
- vue3组件中可以没有 根标签
2.2 基于 vite 创建
vite
是新一代前端构建工具,官网地址:https://vitejs.cn。
vite
的优势如下:
轻量快速的热重载(HMR
热重载的意思是改代码之后局部刷新),能实现极速的服务启动。
对 TypeScript
、JSX
、CSS
等支持开箱即用。
真正的按需编译,不再等待整个应用编译完成。
webpack
构建 与 vite
构建对比图如下:
webpack 分析路由、模块=>进行打包=>服务器准备(8080)。所以我们每次执行npm run serve时都需要等待一段时间(等的就是打包的过程)
vite先打开服务器
具体操作
(点击查看官方文档)
## 1.创建命令
npm create vue@latest
## 2.具体配置
## 配置项目名称
√ Project name: vue3_test
## 是否添加TypeScript支持
√ Add TypeScript? Yes
## 是否添加JSX支持
√ Add JSX Support? No
## 是否添加路由环境
√ Add Vue Router for Single Page Application development? No
## 是否添加pinia环境
√ Add Pinia for state management? No
## 是否添加单元测试
√ Add Vitest for Unit Testing? No
## 是否添加端到端测试方案
√ Add an End-to-End Testing Solution? » No
## 是否添加ESLint语法检查
√ Add ESLint for code quality? Yes
## 是否添加Prettiert代码格式化
√ Add Prettier for code formatting? No
具体步骤:
Need to install the following packages:
create-vue@3.10.2
Ok to proceed? (y) y
项目文件说明
.vscode/extensions.json 官方推荐的插件
public/favicon.ico 页签图标
src/ 源代码文件
.gitignore git的忽略文件
env.d.ts 该文件只有一一行/// <reference types="vite/client" />
且飘红,飘红的原因是当前没有 node_modules(没有依赖),在命令行 npm i 安装,重新打开 vs code就不红了。 .ts文件不认识 .jpg .txt,这个文件的目的就是让ts认识他们。
index.html 入口文件(与webpack区别)
package.json 、package-lock.json 包管理文件
REANME.md可以删除
tsconfig.app.json, tsconfig.json, tsconfig.node.json是ts的配置文件
vite.config.ts 整个工程的配置文件
src文件夹说明
入口文件 index.html把 /src/main.ts 引入
- main.ts
import './assets/main.css' // 引入样式
import { createApp } from 'vue' // 创建应用(花盆)
import App from './App.vue' // 组件(植物的根)
createApp(App).mount('#app')
//挂载到 index.html 的 app div里面,
总结:
Vite
项目中,index.html
是项目的入口文件,在项目最外层。- 加载
index.html
后,Vite
解析<script type="module" src="/src/main.ts"></script>
指向的typescript
。 Vue3
中是通过createApp
函数创建一个应用实例。
2.3 简单案例(vite)
注意:当前笔记及之后 都是使用 vite 创建的工程
这个案例证明了vue3可以写vue2的代码
① <script lang="ts">
这里写明是 ts
- /components/Person.vue
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="showTel">点击查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name: 'Person',
data() {
return {
name: '张三',
age: 18,
tel: '11111111111111'
}
},
methods: {
changeName(){
this.name = 'zhang-san'
},
changeAge(){
this.age++
},
showTel(){
alert(this.tel)
}
},
}
</script>
<style>
.person {
background-color: skyblue;
}
button {
margin: 0 5px;
}
</style>
- /App.vue
对person组件,①引用import ②注册components ③使用
<template>
<div class="haha">
<h1>你好啊</h1>
<Person></Person>
</div>
</template>
<script lang="ts">
import Person from './components/Person.vue'
export default {
name: 'App',
components: {
Person
}
}
</script>
<style>
.haha{
background-color: pink;
}
</style>
3. Vue3核心语法
3.1 OptionsAPI 与 CompositionAPI
Vue2
的API
设计是Options
(配置)风格的。Vue3
的API
设计是Composition
(组合)风格的。
Options API 弊端
选项式/配置式写法
学习vue2就是在学习配置项 data、methods、components。
Options
类型的 API
,数据、方法、计算属性等,是分散在:data
、methods
、computed
中的,若想新增或者修改一个需求,就需要分别修改:data
、methods
、computed
,不便于维护和复用。
Composition API 优势
可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。
⭐3.2 setup
setup
是Vue3
中一个新的配置项,值是一个函数,它是 Composition API
“表演的舞台”,组件中所用到的:数据、方法、计算属性、监视…等等,均配置在setup
中。
小案例
- 下文案例的问题:数据不是响应式(之后会讲)
注意事项:
① setup中的函数没有维护 this。在setup函数中 console.log(this),值为undefined
② setup函数的时机:setup比vue2中的 beforeCreate时机早(之后会讲)
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="showTel">点击查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name: 'Person',
// data() {
// return {
// name: '张三',
// age: 18,
// tel: '11111111111111'
// }
// },
// methods: {
// changeName(){
// this.name = 'zhang-san'
// },
// changeAge(){
// this.age++
// },
// showTel(){
// alert(this.tel)
// }
// },
setup() {
// 数据
// 如果这么简单的定义数据,那就不是响应式数据
// 响应式:后续数据变化 页面上也变化
let name = '张三'
let age = 18
let tel = '1388888'
// 方法
function changeName(){
// this.name = '???'
// 这样写是错的,vue在setup中没有维护this关键字
// console.log(this) undefined
name = 'zhang-san'
}
function changeAge(){
age += 1
}
function showTel(){
alert(tel)
}
// 返回数据和函数
return {name,age,changeName,changeAge,showTel}
}
}
</script>
<style>
.person {
background-color: skyblue;
}
button {
margin: 0 5px;
}
</style>
setup返回值
setup的返回值是对象,把数据、方法交出去
setup() {
...
return {name,age,changeName,changeAge,showTel}
}
setup的返回值如果是一个函数的话,会直接渲染页面内容
setup(){
...
return function() {
'哈哈'
}
}
或者
setup(){
...
return () => '哈哈' //因为setup不维护this 所以可以直接简写为箭头函数
}
setup 与 Options API 的关系
面试题
① data 、methods 、setup可以同时存在
② Vue2
的配置(data
、methos
…)中可以访问到 setup
中的属性、方法。(因为setup是最早的生命周期,当data去读数据的时候,setup已经有数据了)
③ 但在setup
中不能访问到Vue2
的配置(data
、methos
…)。
④ 如果与Vue2
冲突,则setup
优先。
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<hr>
<h2>测试:data读到setup中的数据{{ b }}</h2>
<h2>测试:setup中无法读到data中的数据</h2>
</div>
</template>
<script lang="ts">
export default {
name: 'Person',
data() {
return {
// 测试
a: 100,
b: this.name,
d: 900
}
},
setup() {
let name = '张三'
let age = 18
let tel = '1388888'
// 测试
// let s = d // Cannot find name 'd'.
return {name,age}
}
}
</script>
setup语法糖
setup
函数有一个语法糖,这个语法糖,可以让我们把setup
独立出去,代码如下:
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="showTel">点击查看联系方式</button>
</div>
</template>
<script lang="ts">
export default {
name: 'Person'
}
</script>
<script lang="ts" setup>
let name = '张三'
let age = 18
let tel = '1388888'
// 方法
function changeName(){ name = 'zhang-san'}
function changeAge(){age += 1}
function showTel(){alert(tel)}
</script>
扩展: 上述代码,还需要编写一个不写setup
的script
标签,(在开发者工具)去指定组件名字,比较麻烦,我们可以借助vite
中的插件简化。
-
第一步:
npm i vite-plugin-vue-setup-extend -D
(D是开发依赖的意思) -
第二步:
vite.config.ts
中引入: import VueSetupExtend from ‘vite-plugin-vue-setup-extend’
-
第三步:
<script setup lang="ts" name="Person">
这样就只用一个script标签了:
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="showTel">点击查看联系方式</button>
</div>
</template>
<script lang="ts" setup name="Person2222">
let name = '张三'
let age = 18
let tel = '1388888'
// 方法
function changeName(){ name = 'zhang-san'}
function changeAge(){age += 1}
function showTel(){alert(tel)}
</script>
如果【不】添加这个插件,同时也只写一个script标签,开发者工具中的组件名字和文件名保持一致。
3.2 ref
上文案例中的数据方法:此时数据不是响应式的
vue2中,数据都存放在data里面,只要把数据放在data里面,数据就是响应式,会自动数据代理、数据劫持。
在vue3中,有两个东西定义响应式数据 :① ref ②reactive
ref 创建:基本类型的响应式数据
作用: 定义响应式变量。
语法: 先引入 import {ref} from 'vue'
哪个数据是响应式就用ref包起来 let xxx = ref(初始值)
。
ref() 返回值: 一个RefImpl
的实例对象,简称ref对象
或ref
,ref
对象的value
属性是响应式的。
注意点:
JS
中操作数据需要:xxx.value
,但模板中不需要.value
,直接使用即可。- 对于
let name = ref('张三')
来说,name
不是响应式的,name.value
是响应式的。
<script lang="ts" setup name="Person">
// 引入ref
import {ref} from 'vue'
// 哪个数据是响应式就用ref包起来
let name = ref('张三')
let age = ref(18)
let tel = '1388888'
let address = '北京昌平区'
console.log('name',name);
console.log('age',age);
console.log('tel',tel);
console.log('address',address);
</script>
name、age是 RefImpl 的实例对象
目前先知道 下划线的属性都不是给我们用的,value才是给我们用的。而且模板(HTML)内自动帮我们解析了value,所以模板中使用的时候不需要 {{name.value}}使用,直接{{name}}就可以。但是在JS代码中需要写 name.value
ref 创建:对象类型的响应式数据
- 其实
ref
接收的数据可以是:基本类型、对象类型。 - 若
ref
接收的是对象类型,内部其实也是调用了reactive
函数。
<template>
<div class="car">
<h2>一辆{{ car.brand }}车,价值{{ car.price }}万</h2>
<button @click="changePrice">修改汽车价格</button>
</div>
<hr>
<h2>游戏列表</h2>
<ul>
<li v-for="item in games" :key="item.id">{{item.name}}</li>
</ul>
<button @click="changeFirstGame">修改游戏</button>
</template>
<script lang="ts" setup name="Person">
import {ref} from 'vue'
let car = ref({brand: '奔驰', price: 100})
console.log(car); // 这个对象
let games = ref([
{id:'001', name:'王者荣耀'},
{id:'002', name:'原神'},
{id:'003', name:'三国志'}
])
function changePrice(){
car.value.price += 10
}
function changeFirstGame(){
games.value[0].name = '星穹铁道'
}
</script>
3.3 reactive
reactive 创建:对象类型的响应式数据
作用:定义一个响应式对象(基本类型不要用它,要用ref
,否则报错)
语法: 引入reactive import {reactive} from 'vue'
哪个对象想变成响应式就包裹住哪个let 响应式对象= reactive(源对象)
。
源对象可以是 对象、数组、函数
返回值: 一个Proxy
的实例对象,简称:响应式对象。
注意点:reactive
定义的响应式数据是 深层次 的。
<template>
<div class="car">
<h2>一辆{{ car.brand }}车,价值{{ car.price }}万</h2>
<button @click="changePrice">修改汽车价格</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {reactive} from 'vue'
let car = reactive({brand: '奔驰', price: 100})
console.log(car); // 这个对象
function changePrice(){
car.price += 10
}
</script>
打印这个car:
这个Proxy是window上就有的一个函数 Proxy。数据藏在 target 里面
3.4 ref 对比 reactive
宏观角度看:
1、ref
用来定义:基本类型数据、对象类型数据;
2、reactive
用来定义:对象类型数据。
区别
1、ref
创建的变量必须使用.value
(可以使用volar
插件自动添加.value
)。
2、reactive
重新分配一个新对象,会失去响应式(可以使用Object.assign
去整体替换)。
注意:在ref中整体替换就是可以的。因为 ref 的响应式体现在 car.value,这个值变了 响应式可以检测到。
使用原则
1、若需要一个基本类型的响应式数据,必须使用ref
。
2、若需要一个响应式对象,层级不深,ref
、reactive
都可以。
3、若需要一个响应式对象,且层级较深,推荐使用reactive
。
3.5 toRefs 与 toRef
作用:将一个响应式对象中的每一个属性,转换为ref
对象。
备注:toRefs
与toRef
功能一致,但toRefs
可以批量转换。
语法如下:
// 数据
let person = reactive({name:'张三', age:18, gender:'男'})
// 通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
let {name,gender} = toRefs(person)
// 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力
let age = toRef(person,'age')
- 示例:
加入toRefs
<script lang="ts" setup name="Person">
import {reactive, toRefs} from 'vue'
let person = reactive({
name: '张三',
age: 18
})
let {name, age} = toRefs(person)
function changeName() {
name.value += '~'
}
function changeAge() {
age.value += 1
}
</script>
3.6 computed
计算属性是有缓存的、方法没有
使用方式:import {computed} from 'vue'
computed是一个函数,实参是函数,因为vue3中没有this,所以用箭头函数
必须要使用 set get才能对计算属性进行修改
<script lang="ts" setup name="Person">
import {ref,computed} from 'vue'
let firstName = ref('张')
let lastName = ref('三')
// 这么定义的fullName是一个计算属性,只读、不可以修改
let fullName = computed(()=>{
return firstName.value + '-' + lastName.value
})
// 上述的fullName是 ComputedRefImpl{}
// 这么定义的fullName是一个计算属性,可读可写
let fullName2 = computed({
get(){
return firstName.value + '-' + lastName.value
},
set(val){
const [str1, str2] = val.split('-')
firstName.value = str1
lastName.value = str2
}
})
function changeName(){
fullName2.value = '李-四'
}
</script>
3.7 watch
watch作用:监视数据的变化(和Vue2
中的watch
作用一致)
特点:Vue3
中的watch
只能监视以下四种数据:
ref
定义的数据。(ref能定义基本类型和对象类型)reactive
定义的数据。- 一个函数,返回一个值(也就是一个
getter
函数)。- 一个包含上述内容的数组。
我们在Vue3
中使用watch
的时候,通常会遇到以下几种情况:
情况一
情况一:监视ref
定义的【基本类型】数据:直接写数据名即可,监视的是其value
值的改变。
使用:
引入watch: import {ref, watch} from 'vue'
。watch是函数,有很多参数,目前只看两个 watch(监视对象,回调)
回调函数的参数是(newValue, oldValue)因为setup中没有this(undefined),所以可以直接写成箭头函数。
- 示例
<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】定义的【基本类型】数据
const stopWatch = watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
if(newValue >= 10){
stopWatch()
}
})
</script>
- 注意事项
① watch 里面的 sum(监视的ref的数据,不写value)
② 解除监视 :watch的返回值是一个函数,调用这个函数就可以结束监视
情况二
监视ref
定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。
注意:
若修改的是
ref
定义的对象中的属性,newValue
和oldValue
都是新值,因为它们是同一个对象。若修改整个
ref
定义的对象,newValue
是新值,oldValue
是旧值,因为不是同一个对象了。
watch的第一个参数是:被监视的数据
watch的第二个参数是:监视的回调(新值,旧值)
watch的第三个参数是:配置对象(deep、immediate等等.....)
<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 += '~'
}
function changeAge(){
person.value.age += 1
}
function changePerson(){
person.value = {name:'李四',age:90}
}
/*
监视【ref】定义的【对象类型】数据,监视的是对象的地址值,若想监视对象内部属性的变化,需要手动开启深度监视deep
watch的第一个参数是:被监视的数据
watch的第二个参数是:监视的回调
watch的第三个参数是:配置对象(deep、immediate等等.....)
*/
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
},{deep:true})
</script>
- 注意事项 有些时候 newValue oldValue是一样的
若修改的是ref
定义的对象中的属性,newValue
和 oldValue
都是新值,因为它们是同一个对象。
情况三
监视reactive
定义的【对象类型】数据,且 默认开启了深度监视。且不能关闭,即不能通过deep:false关闭深度监视(隐式创建深层监听)。
<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 += '~'
}
function changeAge(){
person.age += 1
}
function changePerson(){
Object.assign(person,{name:'李四',age:80})
}
function test(){
obj.a.b.c = 888
}
// 监视,情况三:监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
})
watch(obj,(newValue,oldValue)=>{
console.log('Obj变化了',newValue,oldValue)
})
</script>
情况四
监视ref
或reactive
定义的【对象类型】数据中的某个属性,注意点如下:
- 若该属性值不是【对象类型】,需要写成函数形式。
- 若该属性值是依然是【对象类型】,可直接写,也可写成函数,建议写成函数。
结论:监视的要是对象里的属性,那么最好写函数式(因为setup中没有this,所以直接写箭头函数),注意点:如果要同时① 监视对象属性的地址值,②需要关注对象内部,需要手动开启深度监视。
<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内部的一个属性
person.car = {c1:'雅迪',c2:'爱玛'}
}
// 监视person对象里面的 name属性(不是对象类型)
watch(()=>person.name,(newValue,oldValue)=>{
console.log('person.name',newValue,oldValue);
})
接上文代码,如果想监视 person.car(这个属性是一个对象属性),watch的第一个参数可以直接写 person.car ,但是 当执行函数changeCar的时候,无法监视。因为这段代码的意思是监视这个 person.car,当changeCar函数把整个car都换了的时候,就无法监视到原本的car。
watch(person.car,(newValue,oldValue)=>{
console.log('person.car',newValue,oldValue);
})
因此如果需要监视原本 person.car 的地址值的时候,就需要用函数包裹起来(如下文代码)。但此时 修改第一辆第二辆车的时候又没法检测到了,因为地址值没有变化。
watch(()=>person.car,(newValue,oldValue)=>{
console.log('person.car',newValue,oldValue);
})
怎么同时检测呢?加一个deep:true
watch(()=>person.car,(newValue,oldValue)=>{
console.log('person.car',newValue,oldValue);
},{deep:true})
情况五
监视上述的多个数据
<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:'爱玛'}
}
// 监视,情况五:监视上述的多个数据
watch([()=>person.name,person.car],(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
3.8 watchEffect
-
官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。
-
watch
对比watchEffect
-
都能监听响应式数据的变化,不同的是监听数据变化的方式不同
-
watch
:要明确指出监视的数据 -
watchEffect
:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。
-
-
示例
需求:当水温达到60度,或水位达到80cm时,给服务器发请求。
为什么要引入watcheffect?因为如果同时检测的数据多了,每一个都要写进数据里很不方便。
<template>
<div class="person">
<h2>需求:当水温达到60度,或水位达到80cm时,给服务器发请求</h2>
<h2>当前水温:{{temp}}℃</h2>
<h2>当前水位:{{height}}cm</h2>
<button @click="changeTemp">水温+10</button>
<button @click="changeHeight">水位+10</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,watch,watchEffect} from 'vue'
// 数据
let temp = ref(10)
let height = ref(0)
// 方法
function changeTemp(){
temp.value += 10
}
function changeHeight(){
height.value += 10
}
// 监视 -- watch实现
/* watch([temp,height],(value)=>{
// 从value中获取最新的水温(newTemp)、最新的水位(newHeight)
// 数组解构赋值
let [newTemp,newHeight] = value
// 逻辑
if(newTemp >= 60 || newHeight >= 80){
console.log('给服务器发请求')
}
}) */
// 监视 -- watchEffect实现
watchEffect(()=>{
if(temp.value >= 60 || height.value >= 80){
console.log('给服务器发请求')
}
})
</script>
3.9 标签中的ref属性
为什么要引入ref属性?在vue文件的 template(HTML)部分,可能会用 id 表示一个标签的唯一性。但是打包合并文件的时候,不同vue文件的HTML会合并到一起,如果多个组件用了同一个id名字(例如<div id="title">哈哈</div>
)会有冲突。所以要用ref属性。
用法:
① 引入import {ref} from 'vue'
② 通过 ref 创建一个东西, 用于存储其标记的内容 let title = ref()
③ HTML中 <h2 ref="title">北京</h2>
作用:
用于注册模板引用。
① 用在普通DOM
标签上,获取的是DOM
节点。
② 用在组件标签上,获取的是组件实例对象。
示例:
- 用在普通
DOM
标签上,获取的是DOM
节点
<template>
<div class="person">
<h1>中国</h1>
<h2 ref="title2">北京</h2>
<h3>尚硅谷</h3>
<button @click="showLog">点我输出h2这个元素</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref} from 'vue'
// 创建一个title2,用于存储ref标记的内容
let title2 = ref()
console.log(title2);
function showLog(){
console.log(title2.value) // <h2 data-v-4cadc14e>北京</h2>
console.log(title2.value.innerHTML) // 北京
}
</script>
控制台输出:<h2 data-v-4cadc14e>北京</h2>,输出的是DOM节点
注意这里的 data-v-4cadc14e 是因为 style 上加了 scoped 局部样式
- ref 用在组件标签上,获取的是组件实例对象。
注意 ref 如果用在组件标签上,肯定是父标签在其文件中使用,因为父标签中才会使用子组件。因此 父组件获取了子组件的实例对象。但是不能轻易获得子组件里面的数据,必须是子组件通过defineExpose暴露的数据才可以访问。
补充:vue3 中父组件中,只需① 引入组件(import)② 使用,不需要注册。因为是在setup中写的代码。
<!-- 父组件App.vue -->
<template>
<Person ref="ren"/>
<button @click="test">测试</button>
</template>
<script lang="ts" setup name="App">
import Person from './components/Person.vue'
import {ref} from 'vue'
let ren = ref()
function test(){
console.log(ren.value.name)
console.log(ren.value.age)
}
</script>
<!-- 子组件Person.vue中要使用defineExpose暴露内容 -->
<script lang="ts" setup name="Person">
import {ref,defineExpose} from 'vue'
// 数据
let name = ref('张三')
let age = ref(18)
/****************************/
/****************************/
// 使用defineExpose将组件中的数据交给外部
defineExpose({name,age})
</script>
回顾TS
引入的时候,如果要引入这个: /types/index.js ,只用写到 /types,里面的 index.js会自动补上
vue文件路径 @
@ 表示在 src 文件夹
- 示例:接口、泛型、自定义类型
/src/types/index.js
// 定义一个接口,用于限制person对象的具体属性
export interface PersonInter {
id: string,
name: string,
age: number
}
// 一个自定义类型
// export type Persons = Array<PersonInter>
export type Persons = PersonInter[]
/src/components/Person.vue
//因为这里不是具体的值 所以要加一个type
import {type PersonInter,type Persons} from '@/types'
// 定义一个person对象,要符合PersonInter接口规范
let person:PersonInter = {id:'1111', name:'张三',age:60}
console.log(person);
// 定义一个数组,它里面的每一项都符合PersonInter规范
let personList:Array<PersonInter> = [
{id:'1111', name:'张三',age:25},
{id:'2222', name:'李四',age:20},
{id:'3333', name:'王五',age:30}
]
let personList2:Persons = [
{id:'1111', name:'张三',age:25},
{id:'2222', name:'李四',age:20},
{id:'3333', name:'王五',age:30}
]
3.10 props使用
父组件要把数据给子组件
reactive和泛型一起用
import {reactive} from 'vue'
import {type Persons} from '@/types'
let personList = reactive<Persons>([
{id:'1111', name:'张三',age:25},
{id:'2222', name:'李四',age:20},
{id:'3333', name:'王五',age:30}
])
示例
父组件要把数据给子组件
- 接口
// 定义一个接口,限制每个Person对象的格式
export interface PersonInter {
id:string,
name:string,
age:number
}
// 定义一个自定义类型Persons
export type Persons = Array<PersonInter>
App.vue
(父组件)中代码:
<template>
<Person :list="persons"/>
</template>
<script lang="ts" setup name="App">
import Person from './components/Person.vue'
import {reactive} from 'vue'
import {type Persons} from './types'
let persons = reactive<Persons>([
{id:'e98219e12',name:'张三',age:18},
{id:'e98219e13',name:'李四',age:19},
{id:'e98219e14',name:'王五',age:20}
])
</script>
Person.vue
中代码:
<template>
<div class="person">
<ul>
<li v-for="item in list" :key="item.id">
{{item.name}}--{{item.age}}
</li>
</ul>
</div>
</template>
<script lang="ts" setup name="Person">
import {defineProps} from 'vue'
import {type PersonInter} from '@/types'
// 第一种写法:接收。defineProps返回值是一个对象
// const props = defineProps(['list'])
// 第二种写法:接收+限制类型
// defineProps<{list:Persons}>()
// 写法是,defineProps可以传入一个泛型,泛型里面是规定传入数据的类型的,因为父组件不一定只传递一个数据,所以用对象传递
// 第三种写法:接收+限制类型+指定默认值+限制必要性
let props = withDefaults(defineProps<{list?:Persons}>(),{
list:()=>[{id:'asdasg01',name:'小猪佩奇',age:18}]
})
console.log(props)
</script>
- props 用于父组件给子组件传递数据
父组件:
<Son str="helloworld" :list="list"></Son>
这里传递的两个数据是不一样的 str实为一个字符串,list通过:也就是v-bind绑定了数据
子组件中接收数据:
通过defineProps,在模板中可以直接使用
但是在script中不能使用,如果想用的话, 就需要定义变量接收。defineProps返回值是一个对象,对象的每一个元素都是父元素传递来的数据
<script>
import {defineProps} from 'vue' // 引入——其实可以不引入
defineProps(['list'])
// 通过defineProps接收数据
const father_data = defineProps(['list'])
<template>
{{list}}
</template>
因为子组件在接收父组件的数据时,想做一些限制,比如限制类型,指定默认值,限制数据传递必要性,所以引入了第二种第三种写法
- 第二种写法补充说明
defineProps<{list:Persons}>()
。写法是,defineProps可以传入一个泛型,泛型里面是规定传入数据的类型的,因为父组件不一定只传递一个数据,所以用对象限制类型
- 子组件中如果想表明 父组件的数据可传可不传,就要加一个问号
defineProps<{list ? :Persons}>()
- 第三种写法补充说明
// 第三种写法:接收+限制类型+指定默认值+限制必要性
let props = withDefaults(defineProps<{list?:Persons}>(),{
list:()=>[{id:'asdasg01',name:'小猪佩奇',age:18}]
})
withDefaults配置默认值,withDefaults 是一个函数。
第一个参数defineProps获取父组件传递的数据(这里用问号设置为可传可不传递,用泛型限制了传递过来的数据的类型);第二个参数是对象,设置每个属性的默认值,要用函数返回(function xxx(){return yyy}
简写为()=> yyy
)
- defineProps 和 withDefaults都可以不用引入。
definexxx就是宏函数,自动引入
3.11 生命周期
-
概念:
Vue
组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue
会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子 -
规律:
生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。
-
Vue2
的生命周期创建阶段:
beforeCreate
、created
(创建前、创建完毕)挂载阶段:
beforeMount
、mounted
(挂载前、挂载完毕)更新阶段:
beforeUpdate
、updated
(更新前、更新完毕)销毁阶段:
beforeDestroy
、destroyed
(销毁前、销毁完毕) -
Vue3
的生命周期创建阶段:
setup
挂载阶段:
onBeforeMount
、onMounted
更新阶段:
onBeforeUpdate
、onUpdated
卸载阶段:
onBeforeUnmount
、onUnmounted
-
常用的钩子:
onMounted
(挂载完毕)、onUpdated
(更新完毕)、onBeforeUnmount
(卸载之前)
<template>
<div class="person">
<h2>当前求和为:{{ sum }}</h2>
<button @click="changeSum">点我sum+1</button>
</div>
</template>
<!-- vue3写法 -->
<script lang="ts" setup name="Person">
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
// 数据
let sum = ref(0)
// 方法
function changeSum() {
sum.value += 1
}
console.log('setup')
// 生命周期钩子
// vue3在 挂载前,调用 onBeforeMount【里面的函数】
onBeforeMount(()=>{
console.log('挂载之前')
})
onMounted(()=>{
console.log('挂载完毕')
})
onBeforeUpdate(()=>{
console.log('更新之前')
})
onUpdated(()=>{
console.log('更新完毕')
})
onBeforeUnmount(()=>{
console.log('卸载之前')
})
onUnmounted(()=>{
console.log('卸载完毕')
})
</script>
子组件先挂载=>父组件再挂载, 从入口文件开始分析。App组件是最后挂载的
3.12 自定义hooks
什么是hook
?—— 本质是一个函数,把setup
函数中使用的Composition API
进行了封装,类似于vue2.x
中的mixin
。
自定义hook
的优势:复用代码, 让setup
中的逻辑更清楚易懂。
**使用:**src文件夹下新建一个hooks文件夹,里面根据功能创建文件 useDog.ts useSum.ts。ts文件向外暴露函数(hook本质是函数),而且函数内要返回值。
示例:
- useDog.ts
import axios from 'axios'
import {reactive} from 'vue'
export default function (){
// 数据
let dogList = reactive([
'https://images.dog.ceo/breeds/pembroke/n02113023_7292.jpg'
])
// 方法
async function getDog() {
try {
let res = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
// console.log(res.data.message);
dogList.push(res.data.message)
} catch(err) {
console.log(err);
}
}
//给外部提供东西
return {dogList,getDog}
}
- useSum.ts
import {ref} from 'vue'
export default function(){
let sum = ref(0)
function add() {
sum.value += 1
}
//给外部提供东西
return {sum,add}
}
- 组件中使用
<template>
<div class="person">
<h2>当前求和为:{{ sum }}</h2>
<button @click="add">点我加一</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 useSum from '@/hooks/useSum'
import useDog from '@/hooks/useDog'
const {sum,add} = useSum()
const{dogList,getDog} = useDog()
</script>