Vue3初步
1.搭建Vue3工程
1.1 使用Vue CLI
安装Vue CLI(如果你还没有安装的话):
npm install -g @vue/cli
# 或者使用yarn
yarn global add @vue/cli
创建Vue3项目:
vue create my-vue3-project
进入项目目录:
cd my-vue3-project
运行项目:
npm run serve
# 或者使用yarn
yarn serve
1.2 基于Vite
安装Vite(如果你还没有安装的话):
npm create vite@latest my-vue3-project -- --template vue
# 或者使用yarn
yarn create vite my-vue3-project -- --template vue
进入项目目录:
cd my-vue3-project
安装依赖:
npm install
# 或者使用yarn
yarn install
运行项目:
npm run dev
# 或者使用yarn
yarn dev
2.setup
2.1 基本概念
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',
setup(){
// 数据,原来写在data中(注意:此时的name、age、tel数据都不是响应式数据)
let name = '张三'
let age = 18
let tel = '13888888888'
// 方法,原来写在methods中
function changeName(){
name = 'zhang-san' //注意:此时这么修改name页面是不变化的
console.log(name)
}
function changeAge(){
age += 1 //注意:此时这么修改age页面是不变化的
console.log(age)
}
function showTel(){
alert(tel)
}
// 返回一个对象,对象中的内容,模板中可以直接使用
return {name,age,tel,changeName,changeAge,showTel}
}
}
</script>
2.2 setup两种返回值
- 若返回一个对象:则对象中的:属性、方法等,在模板中均可以直接使用。
- 若返回一个渲染函数:则可以自定义渲染内容(此时
template中的内容被渲染函数取代)。
比如,现在有以下代码:
<template>
<div>
111
</div>
</template>
<script>
import {h} from "vue";
export default {
name: "setupComps",
setup(){
return ()=>h('h1',"jack")
}
}
</script>
上面的渲染函数会完全替代原来模板中的内容。所以界面如下:

2.3 vue3 setup和vue2 optionApi的关系
vue2 optionApi和vue3 setup可以并存(但建议不要这样子写)vue2 optionApi可以访问vue3 setup中的属性方法vue3 setup不可以访问vue2 optionApi中的配置(data,methods…)
因为setup执行的时机,beforeCreate之前。(注:setup中,this值为undefined)vue3 setup优先级比vue2 optionApi优先级更高
2.4 vue3 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 lang="ts">
export default {
name:'Person',
}
</script>
<!-- 下面的写法是setup语法糖 -->
<script setup lang="ts">
console.log(this) //undefined
// 数据(注意:此时的name、age、tel都不是响应式数据)
let name = '张三'
let age = 18
let tel = '13888888888'
// 方法
function changName(){
name = '李四'//注意:此时这么修改name页面是不变化的
}
function changAge(){
console.log(age)
age += 1 //注意:此时这么修改age页面是不变化的
}
function showTel(){
alert(tel)
}
</script>
2.5 setup中如何获得组件实例?
setup函数在组件的生命周期中非常早的时候被调用,具体来说是在beforeCreate和created生命周期钩子之前。由于 setup 函数在组件实例被完全创建之前调用,因此在setup函数内部直接访问组件实例(this)是不可能的。Vue3 提供了其他方式来访问或操作通常与组件实例相关的功能。
(1) Props
setup函数接收两个参数,第一个是props,它包含了传递给组件的属性。
setup(props) {
console.log(props.someProp); // 访问传递的属性
}
(2) Context
setup函数的第二个参数是一个context对象,它提供了一些与组件实例相关的属性和方法,如attrs、slots、emit等。
setup(props, { attrs, slots, emit, expose }) {
// 你可以使用 attrs 访问未在 props 中声明的属性
// 使用 slots 访问插槽
// 使用 emit 触发事件
// 使用 expose 暴露属性或方法给模板或其他组件
}
(3) 访问组件实例
虽然不推荐在setup中直接访问组件实例,但如果你确实有这样的需求,可以通过getCurrentInstance函数来实现。这个函数返回一个代表当前组件实例的对象。
import { getCurrentInstance } from 'vue';
setup() {
const instance = getCurrentInstance();
if (instance) {
// 你可以通过 instance.proxy 访问到类似于 Vue 2 中 this 的代理对象
console.log(instance.proxy.$el); // 访问组件的根 DOM 元素(仅在挂载后可用)
}
}
总的来说,Vue3的setup函数设计使得你能够在不直接依赖组件实例的情况下编写更加函数式和可复用的代码。大多数情况下,你应该能够通过props和context参数以及Composition API提供的其他函数来满足你的需求。
3.定义响应式数据
3.1 ref
注意:vue2中的ref原有的功能(获取元素或组件实例对象)依然可以在vue3使用。
(1) ref创建基本类型的响应式数据
返回值是一个RefImpl的实例对象,简称ref对象或ref,ref对象的value属性是响应式的。

(2) 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>
import {ref} from 'vue'
export default {
name: "refComps2",
setup() {
// 数据
let car = ref({ brand: '奔驰', price: 100 })
let games = ref([
{ id: 'ahsgdyfa01', name: '英雄联盟' },
{ id: 'ahsgdyfa02', name: '王者荣耀' },
{ id: 'ahsgdyfa03', name: '原神' }
])
let obj = ref({
a:{
b:{
c:{
d:666
}
}
}
})
console.log(car)
function changeCarPrice() {
car.value.price += 10
}
function changeFirstGame() {
games.value[0].name = '流星蝴蝶剑'
// games.value[0] = {id: 'ahsgdyfa01', name: '流星蝴蝶剑' }//这个也有响应式
}
function test(){
obj.value.a.b.c.d = 999
}
return{
car,
games,
obj,
changeCarPrice,
changeFirstGame,
test
}
}
}
</script>
3.2 reactive(创建对象类型的响应式数据)
reactive可以返回一个Proxy的实例对象,简称响应式对象。
ps:reactive定义的响应式数据是“深层次”的。并且可以处理数组(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>
import { reactive } from 'vue'
export default {
name: "reactiveComps",
setup(){
let car = reactive({ brand: '奔驰', price: 100 })
let games = reactive([
{ id: 'ahsgdyfa01', name: '英雄联盟' },
{ id: 'ahsgdyfa02', name: '王者荣耀' },
{ id: 'ahsgdyfa03', name: '原神' }
])
let obj = reactive({
a:{
b:{
c:{
d:666
}
}
}
})
console.log(obj)
function changeCarPrice() {
car.price += 10
}
function changeFirstGame() {
games[0].name = '流星蝴蝶剑'
// games[0] = {id: 'ahsgdyfa01', name: '流星蝴蝶剑' }//这个也有响应式
}
function test(){
obj.a.b.c.d = 999
}
return{
car,
games,
obj,
changeCarPrice,
changeFirstGame,
test
}
}
}
</script>
<style scoped>
</style>
3.3 ref VS reactive
- 定义数据上
ref可以用来定义基本数据类型和对象数据类型;reactive只能定义对象数据类型。 - 原理上
ref是通过Object.defineProperty的set、get方法实现响应式;reactive是通过proxy的get、set、deleteProperty。 - 使用上
ref创建的变量必须使用.value(可以使用volar插件自动添加.value)。reactive则不需要。 - 响应式
reactive在重新分配一个新对象、将对象传入函数时候、解构时候会失去响应式。
ref在解构时候会失去响应式;在重新分配一个新对象、将对象传入函数时候不会失去响应式。关于响应式下面会仔细分析这一块。
3.4 vue3中proxy和vue2中Object.defineProperty的区别?
(1) vue2响应式原理
原理:
对象采用Object.defineProperty()对数据的读取、修改进行拦截(数据劫持);数组则是采用重写数组的一系列方法来实现拦截。
弊端:
对象新增、删除属性,界面不会更新;直接通过下标修改数组,界面不会更新。
let person={
age:11,
name:"jack"
}
let p={}
for (let key in person){
Object.defineProperty(p,key,{
get(){
return person[key];
},
set(val){
console.log(`p中的${key}被修改,我发现了,需要去更改界面了`);
person[key]=val;
}
})
}
(2) vue3响应式原理
通过es6的proxy,通过内部的get、set、deleteProperty方法拦截对象任意属性的添加、修改、删除等操作;通过reflect反射对源对象的属性进行操作。
let person={
age:11,
name:"jack"
}
const p=new Proxy(person,{
get(target, propName) {
return Reflect.get(target,propName);
},
set(target, propName, newValue) {
console.log(`p中的${propName}被修改或者添加了,我发现了,需要去更改界面了`);
Reflect.set(target,propName,newValue);
},
deleteProperty(target, propName) {
console.log(`p中的${propName}被删除了,我发现了,需要去更改界面了`);
return Reflect.deleteProperty(target,propName);
}
})
3.5 vue3什么情况会失去响应式?
(1) 使用reactive定义的数据重新赋值
<template>
<h1>{{ foo.a }}</h1>
<h1>{{ bar.a }}</h1>
<button @click="handleClick">点我</button>
</template>
<script setup>
import { ref, reactive } from 'vue'
let foo = ref({ a: 1, b: 2, c: 3 })
let bar= reactive({ a: 1, b: 2, c: 3 })
const handleClick = () => {
foo.value = {
a: 11
}
bar= {
a: 99
} //失去响应式
}
</script>
使用ref定义的数据重新赋值不会失去响应式,使用reactive定义的数据重新赋值不会失去响应式。
原因如下:
ref定义数据(包括对象)时,都会变成RefImpl(Ref引用对象) 类的实例,无论是修改还是重新赋值都会调用setter,都会经过reactive方法处理为响应式对象。reactive定义数据(必须是对象),是直接调用reactive方法处理成响应式对象。如果重新赋值,就会丢失原来响应式对象的引用地址,变成一个新的引用地址,这个新的引用地址指向的对象是没有经过reactive方法处理的,所以是一个普通对象,而不是响应式对象。解构同理。
下面举个例子:
<script setup>
import { reactive } from 'vue';
// 定义一个响应式变量
const data = reactive({
name: "",
age: ""
});
// 请求接口
axios.get('/api/data')
.then(res => {
// 直接赋值
data = res.data;//失去响应
})
.catch(err => console.log(err));
</script>
下面来分析分析reactive定义的数据重新赋值时候的解决方法:
逐个属性进行赋值(这个不推荐):
<script setup>
import { reactive } from 'vue';
// 定义一个响应式变量
const data = reactive ({
name:"",
age:""
}});
// 请求接口
axios.get('/api/data')
.then(res => {
// 逐个属性赋值 不推荐
//这样子对象的引用地址没变
data.name= res.data.name;
data.age= res.data.age;
})
.catch(err => console.log(err));
</script>
使用Object.assign()(有些情况不适用):
<script setup>
import { reactive } from 'vue';
// 定义一个响应式变量
const data = reactive ({
name:"",
age:""
}});
// 请求接口
axios.get('/api/data')
.then(res => {
//使用 Object.assign 不会丢失响应式
data = Object.assign(data,res.data)
})
.catch(err => console.log(err));
</script>
使用ref()来进行定义(最简单):
<script setup>
import { ref } from 'vue';
// 定义一个响应式变量
const data = ref ({
name:"",
age:""
});
// 请求接口
axios.get('/api/data')
.then(res => {
// 更新响应式变量的值
data.value = res.data;
})
.catch(err => console.log(err));
</script>
直接在reactive中嵌套一层:
<script setup>
import { reactive } from 'vue';
// 定义一个响应式变量
const data = reactive ({
dataObj:{
name:"",
age:""
}
});
// 请求接口
axios.get('/api/data')
.then(res => {
// 嵌套一层 dataObj
data.dataObj= res.data;
})
.catch(err => console.log(err));
</script>
(2) 解构赋值引起响应式数据丢失
在解构赋值中不管是使用reactive定义的变量还是使用ref定义的变量,都会导致数据丢失,因为解构赋值会创建一个新的引用,而不是原始对象。因此,我们应该避免在解构赋值中使用reactive或ref定义的变量,或者使用拷贝,或者toRefs来避免数据丢失。
<script setup>
import { reactive } from 'vue';
// 定义一个响应式变量
const data = reactive({
name: "码农键盘上的梦",
age: "99"
})
// 解构了 响应式也丢了
let { name } = data; //解构赋值
</script>
下面可以看看这种情况的解决方案:
直接访问reactive定义的变量,而不是使用解构赋值;
使用toRefs方法将响应式对象转化为普通对象的响应式属性(很常用)
<script setup>
import { reactive, toRefs } from 'vue'
// 定义一个响应式变量
const data = reactive({
name: "码农键盘上的梦",
age: "99"
})
// 使用toRefs解决
const { name, age } = toRefs(data)
</script>
在解构赋值时使用拷贝来避免数据丢失:
<script setup>
import { reactive, toRefs } from 'vue'
// 定义一个响应式变量
const data = reactive({
name: "码农键盘上的梦",
age: "99"
})
// 使用拷贝解决
const { name: nameCopy, age: ageCopy } = { ...data }
console.log(nameCopy, ageCopy)
</script>
3.6 toRefs vs toRef
toRef和toRefs可以用来复制reactive里面的属性然后转成ref,而且它既保留了响应式,也保留了引用,也就是你从reactive复制过来的属性进行修改后,除了视图会更新,原有reactive里面对应的值也会跟着更新,如果你知道浅拷贝的话那么这个引用就很好理解了,它复制的其实就是引用+响应式ref
区别:
toRef: 复制对象里的单个属性并转成reftoRefs: 复制对象里的所有属性并转成ref
<template>
<div class="person">
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<h2>性别:{{person.gender}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeGender">修改性别</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对象中的n个属性批量取出,且依然保持响应式的能力
let {name,gender} = toRefs(person)
// 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力
let age = toRef(person,'age')
// 方法
function changeName(){
name.value += '~'
}
function changeAge(){
age.value += 1
}
function changeGender(){
gender.value = '女'
}
</script>
3.7 标签的ref属性
vue2和vue3的标签上的ref用法是一样的。唯一不同的是我们不再使用this.$refs来访问DOM元素,而是使用ref()函数返回的引用。
用在普通dom上:
<template>
<div class="person">
<h1 ref="title1" id="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>
import {ref} from 'vue'
export default {
setup() {
let title1 = ref()
let title2 = ref()
let title3 = ref()
function showLog() {
// 通过id获取元素
const t1 = document.getElementById('title1')
// 打印内容
console.log(t1?.innerText)
// 通过ref获取元素
console.log(title1.value)
console.log(title2.value)
console.log(title3.value)
}
return{
title1,
title2,
title3,
showLog
}
}
}
</script>
用在组件标签上:
<!-- 父组件App.vue -->
<template>
<Person ref="ren"/>
<button @click="test">测试</button>
</template>
<script lang="ts" setup name="App">
import Person from './components/Person.vue'
import {ref} from 'vue'
let ren = ref()
function test(){
console.log(ren.value.name)
console.log(ren.value.age)
}
</script>
<!-- 子组件Person.vue中要使用defineExpose暴露内容 -->
<script lang="ts" setup name="Person">
import {ref,defineExpose} from 'vue'
// 数据
let name = ref('张三')
let age = ref(18)
/****************************/
/****************************/
// 使用defineExpose将组件中的数据交给外部
defineExpose({name,age})
</script>
4.computed vs watch
4.1 computed
<template>
<div class="person">
姓:<input type="text" v-model="person.firstName"> <br>
名:<input type="text" v-model="person.lastName"> <br>
全名:<span>{{ person.fullName }}</span> <br>
<button @click="changeFullName">全名改为:li-si</button>
</div>
</template>
<script>
import {ref, computed, reactive} from 'vue'
export default {
setup() {
let person=reactive({
firstName:"zhang",
lastName:"san",
})
// 计算属性——只读取,不修改
/* person.fullName = computed(()=>{
return person.firstName + '-' + person.lastName
}) */
// 计算属性——既读取又修改
person.fullName = computed({
// 读取
get() {
return person.firstName + '-' + person.lastName
},
// 修改
set(val) {
console.log('有人修改了fullName', val)
person.firstName = val.split('-')[0]
person.lastName = val.split('-')[1]
}
})
function changeFullName() {
person.fullName = 'li-si'
}
return{
person,
changeFullName
}
}
}
</script>
4.2 watch
(1) 监听ref创建的基本数据类型
<template>
<div class="person">
<h1>情况一:监视【ref】定义的【基本类型】数据</h1>
<h2>当前求和为:{{ sum }}</h2>
<button @click="changeSum">点我sum+1</button>
</div>
</template>
<script>
import {ref, watch} from 'vue'
export default {
setup() {
// 数据
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()
}
})
return{
sum,
changeSum
}
}
}
</script>
(2) 监听ref创建的对象数据类型
监视ref定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视(和vue2有点像)。
<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>
import {ref, watch} from 'vue'
export default {
setup() {
// 数据
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】定义的【对象类型】数据,监视的是对象的地址值,
若想监视对象内部属性的变化,需要手动开启深度监视
watch的第一个参数是:被监视的数据
watch的第二个参数是:监视的回调
watch的第三个参数是:配置对象(deep、immediate等等.....)
*/
watch(person, (newValue, oldValue) => {
console.log('person变化了', newValue, oldValue)
},{deep: true})
return {
person,
changeName,
changeAge,
changePerson
}
}
}
</script>
(3) 监听reactive创建的对象数据类型
监视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>
import {reactive, watch} from 'vue'
export default {
setup() {
// 数据
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)
})
return{
person,
obj,
changeName,
changeAge,
changePerson,
test
}
}
}
</script>
(4) 监听ref或reactive定义的对象类型数据中的某个属性
这个可以想想之前vue2是怎么做的?

若该属性值不是【对象类型】;需要写成函数形式。若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。
结论:监视的要是对象里的属性,那么最好写函数式。注意点:若是需要关注对象内,需要手动开启深度监视。
<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>
import {reactive, watch} from 'vue'
export default {
setup() {
// 数据
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,(newValue,oldValue)=>{
console.log('person.name变化了',newValue,oldValue)
})
// 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数
watch(() => person.car, (newValue, oldValue) => {
console.log('person.car变化了', newValue, oldValue)
}, {deep: true})
return{
person,
changeName,
changeC1,
changeC2,
changeCar
}
}
}
</script>
(5) 监视上述的多个数据
<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>
import {reactive, watch} from 'vue'
export default {
setup() {
// 数据
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)
//newValue和oldValue都是数组,里面都有两个元素
//newValue中是person.name的新值和person.car的新值
//oldValue中是person.name的新值和person.car的新值
}, {deep: true})
return {
person,
changeName,
changeAge,
changeC1,
changeC2,
changeCar
}
}
}
</script>
(6) watchEffect
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>
import {ref, watch, watchEffect} from 'vue'
export default {
setup() {
// 数据
let temp = ref(0)
let height = ref(0)
// 方法
function changePrice() {
temp.value += 10
}
function changeSum() {
height.value += 1
}
// 用watch实现,需要明确的指出要监视:temp、height
// watch([temp, height], (value) => {
// // 从value中获取最新的temp值、height值
// const [newTemp, newHeight] = value
// // 室温达到50℃,或水位达到20cm,立刻联系服务器
// if (newTemp >= 50 || newHeight >= 20) {
// console.log('联系服务器')
// }
// })
// 用watchEffect实现,不用明确指出监视的数据
const stopWtach = watchEffect(() => {
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if (temp.value >= 50 || height.value >= 20) {
console.log(document.getElementById('demo')?.innerText)
console.log('联系服务器')
}
// 水温达到100,或水位达到50,取消监视
if (temp.value === 100 || height.value === 50) {
console.log('清理了')
stopWtach()
}
})
return{
temp,height,changePrice,changeSum
}
}
}
</script>
问题:watch vs watchEffect
- 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch:要明确指出监视的数据(需要精确控制监听哪些数据)watchEffect:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性,自动追踪所有依赖)。
(7) 总结
Vue3中的watch只能监视以下四种数据:
ref定义的数据。reactive定义的数据。- 函数返回一个值(
getter函数)。 - 一个包含上述内容的数组。
5.props和emit
5.1 props
下面我们看看props在vue3的用法(具体用法查官网):
//types.ts
// 定义一个接口,限制每个Person对象的格式
export interface PersonInter {
id:string,
name:string,
age:number
}
// 定义一个自定义类型Persons
export type Persons = Array<PersonInter>
//propsParentComp.vue
<template>
<propChildComp :list="persons"></propChildComp>
</template>
<script setup lang="ts" name="propsParentComp">
import propChildComp from "@/components/props/propChildComp.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>
//propsChildComp.vue
<template>
<div>
<ul>
<li v-for="item in list" :key="item.id">
{{ item.name }}--{{ item.age }}
</li>
</ul>
</div>
</template>
<script setup lang="ts" name="propsChildComp">
import {defineProps} from 'vue';
import {type Persons} from "./types"
// 第一种写法:仅接收
// defineProps(['list'])
// 第二种写法:接收+限制类型
// defineProps<{list:Persons}>()
// 第三种写法:接收+限制类型+指定默认值+限制必要性
withDefaults(defineProps<{list?:Persons}>(),{
list:()=>[{id:'asdasg01',name:'小猪佩奇',age:18}]
})
</script>
5.2 emit
父组件:
<template>
<ChildComponent @custom-event="handleCustomEvent" />
</template>
<script setup>
import ChildComponent from './ChildComponent.vue';
const handleCustomEvent = (data) => {
console.log('接收到事件:', data); // 输出: 接收到事件: 这是传递给父组件的数据
};
</script>
子组件:
<template>
<button @click="triggerEvent">触发事件</button>
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['custom-event']);
const triggerEvent = () => {
emit('custom-event', '这是传递给父组件的数据');
};
</script>
6.生命周期
6.1 回顾vue2钩子
- 创建阶段:
beforeCreate、created - 挂载阶段:
beforeMount、mounted - 更新阶段:
beforeUpdate、updated - 销毁阶段:
beforeDestroy、destroyed
6.2 vue3钩子
- 创建阶段:
setup - 挂载阶段:
onBeforeMount、onMounted - 更新阶段:
onBeforeUpdate、onUpdated - 卸载阶段:
onBeforeUnmount、onUnmounted
示例代码如下:
<template>
<div class="person">
<h2>当前求和为:{{ sum }}</h2>
<button @click="changeSum">点我sum+1</button>
</div>
</template>
<!-- vue3写法 -->
<script lang="ts" setup name="Hooks">
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
// 数据
let sum = ref(0)
// 方法
function changeSum() {
sum.value += 1
}
console.log('setup')
// 生命周期钩子
onBeforeMount(() => {
console.log('挂载之前')
})
onMounted(() => {
console.log('挂载完毕')
})
onBeforeUpdate(() => {
console.log('更新之前')
})
onUpdated(() => {
console.log('更新完毕')
})
onBeforeUnmount(() => {
console.log('卸载之前')
})
onUnmounted(() => {
console.log('卸载完毕')
})
</script>
7.自定义hook
什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装,类似于vue2.x中的mixin,但又有一些不同。
示例代码:
//useSum.ts
import {ref,onMounted} from 'vue'
export default function(){
let sum = ref(0)
const increment = ()=>{
sum.value += 1
}
const decrement = ()=>{
sum.value -= 1
}
onMounted(()=>{
increment()
})
//向外部暴露数据
return {sum,increment,decrement}
}
//useDog.ts
import {reactive,onMounted} from 'vue'
import axios,{AxiosError} from 'axios'
export default function(){
let dogList = reactive<string[]>([])
// 方法
async function getDog(){
try {
// 发请求
let {data} = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
// 维护数据
dogList.push(data.message)
} catch (error) {
// 处理错误
const err = <AxiosError>error
console.log(err.message)
}
}
// 挂载钩子
onMounted(()=>{
getDog()
})
//向外部暴露数据
return {dogList,getDog}
}
//组件中具体使用
<template>
<h2>当前求和为:{{sum}}</h2>
<button @click="increment">点我+1</button>
<button @click="decrement">点我-1</button>
<hr>
<img v-for="(u,index) in dogList.urlList" :key="index" :src="(u as string)">
<span v-show="dogList.isLoading">加载中......</span><br>
<button @click="getDog">再来一只狗</button>
</template>
<script lang="ts">
import {defineComponent} from 'vue'
export default defineComponent({
name:'App',
})
</script>
<script setup lang="ts">
import useSum from './hooks/useSum'
import useDog from './hooks/useDog'
let {sum,increment,decrement} = useSum()
let {dogList,getDog} = useDog()
</script>
8.路由
8.1 基本使用
我在https://blog.csdn.net/fageaaa/article/details/145480312有讲解过这部分。
8.2 路由器工作模式
hash模式:
const router = createRouter({
history:createWebHashHistory(), //hash模式
/******/
})
history模式:
const router = createRouter({
history:createWebHistory(), //history模式
/******/
})
9.状态管理pinia
我在https://blog.csdn.net/fageaaa/article/details/145480312有讲解过这部分。下面再来简单谈谈这方面。
9.1 搭建pinia 环境
第一步:npm install pinia
第二步:操作src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
/* 引入createPinia,用于创建pinia */
import { createPinia } from 'pinia'
/* 创建pinia */
const pinia = createPinia()
createApp(App).use(router).use(pinia).mount('#app')
9.2 存储+读取数据
它有三个概念:state、getter、action,相当于组件中的: data、 computed 和 methods。Pinia没有mutations,因为它采用了actions来替代mutations的功能,并且支持同步和异步操作。没有modules,因为它本身就可以分模块。
9.3 修改数据(三种修改方式)
直接修改:
countStore.sum = 666
批量修改:
countStore.$patch({
sum:999,
school:'aaaa'
})
借助action修改:
// 使用countStore
const countStore = useCountStore()
// 调用对应action
countStore.incrementOdd(n.value)
9.4 storeToRefs
(1) storeToRefs是什么
toreToRefs是专门用于转换Pinia的store对象。它能够将Pinia中的store对象中的状态转换为具有.value的ref对象集合,通过.value访问每个属性的值。
注意:pinia提供的storeToRefs只会将数据(包含state、getters)做转换,而Vue的toRefs会转换store中的数据+方法(包含store、getters、actions)。
<template>
<div class="count">
<h2>当前求和为:{{sum}}</h2>
</div>
</template>
<script setup lang="ts" name="Count">
import { useCountStore } from '@/store/count'
/* 引入storeToRefs */
import { storeToRefs } from 'pinia'
/* 得到countStore */
const countStore = useCountStore()
/* 使用storeToRefs转换countStore,随后解构 */
const {sum} = storeToRefs(countStore)
</script>
(2) storeToRefs和toRefs有什么区别
storeToRefs和toRefs都可以将状态对象转换为具有.value的ref对象集合。区别在于storeToRefs是针对pinia的store对象,而toRefs是Vue3中的通用函数,用于处理任意的响应式对象。所以使用 storeToRefs需要引入pinia,而toRefs可以在Vue3中直接使用。
9.5 getters
// 引入defineStore用于创建store
import {defineStore} from 'pinia'
// 定义并暴露一个store
export const useCountStore = defineStore('count',{
// 动作
actions:{
/************/
},
// 状态
state(){
return {
sum:1,
school:'atguigu'
}
},
// 计算
getters:{
bigSum:(state):number => state.sum *10,
upperSchool():string{
return this. school.toUpperCase()
}
}
})
9.6 $subscribe
通过store的 $subscribe() 方法侦听 state 及其变化。mutation是记录state变化信息的对象,state是store的state对象。
sumStore.$subscribe((mutate,state)=>{
console.log('111',mutate,state)
})
9.7 基本示例
下面是pinia在vue文件中使用的一个示例:
<template>
<div class="main">
<h2>main: {{ counterStore.counter }}-{{ counterStore.doubleCounter }}</h2>
<button @click="changeCounter">修改counter</button>
</div>
</template>
<script setup lang="ts">
import useCounterStore from '@/store/counter'
const counterStore = useCounterStore()
function changeCounter() {
counterStore.changeCounterAction(999)
}
</script>
<style lang="less" scoped>
.main {
color: red;
}
</style>
9.8 模块化示例
创建一个 store 文件夹,并在其中创建模块化的store文件。以下是一个示例目录结构:
src/
├── store/
│ ├── index.ts
│ └── modules/
│ ├── user.ts
│ └── shoppingCart.ts
└── main.ts
store/index.ts这个文件用于实例化 Pinia 并统一导出所有模块下的 store:
import { createPinia } from 'pinia';
import * as modules from './modules';
const pinia = createPinia();
// 统一导出所有模块的 store
export const useUserStore = modules.user.useUserStore;
export const useCartStore = modules.shoppingCart.useCartStore;
// 或者你可以使用 export * from './modules'; 然后在模块中分别导出
// export * from './modules/user';
// export * from './modules/shoppingCart';
export default pinia;
注意:在上面的代码中,我们使用了import * as modules from './modules'; 这行代码来导入所有模块。但是,这需要你在每个模块文件中都使用 export语句来导出你的 store。另一种方法是直接在 index.ts 文件中使用 export * from './modules/user'; 和 export * from './modules/shoppingCart'; 来分别导出。
store/modules/user.ts,这个文件定义了用户信息的 store:
import { defineStore } from 'pinia';
export const useUserStore = defineStore('User', {
state: () => ({
loginForm: {},
// 其他用户状态
}),
getters: {
// 定义 getter 方法
},
actions: {
// 定义 action 方法
},
});
store/modules/shoppingCart.ts这个文件定义了购物车信息的 store:
import { defineStore } from 'pinia';
export const useCartStore = defineStore('ShopCart', {
state: () => ({
// 购物车状态
}),
getters: {
// 定义 getter 方法
},
actions: {
// 定义 action 方法
},
});
在 main.ts 中配置 Pinia:
import { createApp } from 'vue';
import App from './App.vue';
import { pinia } from './store'; // 导入 Pinia 实例
const app = createApp(App);
app.use(pinia); // 使用 Pinia
app.mount('#app');
现在,你可以在你的 Vue 组件中使用这些模块化的 store 了。以下是一个示例:
<template>
<div>
<!-- 使用用户 store 中的数据 -->
<p>{{ user.loginForm }}</p>
<!-- 使用购物车 store 中的数据或方法 -->
<!-- ... -->
</div>
</template>
<script setup>
import { useUserStore } from '@/store'; // 引入用户 store
const user = useUserStore(); // 使用用户 store
// 你可以在这里调用 store 中的方法或访问状态
</script>
这样子,就可以在 Vue 项目中使用 Pinia 进行模块化的状态管理了。
10 组件通信
这个我将专门花写一篇博客讲述这部分的内容。
11 vue3新组件
11.1 fragment组件
Fragment是一种不会在最终渲染结果中创建新的DOM元素的组件。在vue2必须要有根标签,vue3中可以没有根标签,内部会将多个标签包在一个Fragment虚拟元素中。Fragment组件不需要我们去调用,他自己默认存在。
好处:减少标签层级,减少内存占用。
11.2 Teleport组件
Teleport是一种能够将我们的组件html结构移动到指定位置的技术。
(1) 基本用法
<template>
<div>
<Teleport to="body">
<div>这部分内容将被渲染到 body 元素中</div>
</Teleport>
</div>
</template>
在上面的代码中,<Teleport>标签包裹了一个<div>元素,通过to属性指定将这个<div>渲染到body元素中。
(2) 常见应用
模态框(Modal):
当创建一个模态框时,通常希望它独立于页面的其他内容,并且覆盖在整个页面之上。使用Teleport可以将模态框的内容直接渲染到body元素中,确保它在页面层级中处于较高的位置,不受其他组件的布局影响。
<template>
<div>
<button @click="showModal = true">打开模态框</button>
<Teleport to="body" v-if="showModal">
<div class="modal">
<h2>模态框标题</h2>
<p>这是模态框的内容。</p>
<button @click="showModal = false">关闭</button>
</div>
</Teleport>
</div>
</template>
<script>
export default {
data() {
return {
showModal: false
};
}
};
</script>
<style>
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border-radius: 5px;
}
</style>
与第三方库集成:
某些第三方库可能需要特定的DOM结构或位置来正确显示其内容。通过 Teleport,可以将与这些库相关的组件渲染到合适的位置,以确保它们能够正常工作。
例如,假设我们使用一个第三方的地图库,它要求地图容器必须在页面的特定位置。我们可以使用 Teleport将地图组件渲染到该位置。
<template>
<div>
<Teleport to="#map-container">
<div id="map">这是地图组件</div>
</Teleport>
</div>
</template>
在页面的其他地方,我们有一个<div>元素作为地图的容器:
<div id="map-container"></div>
全局通知组件:
全局通知通常需要在页面的顶部或特定位置显示,以便用户能够立即注意到。使用Teleport可以将通知组件渲染到一个固定的位置,而不受页面其他部分布局的影响。
<template>
<div>
<button @click="showNotification = true">显示通知</button>
<Teleport to="#notification-container" v-if="showNotification">
<div class="notification">重要通知!</div>
</Teleport>
</div>
</template>
在页面中设置通知容器:
<div id="notification-container"></div>
11.3 Suspense组件
等待异步组件时渲染一些额外内容,让应用有更好的用户体验。(类似于骨架屏原理)
使用步骤:
- 异步引入组件
- 使用
Suspense包裹组件,并配置好default与fallback
import { defineAsyncComponent,Suspense } from "vue";
const Child = defineAsyncComponent(()=>import('./Child.vue'));
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中.......</h3>
</template>
</Suspense>
</div>
</template>
这个有点类似于异步组件的处理加载状态功能。详情见我写的文章https://blog.csdn.net/fageaaa/article/details/145892175中的2.4。
12 总结
12.1 基本新知识
- setup语法糖
- 定义响应式数据
- 生命周期钩子
- 常见的API:
生命周期钩子
setup()、onBeforeMount()、onMount()、onBeforeUpdate()、onUpdated()、onBeforeUnMount()、onUnMounted()、onErrorCaptured()、onActivated()、onDeactivated()
响应式数据:
ref()、reactive()、toRef()、toRefs()
其它:
watch()、computed()、watchEffect()、provide、inject - 路由使用方法(与vue2有些不同)
- 状态管理工具使用方法(vue3使用
pinia,vue2推荐用vuex) - 组件通信
- 自定义hook
- vue中新组件:
Fragment、Teleport、Suspense
12.2 补充
(1) vue3中自定义指令
vue3自定义指令中的钩子函数有created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount和unmounted,与vue2有些不同。
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode) {}
}
下面是vue3中懒加载自定义指令的示例:
app.directive('lazyload', {
created(el, binding) {
el.setAttribute('data-src', binding.value)
},
mounted(el) {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
const dataSrc = img.getAttribute('data-src')
if (dataSrc) {
img.src = dataSrc
observer.unobserve(img)
}
}
})
})
observer.observe(el)
}
})
<template>
<div class="container">
<img v-lazyload="'https://picsum.photos/200/300'" class="lazy-image" alt="Lazy loaded image">
<img
v-for="id in 20"
:key="id"
v-lazyload="`https://picsum.photos/200/300?random=${id}`"
alt="随机图片"
class="lazy-image"
>
</div>
</template>
<style scoped>
.container {
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
.lazy-image {
width: 100%;
height: 300px;
object-fit: cover;
background-color: #f5f5f5;
/* 加载前的背景色 */
transition: opacity 0.3s;
}
}
</style>
(2) 自定义过滤器(vue3不支持过滤器了)
可以通过自定义hooks来完成过滤器的作用。
(3) mixin
Vue3中仍然支持mixins,但官方推荐使用组合式API(Composition API)作为更好的替代方案。
(4) nexttick
<template>
<div>
<button @click="updateMessage">更新消息</button>
<p ref="messageElement">{{ message }}</p>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue';
const message = ref('Hello Vue 3');
const messageElement = ref(null); // 使用ref来引用DOM元素
function updateMessage() {
message.value = 'Hello again!';
nextTick(() => {
if (messageElement.value) { // 确保元素已经挂载并可用
messageElement.value.scrollIntoView({ behavior: 'smooth' });
}
});
}
</script>
(5) v-model
用在组件上(vue2):
v-model只能使用一次- 默认传递
value属性,接受input事件 - 默认传递的属性和事件可通过
model选项进行修改
用在组件上(vue3):
v-model只能使用一次,但可以在v-model后添加参数可使用多次- 默认传递
modelValue属性(默认绑定的不是value),接受update:modelValue事件(接收的方法不是input);当在v-model后添加参数时如v-model:myPropName,则传递为myPropName属性,接受update:myPropName事件 - 默认传递的属性和事件无法修改,必须是
modelValue和update:modelValue
(6) 插件
与vue2差别不大:
创建插件文件:
// myPlugin.js
export default {
install(app, options) {
// 1. 添加全局方法或属性
app.config.globalProperties.$myGlobalMethod = function () {
// 一些逻辑
console.log('这是全局方法');
};
// 2. 添加实例方法
app.config.globalProperties.$myInstanceMethod = function () {
// 一些逻辑
console.log('这是实例方法');
};
// 3. 注入组件选项
app.component('MyComponent', {
// ... 选项 ...
});
// 4. 注入一个指令
app.directive('my-directive', {
// ... 钩子函数 ...
});
// 5. 通过传入的选项做一些事情(可选)
if (options) {
console.log('Options:', options);
}
}
};
在Vue应用中使用插件
// main.js 或其他入口文件
import { createApp } from 'vue';
import App from './App.vue';
import MyPlugin from './plugins/myPlugin'; // 确保路径正确
const app = createApp(App);
// 使用插件(可以传递选项)
app.use(MyPlugin, { someOption: true });
app.mount('#app');


2443

被折叠的 条评论
为什么被折叠?



