前端技术日新月异的今天,Vue 3以其强大的组合式API(Composition API)、TypeScript的静态类型检查能力以及Vite的极速开发体验,成为了构建现代Web应用的热门选择。结合Element Plus这一为Vue 3设计的UI组件库,我们可以快速搭建出既美观又高效的前端项目。本文将带你一步步从零开始,使用Vue 3、TypeScript、Vite和Element Plus搭建一个基本的Web项目。
一、环境准备
1、安装Node.js。运行node -v
和npm -v
来检查Node.js和npm是否已安装及版本信息
2 、2 傻瓜式起项目就不多说了,官网说得很清楚: https://cn.vitejs.dev/
由此可知webpack老大哥不管你用不用看不看都给你从路由—>模块—>处理全部加载处理完成出来,当项目资源堆积起来越来越多的时候就会变得非常非常慢。而vite项目一启动直接Server ready,从入口文件一进来看你从什么路由进来,vite就给加载当前路由链路的资源,有点按需加载 懒加载那意思。
3、安装Element Plus: npm install element-plus --save
4、配置下我们的main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import App from './App.vue'
import router from './router'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { size: 'small', zIndex: 3000 })
// 全局注册所有图标组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')
5、举例:建房子
import { createApp } from 'vue':可以看作是房子的地基
import App from './App.vue':房子的主体构造,是一个组件(根)
import { createPinia } from 'pinia':把房子的主体构造存储思想全局共享给每个房子部位,pinia 和 vuex 具有相同的功效, 是 Vue 的存储库,它允许您跨组件/页面共享状态。设计使用的是 Composition api,更符合vue3的设计思维。
import router from './router':房子的楼梯,路,门口标识,
app.mount('#app'):将房子的主体构造放到地基上
6、安装Element Plus
https://element-plus.org/zh-CN/
7、安装router: npm install vue-router --save
二、optionsAPl与CompositionAPl
optionsAPl配置项API:当想要完成某功能,功能A和功能B都得在 data、methods 、
computed分别拆散去写,不利于维护和复用,可读性弱。
CompositionAPl组合式API:集中式的风格,Composition API主要围绕setup
函数展开,将相关逻辑集中在一起,不再分散于不同的Vue选项。这使得维护和理解组件变得更加容易,代码复用,类型支持非常适合使用TypeScript,因为它从根本上是函数式的,易于集成类型系统
1
、什么是setup?
setup
函数是 Vue 3 组合式 API 的入口点。当一个组件实例被创建时,setup
函数会在组件被解析之前(以及 data
、 computed
和 methods
被处理之前)被调用。它是处理组件逻辑的主要地方,返回的数据和方法可以直接在模板中使用。
setup特性其在created之前执行,内部无this。以前放在data里面的 直接可以在setup里面定义变量,方法和数据需要return交出去。放在methods里面的直接可以在setup里面写一个个的函数。
setup的返回值也可以对象也可以是渲染函数。
setup语法和optionsAPl语法可以共存,但是setup语法不能读optionsAPl语法里面的东西,而optionsAPl语法可以读到setup语法的东西使用。(不建议混写,太混乱了)
2、setup语法糖
它是组件内使用 Composition API 的入口点。setup
函数在组件实例被创建时执行,此时 data
、computed
、methods
和 生命周期钩子
还未被初始化。
插件安装:npm i vite-plugin-vue-setup-extend
vite.config.ts引入setup插件
setup
函数的主要作用:
-
定义组件的
data
、computed
、methods
等,并不再需要return
返回这些选项。 -
通过
ref
或reactive
API 来响应式地管理数据。 -
使用
watch
或watchEffect
来监听响应式数据的变化。 -
使用
provide
和inject
实现组件之间的通信。 -
使用
onMounted
,onUnmounted
,onUpdated
等生命周期钩子
ref和reactive
响应式编程是非常重要的概念,其中Ref
和Reactive
是两个关键的API。
宏观角度看:
ref 用来定义:基本类型数据、对象类型数据;
reactive 用来定义:对象类型数据。2.
区别:
ref 创建的变量必须使用.value(可以使用 volar 插件自动添加.value)。1.
2.reactive 重新分配一个新对象,会失去响应式(可以使用 0bject.assign 去整体替换)
使用原则:
1.若需要一个基本类型的响应式数据,必须使用ref。
2.若需要一个响应式对象,层级不深,ref、reactive 都可以。
3.若需要一个响应式对象,且层级较深,推荐使用reactive。
<script setup>
import { ref } from 'vue';
const count = ref(0); // 创建一个Ref对象,初始值为0
count.value = 1; // 修改Ref对象的值
console.log(count.value); // 访问Ref对象的值
</script>
<script setup>
语法是一种简洁的写法,可以在单文件组件中更便捷地使用ref
。
<template>
<div>
<p>User Name: {{ user.name }}</p>
</div>
</template>
<script setup>
import { reactive } from 'vue';
const user = reactive({
name: 'Alice',
});
</script>
使用Reactive
能够管理复杂对象数据,确保对象属性的变化能够被追踪
toref和torefs
<script setup lang="ts">
import {reactive,toRefs,toRef} from 'vue'
let person = reactive({
name:'张三'
age:18
})
let {name,age}= toRefs(person)
let nl = toRef(person,'age')
console.log(nl.value)// 18
console.log(name.value)// '张三'
</script>
作用:将一个响应式对象中的每一个属性,转换为ref 对象。
备注:toRefs与toRef 功能一致,但 toRefs 可以批量转换。
computed
计算属性computed,vue2和vue的写法不同
vue2
<template>
姓:<input type="text"v-model="params.fristName"/>
<br/>
名:<input type="text"v-model="params.lastName"/>
<br/>
<h3 v-show="params.lastName || params.fristName">全名:{{ get }}</h3></template>
<script>
export default{
data(){
return{
// 数据
params :{
fristName:""
lastName:""
}
}
},
// 计算属性
computed:{
get(){
// 不要在 getter 中做异步请求或者更改 DOM
return this.params.fristName +''+ this.params.lastName //只读,get函数中的值,被调用或被修改就调用},
set(newvalue){
console.log('newvalue',newvalue)}// 要想可改写,就在set,监听修改值的变化
</script>
vue3
<template>
姓:<input type="text"v-model="params.fristName"/>
<br/>名:<input type="text"v-model="params.lastName"/>
<h3 v-show="params.lastName params.fristName">
全名:{{ params.fullName }}</h3></template>
<script setup>
import {computed,reactive} from 'vue'//引入vue中的computed,reactive//
let params =reactive({
fristName:""
lastName:""
})
// 计算属性 简写
// params.fullName = computed(()=>{return params.fristName ++ params.lastName
}
// })
// 完整写法
params.fullName = computed({
get(){
// 不要在 getter 中做异步请求或者更改 DOM
return params.fristName +'-'+ params.lastName}//只读,get函数中的值,被调用或被修改就调用
set(newvalue)f
console.log('newvalue',newvalue)}//要想可改写,就在set,监听修改值的变化
</script>
watch
vue3
中watch
只能监听的以下四种数据:
- 一个getter函数(一个能返回值的函数)
- ref定义的值
- reactive定义的值
- 包含以上内容的数组
watch监听ref简单的基本类型数据,需要手动开启深度监视
<template>
<div class="itemStyle">
<div>
姓名1: <input type="text" v-model="dataRef.name">
姓名2: <input type="text" v-model="dataReactive.name">
</div>
<div>
<button type="button" @click="handleChangeData">修改数据</button>
</div>
</div>
</template>
<script setup lang="ts" name="item">
import {ref,reactive,toRefs,toRef,watch} from "vue"
let dataRef = ref({
name:"小张",
})
let dataReactive = reactive({
name:"小张",
other:{
a:"1111",
b:"2222",
c:{
d:"1111",
e:"2222",
}
}
})
const handleChangeData = ()=>{
data.value = {
name:"小红",
}
Object.assign(dataReactive,{
name:"小红",
})
}
//ref的监听
watch(dataRef,(newVal,oldVal)=>{
console.log("新值:",newVal);
console.log("旧值:",oldVal);
if(newVal == '小红'){
stopWatch()//解除监听
}
},{
deep:true,//开启深度监听
})
//reactive的监听
//此时是有问题的:oldVal会和newVal数据保持一致,当data里面的任意值改变,都会触发该监听,强制开启深度监听
//watch(dataReactive,(newVal,oldVal)=>{
// console.log("新值:",newVal);
// console.log("旧值:",oldVal);
//})
//使用()=>箭头函数监听data对象中name属性
watch(()=>data.name,(newVal,oldVal)=>{
console.log("新值:",newVal);
console.log("旧值:",oldVal);
})
//函数形式
//监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数
watch(()=>dataReactive.other.c,(newVal,oldVal)=>{
console.log("新值:",newVal);
console.log("旧值:",oldVal);
})
//多个数据
//监视,情况五:监视上述的多个数据
watch([dataReactive.other.c,()=>dataReactive.name,()=>dataReactive.other.c,()=>dataReactive.other.c.d],(newVal,oldVal)=>{
console.log("新值:",newVal);
console.log("旧值:",oldVal);
},{deep:true})
</script>
watchEffect
watchEffect
是 Vue3 中提供的一个新特性,用于监听响应式数据的变化,并在数据发生变化时执行指定的回调函数。
和watch
不同,watchEffect
不需要指定要监听的数据,而是会自动追踪函数中使用的响应式数据,并在这些数据发生变化时重新执行回调函数。这种自动追踪的特性可以简化代码,并提高应用的性能
标签的ref属性
在vue2.x
中,可以通过给元素添加ref='xxx'
属性,然后在代码中通过this.$refs.xxx
获取到对应的元素
然而在vue3
中时没有$refs
这个东西的,因此vue3
中通过ref
属性获取元素就不能按照vue2
的方式来获取
<template>
<div ref='info'>hello</div>
<button @click='handleClick'>点击</button>
</template>
<script setup lang="ts" name="item">
import { ref } from 'vue'
// Vue3中通过ref操作DOM
// 1、定义一个响应式变量
const info = ref(null)
const handleClick = ()=>{
// 4、此时可以通过info变量操作DOM
console.log(info.value.innerHTML)
}
</script>
vue3 ts中的泛型
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法
//定义一个接口,用于限制person对象的具体属性
export interface PersonInter {
id:string,
name:string,
age:number
}
//一个自定义类型
export type Persons= Array<PersonInter>
<template>
<div class="person"
???
</div>
</template>
<script lang="ts" setup name="Person">
import {type PersonInter, type PersonInterList} from '@/types
let person:PersonInter = {id:'asyud7asfd01',name:'张三',age:60}
let personList:Array<PersonInterList>=[
{id:'asyud7asfd01',name:'张三',age:60},
{id:'asyud7asfd02',name:'李四',age:18},
{id:'asyud7asfd03',name:'王五',age:5}]
</script>
props的使用
props
是组件间通信的一种方式,允许父组件向子组件传递数据。Vue 3 保留了 Vue 2 中 props
的基本概念,但在一些细节和性能上进行了改进。需要使用 defineProps
宏来声明 props。这是 Vue 3 Composition API 的一部分
假设我们有一个父组件 ParentComponent.vue
,它需要向子组件 ChildComponent.vue
传递一个用户的信息,包括用户的ID和姓名
父组件 ParentComponent.vue
<template>
<div>
<!-- 使用 v-bind (或简写为 ':') 将 user 对象传递给 ChildComponent 的 userProp 属性 -->
<ChildComponent :userProp="user" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 创建一个响应式的数据对象 user
const user = ref({
id: 1,
name: '张三'
});
</script>
子组件 ChildComponent.vue
<template>
<div>
<p>用户ID: {{ userProp.id }}</p>
<p>用户名: {{ userProp.name }}</p>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
// 使用 defineProps 来声明接收的 props
const props = defineProps({
userProp: {
type: Object,
required: true,
// 可以添加更复杂的验证逻辑
validator: (value) => {
return value.id !== undefined && typeof value.name === 'string';
}
}
});
</script>
单向数据流:props
是单向数据流的,父组件可以传递数据到子组件,但子组件不能直接修改 props
的值。如果子组件需要修改这些数据,它应该定义自己的局部数据属性或使用计算属性,并基于 props
的值来更新这些属性
vue2和vue3生命周期
1.命名变化:
- Vue 2:生命周期钩子函数的名称通常以"before"和"after"为前缀,例如"beforeCreate"、"created"、"beforeMount"、"mounted"、"beforeUpdate"、"updated"、"beforeDestroy"和"destroyed"。
- Vue 3:在Vue 3中,这些钩子函数的名称被修改为以"on"为前缀,并增加了一些新的命名规则,如"onBeforeCreate"、"onCreated"、"onBeforeMount"、"onMounted"、"onBeforeUpdate"、"onUpdated"、"onBeforeUnmount"和"onUnmounted"。这一变化使得Vue 3的生命周期钩子函数命名更加统一和直观。
2. 新增的生命周期钩子函数:
- Vue 3引入了一些新的生命周期钩子函数,这些函数在Vue 2中是不存在的。"onBeforeMount"和"onBeforeUpdate"这两个钩子函数允许开发者在组件挂载和更新之前执行一些操作。此外,Vue 3还引入了"onRenderTracked"和"onRenderTriggered"等钩子函数,用于在组件的渲染跟踪和触发过程中执行特定的逻辑。
3. 生命周期的触发时机
- Vue 2:在Vue 2中,生命周期钩子函数的触发时机是固定的,按照组件的创建、挂载、更新和销毁等阶段依次触发。
- Vue 3:Vue 3引入了基于组合API的组件写法,并使用"setup"函数来配置组件。这导致了生命周期的触发时机发生了变化。特别是,"setup"函数在组件创建过程中被较早地调用,甚至早于"beforeCreate"和"created"钩子函数。此外,Vue 3采用了异步更新的机制,这也可能影响到生命周期函数的触发时机。
vue3的hook
Vue 3中的hook(或称为Composition API)具有多种重要作用
1.函数式编程:以函数式编程的方式组织和复用逻辑。这意味着你可以将相关的逻辑封装成独立的函数(即hook),然后在多个组件中重复使用这些函数,低代码,复用性强。
2.避免命名冲突:与Vue 2中的mixins相比,Composition API中的hook通过函数调用的方式避免了命名冲突的问题。因为每个hook都是一个独立的函数,所以它们之间的变量和方法不会相互干扰。
3.分离关注点和可读性:每个hook都专注于一个特定的功能或关注点,使得组件的结构更加清晰、易于理解。
4.按需引入和组合性:Composition API允许你按需引入所需的hook,而不是像Vue 2那样必须将所有选项都放在组件的options对象中。这种按需引入的方式可以减少代码的冗余和不必要的复杂性。也可多个hook组合在一起,以创建一个功能更强大的组件
import { ref } from 'vue';
import axios from 'axios';
export function useLogin() {
const isLoading = ref(false);
const error = ref('');
const login = async (username, password) => {
isLoading.value = true;
try {
await axios.post('/api/login', { username, password });
// 登录成功后的逻辑,比如跳转到首页
console.log('登录成功');
} catch (e) {
error.value = e.message;
}
isLoading.value = false;
};
return { isLoading, error, login };
}
// 在组件中使用
<template>
<div>
<button @click="login(username.value, password.value)">登录</button>
<p v-if="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useLogin } from './useLogin';
const { isLoading, error, login } = useLogin();
const username = ref('');
const password = ref('');
</script>
路由
Vue 2中,通过new Router()
函数创建路由实例,并传入配置对象,包括路由规则(routes)等。
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const router = new Router({
routes: [
// 路由规则
]
})
Vue 3中,使用createRouter
函数创建路由实例,并需要显式地指定history类型(如createWebHistory
或createWebHashHistory
)。
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
// 路由规则
]
})
Vue 2中,通过new Vue()
的选项对象中直接挂载路由实例。
new Vue({
router,
render: h => h(App),
}).$mount('#app')
Vue 3中,使用createApp
创建的应用实例上有一个.use()
方法来注册插件,包括路由
const app = createApp(App)
app.use(router)
app.mount('#app')
Vue 2和Vue 3中,关于路由的嵌套、参数配置、属性以及重定向的写法,虽然两者在基本概念上相似,但在具体实现和语法细节上存在一些细微差异,不就一一举例了
Pinia 的理解
Pinia 是 Vue.js 生态系统中的一个状态管理库,专为 Vue 3 设计,但也可以与 Vue 2 兼容(通过适当的适配器)。Pinia 旨在提供一种比 Vuex 更简单、更灵活的方式来管理 Vue 应用程序的状态。
通过调用 defineStore
函数并传入一个配置对象来创建 Store。这个配置对象包含了 State、Getters 和 Actions 的定义。defineStore
函数内部会创建一个新的 Store 实例,并使用 Vue 3 的响应式系统来使 State 变得响应式。
和vuex一样 也是项目的状态管理器,但是有不同的区别:
1.vuex是集中式的架构将所有的状态存储在一个单一的全局状态树中。Vuex通过mutations和actions来修改和处理状态。pina是去中心化的架构分布在多个模块每个模块拥有自己的状态
vuex
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
}
}
})
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</div>
</template>
<script>
export default {
computed: {
count() {
return this.$store.state.count
}
},
methods: {
increment() {
this.$store.commit('increment')
},
decrement() {
this.$store.commit('decrement')
}
}
}
</script>
pinia
// counterStore.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
}
}
})
<template>
<div>
<p>{{ counter.count }}</p>
<button @click="counter.increment()">Increment</button>
<button @click="counter.decrement()">Decrement</button>
</div>
</template>
<script setup>
import { useCounterStore } from './store/counterStore'
const counter = useCounterStore()
</script>
Pinia的API设计更加符合Vue 3的组合式API风格,而Vuex的API则更偏向于Vue 2的Options API风格
组件通信
1. Props
Vue 2 和 Vue 3:
- Props 在两个版本中的使用方式基本相同。父组件通过
props
向子组件传递数据,子组件通过props
接收数据。Vue 3中继续支持这种方式,没有显著变化。加了setup语法的写法不同
2. 自定义事件
Vue 2 和 Vue 3:
- 自定义事件 也基本保持一致。子组件通过
this.$emit('eventName', data)
触发事件,父组件通过@eventName="handler"
监听事件。Vue 3中继续支持这种方式,并且没有显著变化。加了setup语法的写法不同
3. _mitt (非Vue官方)
Vue 2 和 Vue 3:
- _mitt 或类似的事件总线库在两个版本中的使用方式相同,因为它们不依赖于Vue的内部实现。你可以在Vue 2或Vue 3项目中以相同的方式使用_mitt来跨组件通信。
-
<template> <ChildComponent v-model:modelValue="parentValue" /> </template> <script setup> import { ref } from 'vue'; import ChildComponent from './ChildComponent.vue'; const parentValue = ref(''); </script>
4. v-model
Vue 2 和 Vue 3:
- Vue 2 中的
v-model
默认是基于value
属性和input
事件的语法。 - Vue 3 引入了自定义
v-model
的能力,允许开发者指定prop和event。例如,v-model:customModel="someData"
可以映射到customModel
prop和update:customModel
事件。 - vue2通常用于表单输入,但也可以自定义。
- vue3:子组件
ChildComponent.vue
<template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template> <script setup> import { defineProps, defineEmits } from 'vue'; const props = defineProps(['modelValue']); const emit = defineEmits(['update:modelValue']); </script>
父组件
Parent.vue
:
5. $attrs
Vue 2 和 Vue 3:
- attrs在两个版本中都包含了父作用域中不作为prop被识别(且获取)的特性绑定(class和style除外)。在Vue3中,‘attrs
包含组件标签上所有未声明的属性(包括class和style,但可以通过
inheritAttrs: false`来阻止它们出现在根元素上)。
6. $refs
Vue 2 和 Vue 3:
- refs在两个版本中的使用方式基本相同。它们用于访问组件实例或DOM元素。Vue3中,‘refs
的访问方式(通过
this.refs‘或模板引用)没有变化,但Vue3的响应式系统(Proxy)使得对‘refs`的访问更加灵活和高效。
7. $parent
Vue 2 和 Vue 3:
- parent在两个版本中都允许你访问一个组件实例的父实例。然而,Vue官方通常不推荐直接使用‘parent
进行组件间的通信,因为这会导致组件间的紧密耦合。Vue 3中继续支持
$parent`,但同样建议避免使用。
8. provide / inject
Vue 2 和 Vue 3:
- provide / inject 在Vue 2.2.0+中引入,用于跨组件通信,无论组件层次结构有多深。在Vue 3中,
provide
和inject
的API保持不变,但Vue 3的响应式系统使得inject
可以接收响应式的数据。
shallowRef和shallowReactive
shallowRef
和shallowReactive
是两种用于创建浅响应式数据的API,提供了浅响应式的支持。shallowRef
适用于基本数据类型或不需要对象内部属性响应式的场景;shallowReactive
适用于对象数据,但只需要顶层属性响应式的场景。
shallowRef 示例
<template>
<div>
<p>用户名: {{ user.value.name }}</p>
<p>邮箱: {{ user.value.email }}</p>
<button @click="updateUser">更新用户信息</button>
</div>
</template>
<script setup>
import { shallowRef } from 'vue';
// 使用shallowRef创建浅响应式引用
const user = shallowRef({
name: '张三',
email: 'zhangsan@example.com'
});
// 更新用户信息时,替换整个对象
function updateUser() {
user.value = {
name: '李四',
email: 'lisi@example.com'
};
}
</script>
shallowReactive 示例
<template>
<div>
<p>用户名: {{ userInfo.name }}</p>
<p>详情: {{ userInfo.details.bio }}</p>
<button @click="updateUserName">更新用户名</button>
<!-- 注意:以下按钮不会触发视图更新,因为details.bio是深层属性 -->
<button @click="updateBio">更新简介(不会触发视图更新)</button>
</div>
</template>
<script setup>
import { shallowReactive } from 'vue';
// 使用shallowReactive创建浅响应式对象
const userInfo = shallowReactive({
name: '张三',
details: {
bio: '这是张三的简介'
}
});
// 更新用户名时,会触发视图更新
function updateUserName() {
userInfo.name = '李四';
}
// 更新简介时,不会触发视图更新(因为details是浅响应式的)
function updateBio() {
userInfo.details.bio = '这是李四的新简介';
}
</script>
readonly 与 shallowReadonly
readonly
和shallowReadonly
是另外两个用于创建响应式数据的辅助函数,但它们与shallowRef
和shallowReactive
不同,主要用于创建只读响应式数据。这意味着你可以读取这些数据,但不能修改它们(尝试修改时,Vue会警告你,但不会抛出错误)。
readonly
readonly
函数接收一个响应式对象(无论是由reactive
、ref
、shallowReactive
还是shallowRef
创建的)并返回一个新的只读代理对象。这个新对象保持原始对象的响应性,但你不能修改它。任何尝试修改它的操作都不会影响原始对象,但Vue会警告你尝试进行了非响应式操作
注意,虽然copy
是只读的,但它仍然是响应式的,
import { reactive, readonly } from 'vue';
const original = reactive({ count: 0 });
const copy = readonly(original);
// 尝试修改copy将不会生效,并且Vue会警告你
copy.count++; // 警告: Set operation on key "count" failed: target is readonly.
// 但original仍然可以修改
original.count++;
console.log(original.count); // 1
console.log(copy.count); // 1(因为copy是响应式的,所以它会反映original的变化)
shallowReadonly
与readonly
类似,shallowReadonly
也创建一个只读代理对象,但它只保持顶层属性的只读性。如果顶层属性的值是对象或数组,那么这些内部对象的属性仍然可以被修改(尽管这不是推荐的做法,因为它违反了只读性的原则)
import { reactive, shallowReadonly } from 'vue';
const original = reactive({
nested: { foo: 1, bar: 2 }
});
const copy = shallowReadonly(original);
// 尝试修改顶层属性将不会生效,并且Vue会警告你
copy.nested = {}; // 警告: Set operation on key "nested" failed: target is readonly.
// 但你可以修改嵌套对象的属性(不推荐)
copy.nested.foo = 10; // 这将修改original.nested.foo,因为shallowReadonly只保护顶层属性
console.log(original.nested.foo); // 10
console.log(copy.nested.foo); // 10
toRaw与markRaw
toRaw
和markRaw
是两个与响应式系统紧密相关的实用函数,它们各自扮演着不同的角色。
toRaw
toRaw
函数用于获取一个由Vue 3响应式系统(如reactive
、readonly
、shallowReactive
或shallowReadonly
)创建的代理对象的原始版本。这意味着,通过toRaw
,你可以绕过Vue的响应式追踪,直接访问和操作原始对象。
import { reactive, toRaw } from 'vue';
// 创建一个响应式对象
const state = reactive({
count: 0,
nested: {
value: 'Hello, Vue!'
}
});
// 使用toRaw获取原始对象
const rawState = toRaw(state);
// 现在rawState是state的原始对象,修改它不会触发Vue的响应性更新
rawState.count = 10; // 这不会更新视图,因为Vue的响应性系统没有追踪到这次修改
// 但是,如果你通过原始对象访问嵌套对象,并修改它,那么由于Vue的响应性系统追踪了嵌套对象,视图可能会更新(取决于你如何使用它)
rawState.nested.value = 'Hello, Raw!'; // 这可能会触发依赖于nested.value的视图的更新
// 注意:虽然修改nested.value可能会触发更新,但这并不是因为直接修改了rawState(Vue不追踪rawState),
// 而是因为nested对象本身仍然是响应式的,Vue追踪了它的变化。
// 如果你想要完全避免这种情况,可以使用markRaw来标记nested对象,使其非响应式
const nonReactiveNested = markRaw({ value: 'Non-reactive' });
// 更新state以包含非响应式对象
state.nested = nonReactiveNested;
// 现在,无论你怎么修改nonReactiveNested,都不会触发Vue的响应性更新
nonReactiveNested.value = 'This change will not trigger updates.';
// 注意:markRaw在这个示例中没有直接和toRaw一起使用,但它们是处理Vue响应性系统的两个有用工具。
markRaw
markRaw
函数用于标记一个对象,使其永远不会被Vue的响应式系统代理。即使你尝试使用reactive
或ref
来转换它,它也会保持原样,不会变成响应式对象。
import { reactive, markRaw } from 'vue';
const largeData = markRaw({ /* 大量数据 */ });
const state = reactive({
data: largeData
});
// 修改 largeData 的属性不会触发视图更新
state.data.someProperty = 10;
customRef
customRef
是一个高级功能,允许你创建自定义的响应式引用(ref)。这在你需要更细致地控制响应式行为时非常有用,比如实现自定义的依赖追踪逻辑、防抖(debounce)、节流(throttle)等。
function useDebouncedRef(value, delay = 300) {
let timeout = null;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
};
});
}
// 在组件的setup函数中使用
export default {
setup() {
const debouncedCount = useDebouncedRef(ref(0), 500);
// 注意:这里我们实际上是将ref(0)作为初始值传给了useDebouncedRef,
// 然后useDebouncedRef内部会创建一个新的响应式引用,并通过customRef返回。
// 但为了简化示例,我们假设useDebouncedRef直接返回了一个响应式引用。
// 你可以通过debouncedCount.value来访问和修改这个防抖的引用
// ...
}
};
在实际应用中,你可能希望 useDebouncedRef
直接接收一个初始值,并在内部使用 ref
来封装它。不过,为了展示 customRef
的工作原理,上面的例子已经足够清晰了
Teleport
Teleport 组件是一个强大的工具,它允许开发者将组件的内容渲染到 DOM 中的任何位置,而不受组件嵌套结构的限制,将内部的部分模板“传送”到该组件的 DOM 结构外层的位置,处理全局提示、弹窗、模态框等需要脱离当前组件层级渲染的场景时非常有用
<template>
<div>
<!-- 组件的其他内容 -->
<teleport to="body">
<!-- 模态框、弹窗等内容 -->
<div class="modal">
<!-- 模态框内容 -->
</div>
</teleport>
</div>
</template>
<teleport to="body">
标签内的内容(即模态框)将被渲染到 <body>
标签下,而不是在组件自身的位置。这样,模态框就可以脱离组件的嵌套结构,灵活地渲染到页面的任何位置
Suspense
<Suspense>
组件,它是一个内置组件,用于处理异步组件的加载状态。<Suspense>
允许你定义一个等待异步组件时渲染的备用内容(fallback),以及异步组件加载完成后渲染的内容。这有助于提供更好的用户体验,尤其是在处理路由懒加载或大型组件加载时
<template>
<Suspense>
<!-- 异步组件加载完成后渲染的内容 -->
<template #default>
<AsyncComponent />
</template>
<!-- 异步组件加载时渲染的备用内容 -->
<template #fallback>
Loading...
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue';
export default {
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./AsyncComponent.vue')
)
}
}
</script>
<Suspense>
使用作用域插槽来接收 #default
和 #fallback
插槽的内容,还支持一个可选的 #error
插槽,用于在异步组件加载失败时渲染内容。<Suspense>
也可以与 Vue Router 的路由懒加载功能结合使用,以在路由切换时提供加载状态反馈
注意:使用 defineAsyncComponent
函数时,你需要提供一个加载函数,该函数应该返回一个 Promise
,该 Promise
在解析时返回组件的定义。Vue 会在组件需要渲染到 DOM 时调用这个加载函数,并等待 Promise
解析完成
const AsyncComponentWithOptions = defineAsyncComponent({
loader: () => import('./AsyncComponent.vue'),
loadingComponent: LoadingComponent, // 加载时的备用组件
errorComponent: ErrorComponent, // 加载失败时渲染的组件
delay: 200, // 延迟显示加载中组件的时间
timeout: 3000, // 超时时间,单位毫秒
// 可以是一个错误处理函数,接收错误信息和解析器作为参数
onError(error, retry, fail, attempts) {
if (attempts <= 1) {
retry();
} else {
fail();
}
}
});
搭配路由
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue') // 异步组件
},
{
path: '/about',
name: 'About',
// 带有加载和错误组件的异步组件示例
component: defineAsyncComponent({
loader: () => import('../views/About.vue'),
loadingComponent: () => import('../components/Loading.vue'),
errorComponent: () => import('../components/Error.vue'),
delay: 200,
timeout: 3000
})
// 注意:通常你不需要在路由配置中直接使用 defineAsyncComponent,
// 除非你需要上述的加载和错误处理选项。
// 对于简单的懒加载,直接使用 () => import(...) 就足够了。
},
// 其他路由...
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
export default router;