Vue3 笔记
1. 项目的创建与运行
1.1 基于 webpack 构建工具 ------ vue-cli 脚手架
bash
# 安装脚手架(全局)
npm i -g @vue/cli
# 切换到项目所在目录
cd 项目所在目录
# 创建项目(按提示选择 Vue 版本)
vue create 项目名
# 进入项目
cd 项目名
# 运行项目(开发环境)
npm run serve
1.2 基于 Vite 构建工具 ------ Vite
bash
# 创建项目
npm init vite-app 项目名
# 按提示选择开发框架:输入 Vue 并回车
# 选择版本:默认为 Vue 3,直接回车确认
# 选择是否需要 TypeScript:按需选择 Yes / No
# 进入项目
cd 项目名
# 启动项目
npm run dev
注:
npm init vite-app是早期 Vite 的创建方式,目前已较少使用,推荐直接使用下面的create-vue。
1.3 基于 Vite 构建工具 ------ create-vue 脚手架【推荐】
bash
# 切换到项目所在目录
cd 项目所在目录
# 创建项目(无论是否安装过脚手架都用此方式)
npm init vue@latest
# 运行后会交互式配置:项目名、TypeScript、JSX、Router、Pinia、Vitest 等
# 进入项目
cd 项目名
# 初始化安装依赖
npm i
# 运行项目
npm run dev
2. 目录结构
2.1 main.js 入口文件
js
// 引入的不再是 Vue 构造函数,而是名为 createApp 的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
// 创建应用实例对象 app(类似于 Vue2 中的 vm,但 app 比 vm 更"轻")
const app = createApp(App)
// 挂载到 #app 容器上
app.mount('#app')
// 整合写法:
// createApp(App).mount('#app')
// Vue2 写法(对比):
/* import Vue from 'vue'
const vm = new Vue({
render: h => h(App)
})
vm.$mount('#app') */
注:
- Vue3 中创建应用使用
createApp()工厂函数,不再需要new Vue();- 其他文件与 Vue2 基本一致,需要注意的是:Vue3 的
.vue文件模板中可以没有根标签(支持多根节点,即 Fragment)。
3. 组合式 API
3.1 setup()
setup() 是组合式 API 的核心入口,所有组合式 API(数据、方法、计算属性、侦听属性等)都可以写在它里面。它返回一个对象,对象中为该函数的数据、函数等。
注意点:
setup()在beforeCreate之前 执行一次,且其中的this是undefined(不能用this访问组件实例);setup()不能被async修饰 。若写成 async 函数,返回的是 Promise,需要配合Suspense组件使用,一般不建议这样写;setup()有两个参数:props和context。
js
<script>
export default {
name: 'App',
setup() {
// 数据
let name = '张三'
let age = 18
// 方法
function sayHello() {
alert(`我叫${name},我${age}岁了,你好!`)
}
// 返回对象:数据和方法都要 return 出去才能被模板使用
return {
name,
age,
sayHello
}
}
}
</script>
3.1.1 setup() 中的 props 参数
作用:接收父组件传递过来的数据。
html
<!-- App.vue(父组件):向子组件传值 -->
<template>
<User name="张三" age="30" :sex="sex"></User>
</template>
<script>
import { ref } from 'vue'
import User from './components/User.vue'
export default {
name: 'App',
components: { User },
setup() {
let sex = ref('男')
return { sex }
}
}
</script>
html
<!-- User.vue(子组件):接收数据 -->
<template>
</template>
<script>
export default {
name: 'User',
// 首先像 Vue2 一样用 props 配置项接收
props: ['name', 'age', 'sex'],
// 再在 setup 中通过 props 参数接收(setup 中没有 this!)
setup(props) {
// props 被包装成响应式代理对象(Proxy)
console.log(props.name)
console.log(props.age)
console.log(props.sex)
}
}
</script>
注:
props会被包装成响应式的 Proxy 对象,不需要 return 出去即可在模板中使用;props是只读的,子组件不应修改 props(数据流是单向的);- 对
props进行解构会失去响应式 ,若确需解构请使用toRefs(props)(setup非语法糖场景)。
3.1.2 setup() 中的 context 参数 ------ 组件上下文对象
context 上下文对象包含三个属性:attrs、slots、emit。
| 属性 | 作用 | 相当于 Vue2 |
|---|---|---|
attrs |
值为对象,包含组件外部传递过来、但没有在 props 配置中声明的属性 | this.$attrs |
slots |
收到的插槽内容 | this.$slots |
emit |
分发自定义事件的函数 | this.$emit |
(1)自定义事件 ------ 子传父:context 中的 emit()
子组件利用 context.emit() 触发事件,第一个参数为父组件在子组件标签上绑定的事件名,后续参数为父组件事件处理函数所需的参数。
html
<!-- App.vue(父组件) -->
<template>
<Demo @hello="showHelloMsg" msg="你好啊" school="尚硅谷"/>
</template>
<script>
import Demo from './components/Demo'
export default {
name: 'App',
components: { Demo },
setup() {
function showHelloMsg(value) {
alert(`你好啊,你触发了hello事件,我收到的参数是:${value}!`)
}
return { showHelloMsg }
}
}
</script>
html
<!-- Demo.vue(子组件) -->
<template>
<h1>一个人的信息</h1>
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<button @click="test">测试触发一下Demo组件的Hello事件</button>
</template>
<script>
import { reactive } from 'vue'
export default {
name: 'Demo',
props: ['msg', 'school'],
// 声明自定义事件(推荐写法,增强代码可读性)
emits: ['hello'],
setup(props, context) {
let person = reactive({
name: '张三',
age: 18
})
function test() {
// 触发自定义事件并传参
context.emit('hello', 666)
}
return { person, test }
}
}
</script>
(2)context 中的 attrs 与 slots
js
setup(props, context) {
// attrs:接收子组件标签上定义的、但未在 props 中声明的属性
console.log(context.attrs) // 相当于 Vue2 的 this.$attrs
// slots:接收子组件标签体的插槽节点内容
console.log(context.slots) // 相当于 Vue2 的 this.$slots
// emit:触发自定义事件
console.log(context.emit)
}
关于
attrs的注意点:
- Vue2 中可以不显式声明 props 配置项,未声明的属性最终都会存放在
$attrs中;- Vue3 中未在
props中声明的属性同样会进入attrs,单根组件会自动继承到根元素上(不会报警告);只有当组件是多根节点 且未用attrs显式绑定时,才会出现"无法自动继承"的警告。建议始终显式声明props以保证代码清晰;props可以声明类型、默认值、必填项等校验规则,attrs无法对这些属性做限制。
3.2 ref() ------ 引用对象
ref() 将数据变成响应式数据,数据可以是基本类型 ,也可以是对象类型 。在 JS 中读写需要 .value,在模板中不需要 .value(模板会自动解包)。
js
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
// 数据
let name = ref('张三')
let age = ref(18)
// 方法
function changeInfo() {
name.value = '李四'
age.value = 48
}
return { name, age, changeInfo }
}
}
</script>
ref 的底层原理:
- ref 处理基本类型 时,通过
RefImpl实例的get value() / set value()访问器实现数据劫持(思路与Object.defineProperty的 getter/setter 类似,但实现机制不同,用的是 ES6 Class 的访问器属性); - ref 处理对象类型 时,底层调用的是对
reactive()的封装,即通过 ES6 的Proxy实现响应式,嵌套对象都有响应式。
js
// 对象类型会被包装成 RefImpl,其 .value 是 reactive 产生的 Proxy 代理对象
let job = ref({
type: '前端工程师',
salary: '30K'
})
// 修改时:
job.value.type = 'UI设计师'
job.value.salary = '60K'
js
// Proxy 实现的响应式,对象中的对象,对象中的对象,都有响应式处理(底层递归处理)
let userRefImpl = ref({
name: 'jack',
age: 20,
addr: {
city: '北京',
street: '大兴区凉水河二街'
}
})
function modifyUser() {
// 这种整体替换 .value 的方式,走的是 RefImpl 的 setter
// counterRefImpl.value = 200
// userRefImpl.value 是一个 Proxy 代理对象
userRefImpl.value.name = '李四'
userRefImpl.value.age = 90
// 修改嵌套地址信息(有响应式)
userRefImpl.value.addr.city = '邯郸'
userRefImpl.value.addr.street = '太极二路'
}
3.3 reactive()
reactive() 只处理对象类型(对象/数组)的数据 ,读取或修改数据时不需要 .value,返回的是一个 Proxy 对象。
js
<script>
import { ref, reactive } from 'vue'
export default {
name: 'App',
setup() {
let name = ref('张三')
let age = ref(18)
// reactive 只能处理对象类型
let job = reactive({
type: '前端工程师',
salary: '30K'
})
function changeInfo() {
name.value = '李四'
age.value = 48
// reactive 对象修改不需要 .value
job.type = 'UI设计师'
job.salary = '60K'
}
return { name, age, job, changeInfo }
}
}
</script>
3.3.1 回顾 Vue2 对象、数组的响应式处理
在 Vue2 中:
- 想添加/删除对象的属性或方法,不能直接修改;
- 数组不能通过下标直接修改;
- Vue2 提供
$set()、$delete()等方法解决,修改/删除数组元素也可用数组自身方法(Vue2 重写了数组方法使其具有响应式)。
js
// Vue2 中的写法
methods: {
addSex() {
this.$set(this.person, 'sex', '女') // 添加属性
// Vue.set(this.person, 'sex', '女')
},
deleteName() {
Vue.delete(this.person, 'name') // 删除属性
// this.$delete(this.person, 'name')
},
updateHobby() {
this.person.hobby.splice(0, 1, '逛街') // 修改数组元素
// this.$set(this.person.hobby, 0, '逛街')
}
}
模拟 Vue2 的响应式原理(Object.defineProperty):
js
// 源数据
let person = { name: '张三', age: 18 }
// 模拟 Vue 中实现响应式
let p = {}
Object.defineProperty(p, 'name', {
get() { // 有人读取 name 时调用
return person.name
},
set(value) { // 有人修改 name 时调用
console.log('有人修改了name属性,我发现了,我要去更新界面!')
person.name = value
}
})
Object.defineProperty(p, 'age', {
get() { return person.age },
set(value) {
console.log('有人修改了age属性,我发现了,我要去更新界面!')
person.age = value
}
})
3.3.2 Vue3 中的响应式原理(Proxy + Reflect)
Vue3 不存在上述 Vue2 的问题,功劳是 reactive() 内部通过 Proxy 实现代理,并通过 Reflect 操作源对象内部数据。
模拟 Vue3 的响应式原理:
js
// 模拟 Vue3 中实现响应式
const p = new Proxy(person, {
// 有人读取 p 的某个属性时调用
get(target, propName) {
console.log(`有人读取了p身上的${propName}属性`)
return Reflect.get(target, propName)
},
// 有人修改 p 的某个属性、或给 p 追加某个属性时调用
set(target, propName, value) {
console.log(`有人修改了p身上的${propName}属性,我要去更新界面了!`)
return Reflect.set(target, propName, value)
},
// 有人删除 p 的某个属性时调用
deleteProperty(target, propName) {
console.log(`有人删除了p身上的${propName}属性,我要去更新界面了!`)
return Reflect.deleteProperty(target, propName)
}
})
Proxy 三个拦截器说明:
| 拦截器 | 触发时机 | 参数 |
|---|---|---|
get(target, propName) |
读取属性 | target:源数据(person);propName:属性名 |
set(target, propName, value) |
修改/新增属性 | value:修改的新值 |
deleteProperty(target, propName) |
删除属性 | --- |
对比 Vue2 的优势:不再需要像 Vue2 一样遍历被代理对象的各个属性,只需一套
Proxy()并配置上get()、set()、deleteProperty()即可;新增/删除属性、数组下标修改都具备响应式,嵌套对象的数据也都有响应式。
控制台验证效果:
js
let person = { name: '张三', age: 18 }
const p = new Proxy(person, { /* 上面的三个拦截器 */ })
p.name = '李四' // 输出:有人修改了p身上的name属性,我要去更新界面了!
p.sex = '男' // 新增属性同样触发 set
delete p.name // 输出:有人删除了p身上的name属性,我要去更新界面了!
3.3.3 reactive 对比 ref
| 对比维度 | ref | reactive |
|---|---|---|
| 定义数据 | 用来定义基本类型数据;也可以定义对象/数组(内部会自动通过 reactive 转为代理对象) |
用来定义对象(或数组)类型数据 |
| 原理 | 基本类型通过 RefImpl 的 getter/setter 实现数据劫持;对象类型借助 reactive |
通过 Proxy 实现数据劫持,并通过 Reflect 操作源对象内部数据 |
| 使用 | 操作数据需要 .value;模板中读取不需要 .value |
操作与读取数据均不需要 .value |
使用建议:基本类型用
ref,对象/数组用reactive;若 ref 包裹对象,则需注意"整体替换.value与修改内部属性的差异"。
3.4 computed 计算属性
computed 是组合式 API,需要从 vue 中引入。参数是回调函数 (简写)或对象(完整写法,包含 get/set)。与 Vue2 计算属性写法一致,同样有简写形式与完整 get/set 写法;若计算属性会被修改,请写成完整写法。
js
<script>
import { reactive, computed } from 'vue'
export default {
name: 'Demo',
setup() {
let person = reactive({
firstName: '张',
lastName: '三'
})
// 计算属性-简写(没有考虑计算属性被修改的情况)
/* person.fullName = computed(() => {
return person.firstName + '-' + person.lastName
}) */
// 计算属性-完整写法(考虑读和写)
person.fullName = computed({
get() {
return person.firstName + '-' + person.lastName
},
set(value) {
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
return { person }
}
}
</script>
补充:
computed具备缓存机制,只有依赖的响应式数据变化时才会重新计算;而普通函数每次渲染都会重新执行。
3.5 watch() 侦听属性
组合式 API,监听数据是否发生变化,与 Vue2 功能相同,但写法上有一些细节差异。
js
import { ref, reactive, watch } from 'vue'
// 示例数据
let sum = ref(0)
let msg = ref('你好啊')
let person = reactive({
name: '张三',
age: 18,
job: { j1: { salary: 20 } }
})
(1)侦听 ref 响应式的单个基本类型数据
js
watch(sum, (newValue, oldValue) => {
console.log('sum变了', newValue, oldValue)
}, { immediate: true }) // 配置对象:immediate 立即监听 / deep 深度监听
(2)侦听 ref 响应式的多个数据
js
watch([sum, msg], (newValue, oldValue) => {
console.log('sum或msg变了', newValue, oldValue) // 新值旧值都是数组形式
}, { immediate: true })
(3)侦听 reactive 对象的全部属性
js
watch(person, (newValue, oldValue) => {
console.log('person变化了', newValue, oldValue)
}, { deep: false }) // 注意:此处的 deep 配置无效
注意:
- 此处无法正确获取旧值,目前获取到的新旧值相同;
- 默认开启深度监视 ,
deep: false无效。
(4)侦听 reactive 对象的某个基本类型属性
js
// 参数1需要写成回调函数,返回值为需要被侦听的对象属性
watch(() => person.name, (newValue, oldValue) => {
console.log('person的name变化了', newValue, oldValue)
})
(5)侦听 reactive 对象的多个基本类型属性
js
// 参数1需要写成回调函数数组
watch([() => person.name, () => person.age], (newValue, oldValue) => {
console.log('person的name或age变化了', newValue, oldValue)
})
(6)侦听 reactive 对象的属性为对象(拿不到旧值)
js
// 写法一:回调函数,返回 reactive 对象中的某个对象 ------ deep 配置有效
watch(() => person.job, (newValue, oldValue) => {
console.log('person的job变化了', newValue, oldValue)
}, { deep: true })
// 写法二:以 xx.yy 的形式,只需保证 yy 是一个响应式对象即可
// watch(person.job, (newValue, oldValue) => {
// console.log('person的job变化了', newValue, oldValue)
// }, { deep: false }) // 注意:第二种形式仍默认开启深度监视,deep:false 无效
注:参数 1 如果写成函数写法 ,则可以有效配置
deep;若采用第二种形式,则仍默认开启深度监视,deep: false配置无效。
小细节 ------ 侦听 ref 响应式的对象:
js
let data = ref({
a: { b: { c: { counter: 1 } } }
})
// 写法一:直接写 ref 对象(本质是 RefImpl 对象)
// 该写法支持 deep 配置;若需侦听对象内部某个属性的变化,需要手动开启 deep: true
watch(data, (newValue, oldValue) => {
console.log(newValue, oldValue)
}, { deep: true })
// 写法二:写 .value(本质是 Proxy 对象,即 reactive 对象)
// 与上述情况(3)一致:默认开启深度监视,无法获取旧值,deep 配置无效
/* watch(data.value, (newValue, oldValue) => {
console.log(newValue, oldValue)
}, { deep: false }) // 拿不到 oldValue,deep 配置无效 */
小细节汇总:
- 若侦听的基本类型数据是 ref 响应式的,被侦听的数据不用加
.value;- 若侦听的对象是 ref 响应式的:
- 直接写 ref 对象(RefImpl):支持
deep配置,默认没有开启深度监视;- 写
.value(Proxy/reactive 对象):默认开启深度监视且无法关闭,无法获取旧值。
3.6 watchEffect() 监听某些数据
watchEffect 接收一个回调函数作为参数,该回调函数中用到了哪些数据,就会监听哪些数据(自动收集依赖 );回调函数中用到的数据发生改变时,回调会重新执行,且会立即执行一次。
js
watchEffect(() => {
const x1 = sum.value
const x2 = person.job.j1.salary
console.log('watchEffect所指定的回调执行了')
})
3.6.1 与 watch() 的区别
| 对比 | watch | watchEffect |
|---|---|---|
| 套路 | 既要指明监视的属性 ,也要指明监视的回调 | 不用指明监视哪个属性,回调中用到的属性即被监视 |
| 类似 computed | --- | 有点像 computed,但 computed 注重计算出来的值 (必须有返回值);watchEffect 更注重过程(函数体),不用写返回值 |
| 立即执行 | 需配置 immediate: true |
默认立即执行一次 |
补充:
watchEffect返回一个停止函数,调用即可停止监听;Vue3.5+ 还提供watchPostEffect(在组件更新后触发)和watchSyncEffect(同步触发)。
3.7 toRef() ------ 将响应式对象的某个属性变成响应式
toRef 接收两个参数:第一个为响应式对象,第二个为对象中的某个属性(属性名,字符串)。
js
import { reactive, toRef } from 'vue'
let person = reactive({
name: '张三',
age: 18,
job: { j1: { salary: 20 } }
})
// 在 setup 的 return 中用上该函数:
// 1. 可以简化模板中的书写(模板里直接写 name 而不是 person.name)
// 2. 使其数据变为响应式,修改后也会联动使得该对象数据发生响应
return {
person,
name: toRef(person, 'name'), // ✔ 正确写法
age: toRef(person, 'age'),
salary: toRef(person.job.j1, 'salary')
}
易错点:
toRef(person, 'name')才是正确写法,不是toRef(person.name);- 若使用
ref(person.name),虽然也会使数据变为响应式,但实际上是重新生成 了一个 RefImpl 对象,该数据的变化不会影响原对象数据的变化(两者脱钩);- 官方文档中
toRef一般用于 reactive 对象第一层 属性;深层属性建议先取到那层对象再toRef(如toRef(person.job.j1, 'salary'))。
3.8 toRefs()
toRefs 与 toRef 的区别:该函数传入响应式对象作为参数,可以将响应式对象的所有属性与之联动并形成响应式,常配合解构展开使用。
js
import { reactive, toRefs } from 'vue'
let person = reactive({
name: '张三',
age: 18,
job: { j1: { salary: 20 } }
})
const x = toRefs(person)
console.log('******', x)
// 返回一个对象(常用)
return {
person,
// name: toRef(person, 'name'),
// age: toRef(person, 'age'),
// salary: toRef(person.job.j1, 'salary'),
...toRefs(person) // ✔ 一次性展开所有属性,且保持响应式
}
补充:
toRefs常用于解构 props / reactive 对象后仍保持响应式的场景。
3.9 provide() 与 inject()
顶层组件(祖)向底层组件(孙)传递数据和方法,实现跨组件通信(无需逐层通过 props 传递)。
html
<!-- App.vue(顶层组件 / 祖) -->
<script>
import { reactive, toRefs, provide } from 'vue'
import Child from './components/Child.vue'
export default {
name: 'App',
components: { Child },
setup() {
let car = reactive({ name: '奔驰', price: '40W' })
// provide(key, value):两个参数,第一个为自定义 key,第二个为传递的数据
provide('car', car)
return { ...toRefs(car) }
}
}
</script>
html
<!-- Son.vue(底层组件 / 孙) -->
<script>
import { inject } from 'vue'
export default {
name: 'Son',
setup() {
// inject(key):参数需与 provide 的第一个参数对应,用变量接收返回值
let car = inject('car')
return { car }
}
}
</script>
html
<template>
<h3>我是Son组件(孙),{{car.name}}--{{car.price}}</h3>
</template>
传递方法(实现底层组件间接修改顶层组件的数据):
js
// 1. 顶层组件提供数据 / 响应式数据 / 方法
provide('data-key', 'this is room data')
const count = ref(0)
provide('count-key', count)
setTimeout(() => { count.value = 100 }, 3000)
const setCount = () => { count.value++ }
provide('setCount-key', setCount)
js
// 2. 底层组件接收数据 / 响应式数据 / 方法
const roomData = inject('data-key')
const countData = inject('count-key')
const setCount = inject('setCount-key')
html
<button @click="setCount">修改顶层组件的数据</button>
注:
- 底层组件接收到的数据一般不被修改,所以顶层组件可以传递方法给底层组件,由底层组件调用方法间接修改顶层组件的数据;
- 传递 ref/reactive 数据时保持响应式 ;
inject还可以传入默认值:inject('key', 默认值)。
3.10 判断响应式类型
js
import { ref, reactive, toRefs, readonly, isRef, isReactive, isReadonly, isProxy } from 'vue'
let car = reactive({ name: '奔驰', price: '40W' })
let sum = ref(0)
let car2 = readonly(car) // 将 reactive 对象再包一层只读
console.log(isRef(sum)) // TRUE
console.log(isReactive(car)) // TRUE
console.log(isReadonly(car2)) // TRUE
console.log(isProxy(car)) // TRUE
console.log(isProxy(sum)) // FALSE(ref 基本类型不是 Proxy)
| API | 作用 |
|---|---|
isRef |
检查某个值是否为 ref |
isReactive |
检查一个对象是否是由 reactive() 或 shallowReactive() 创建的代理 |
isReadonly |
检查传入的值是否为只读对象 |
isProxy |
检查一个对象是否是由 reactive()、readonly()、shallowReactive() 或 shallowReadonly() 创建的代理 |
4. 生命周期钩子
4.1 配置项的形式 ------ 在 setup() 之外写
与 Vue2 的区别之一:原来的 beforeDestroy() 和 destroyed() 变为 beforeUnmount() 和 unmounted()。
js
export default {
name: 'Demo',
setup() { ... },
// 通过配置项的形式使用生命周期钩子
beforeCreate() { console.log('---beforeCreate---') },
created() { console.log('---created---') },
beforeMount() { console.log('---beforeMount---') },
mounted() { console.log('---mounted---') },
beforeUpdate() { console.log('---beforeUpdate---') },
updated() { console.log('---updated---') },
beforeUnmount(){ console.log('---beforeUnmount---') },
unmounted() { console.log('---unmounted---') }
}
4.2 组合式 API 的写法 ------ 在 setup() 内部写
需要从 vue 中引入,在函数名之前加上 on;setup() 相当于 beforeCreate() 及 created()。
js
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
setup() {
// 在 setup 内部使用组合式生命周期钩子
onBeforeMount(() => { console.log('---onBeforeMount---') })
onMounted(() => { console.log('---onMounted---') })
onBeforeUpdate(() => { console.log('---onBeforeUpdate---') })
onUpdated(() => { console.log('---onUpdated---') })
onBeforeUnmount(() => { console.log('---onBeforeUnmount---') })
onUnmounted(() => { console.log('---onUnmounted---') })
}
注:
setup()比beforeCreate()先执行的前提是这两个函数是同级写法(即 4.1 的配置项写法)。
4.3 生命周期对照表
Vue3.0 提供 Composition API 形式的生命周期钩子,与 Vue2.x 钩子对应关系如下:
| Vue2.x 选项式钩子 | Vue3.x 组合式钩子 |
|---|---|
beforeCreate |
setup() |
created |
setup() |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
补充:还有两个较少用的组合式钩子:
onActivated/onDeactivated:配合<keep-alive>缓存组件时触发;onErrorCaptured:捕获后代组件传递的错误时触发。
5. 全局事件总线(mitt)
Vue3 中移除了 $on、$off、$once 等实例方法,官方推荐使用第三方库 mitt 实现全局事件总线(类似于 Vue2 的 EventBus)。
5.1 安装
bash
npm i mitt
5.2 创建事件总线文件
创建文件:src/utils/event-bus.js
js
import mitt from 'mitt'
export default mitt()
5.3 使用 ------ 以子组件向父组件传值为例
原则:谁接收,谁绑定(on);谁传值,谁调用(emit)。
html
<!-- App.vue(父组件,接收方) -->
<template>
<User></User>
</template>
<script>
// 导入全局事件总线对象
import emitter from './utils/event-bus.js'
import User from './components/User.vue'
import { onMounted } from 'vue'
export default {
name: 'App',
components: { User },
setup() {
// 生命周期钩子:mounted
onMounted(() => {
// 给全局事件总线绑定事件(第二个参数必须传函数!)
emitter.on('event1', showInfo)
})
function showInfo(user) {
alert(`姓名:${user.name},年龄:${user.age}岁`)
}
return { showInfo }
}
}
</script>
html
<!-- User.vue(子组件,传值方) -->
<template>
<button @click="triggerEvent1">触发全局事件总线上的event1事件</button>
</template>
<script>
import emitter from '../utils/event-bus.js'
export default {
name: 'User',
setup() {
function triggerEvent1() {
// 触发全局事件总线上的事件 event1 并传参
emitter.emit('event1', { name: '张三', age: 30 })
}
return { triggerEvent1 }
}
}
</script>
5.4 解绑事件
js
// 解绑全部事件
function clearAllEvent() {
emitter.all.clear()
}
// 解绑指定事件
function clearEvent1() {
emitter.off('event1')
}
易错点 :
emitter.on('event1', 'showInfo')是错误 写法 ------ mitt 的on方法第二个参数必须是回调函数 ,不能传字符串;应写成emitter.on('event1', showInfo)。
6. 其他知识点
6.1 钩子函数 hook(自定义 hook)
本质就是代码复用,与 Vue2 中的 mixin 类似,但更灵活、更清晰。
使用步骤:
- 在
src/hooks/目录下新建 js 文件,以该函数功能命名,例如sum.js:
js
// src/hooks/sum.js
import { reactive } from 'vue'
export default function () {
// data
let data = reactive({
num1: 0,
num2: 0,
result: 0
})
// methods
function sum() {
data.result = data.num1 + data.num2
}
// 返回一个对象
return { data, sum }
}
- 在组件中导入并调用:
html
<!-- App.vue -->
<template>
<div>
数字2: <input type="number" v-model="data.num2">
求和结果: {{data.result}}
<button @click="sum">求和</button>
</div>
</template>
<script>
import sum from './hooks/sum.js'
export default {
name: 'App',
setup() {
// 调用钩子函数
let result = sum()
// 返回一个对象
return result
}
}
</script>
6.2 shallowRef 组合式 API 之一
- 对于基本数据类型 ,结果上与
ref()一样; - 对于对象数据类型 ,使用后该数据的内部属性不具有响应式(不会创建 Proxy 代理对象);
- 使用场景:当对象中的数据不会改变 ,或以后要改变时是整体替换对象 (
data.value = {...}),此时用 shallowRef 效率更高。
js
import { ref, shallowRef } from 'vue'
export default {
name: 'App',
setup() {
// shallowRef 和 ref 用在基本数据类型上,没有区别。
// 数据本身就是基本数据类型,再浅也浅不到哪儿去。
// let counter = ref(1)
// let counter = shallowRef(1)
// 当 shallowRef 和 ref 都用在对象上的时候:
// ref 是有响应式的,底层会创建 Proxy 代理对象;
// shallowRef 是没有响应式的,底层也不会创建 Proxy 对象。
// 有的时候,这个对象中的属性可能永远都不可能修改,如果要改,也是更换对象。
// 显然这个时候可以使用 shallowRef,进行优化。
let data = shallowRef({ counter: 1 })
console.log(data)
function changeCounter() {
// 整体替换对象(触发响应式)
data.value = { counter: 10000 }
}
return { data, changeCounter }
}
}
6.3 shallowReactive 组合式 API 之一
与 reactive 的区别:shallowReactive 只支持第一层的响应式,深层嵌套的对象没有响应式。
js
import { shallowReactive } from 'vue'
export default {
name: 'App',
setup() {
let data = shallowReactive({
counter2: 1000, // 第一层,有响应式
a: { // 深层嵌套
b: {
c: { counter: 1 } // 无响应式,修改不会触发更新
}
}
})
return { data }
}
}
html
<template>
<h2>计数器:{{data.a.b.c.counter}}</h2> <!-- 深层,不响应 -->
<button @click="data.a.b.c.counter++">点我加1</button>
<h2>计数器:{{data.counter2}}</h2> <!-- 第一层,响应 -->
<button @click="data.counter2++">点我加1</button>
</template>
6.4 readonly 与 shallowReadonly ------ 深只读与浅只读
将响应式对象再套一层 readonly() 或 shallowReadonly():
readonly():深只读 ,不允许修改该对象中任意层级的数据;shallowReadonly():浅只读 ,只不允许修改对象第一层的数据。
应用场景:当其他组件将数据传递过来但不允许被修改时,可以使用 readonly 保护数据。
js
import { reactive, readonly, shallowReadonly } from 'vue'
let car = reactive({ name: '奔驰', price: '40W' })
let car2 = readonly(car) // 深只读:car2.name / car2.price 都不可修改
let car3 = shallowReadonly(car) // 浅只读:car3.name 不可修改,嵌套层可修改
6.5 toRaw 与 markRaw
| API | 作用 |
|---|---|
toRaw |
将响应式对象转换为普通对象 (只适用于 reactive 生成的响应式对象) |
markRaw |
标记某个对象,让这个对象永远都不具备响应式。比如集成第三方库时,有一个巨大的只读列表,不让其具备响应式是一种性能优化 |
js
// toRaw 示例
import { reactive, toRaw } from 'vue'
export default {
name: 'App',
setup() {
// data 是具备响应式的
let data = reactive({ counter1: 1 })
// 获取 data 这个响应式对象的原始对象
// 操作原始对象的时候,没有响应式处理的
function getRawObject() {
let rawObj = toRaw(data) // rawObj 就是 data 的原始对象
rawObj.counter1++ // 修改原始对象,不会具有响应式
console.log('原始对象', rawObj)
}
return { data, getRawObject }
}
}
html
<template>
<h2>计数器1:{{data.counter1}}</h2>
<button @click="data.counter1++">计数器加1</button>
<button @click="getRawObject">获取data的原始对象</button>
</template>
6.6 customRef ------ 自定义 ref
customRef 用于创建自定义的 ref 对象,可以显式控制依赖追踪(track)与触发更新(trigger)的时机。
js
// 自定义一个防抖 ref(示例)
import { customRef } from 'vue'
function useDebouncedRef(value) {
// 通过调用 customRef 函数来获取一个自定义的 ref 对象
// 必须给该函数传递一个回调(可称为 factory)
// 回调有两个重要参数:track 是追踪,trigger 是触发
const x = customRef((track, trigger) => {
return {
// 模板语句中只要使用到该数据,get 会自动调用
get() {
console.log('get执行了')
track() // 告诉 Vue 去追踪这个 value 的变化
return value
},
// 当模板语句中修改该数据的时候,set 会自动调用
set(newValue) {
console.log('set执行了', newValue)
value = newValue
trigger() // 触发(通知去调用 get 方法,更新界面)
}
}
})
return x
}
// 使用自定义的 ref 同样具备响应式
let name = useDebouncedRef('test')
6.7 teleport 组件 ------ 将内容传送到指定位置
<teleport> 将内容传送到 to 属性指定的位置,to 属性可以写 HTML 标签(如 body)或 CSS 选择器(如 #app)。
html
<template>
<teleport to="body">
<div v-if="isShow" class="mask">
<div class="dialog">
<h3>我是一个弹窗</h3>
<h4>一些内容</h4>
<h4>一些内容</h4>
<h4>一些内容</h4>
<button @click="isShow = false">关闭弹窗</button>
</div>
</div>
</teleport>
</template>
应用场景:弹窗、提示框、loading 等需要脱离当前组件层级(如避免被
overflow: hidden/z-index影响)的内容。
6.8 defineAsyncComponent ------ 动态/异步引入组件
参数是一个回调函数,需要返回一个 import() 的 Promise。
js
<script>
// import Child from './components/Child' // 静态引入
import { defineAsyncComponent } from 'vue'
// 异步引入
const Child = defineAsyncComponent(() => import('./components/Child'))
export default {
name: 'App',
components: { Child },
}
</script>
静态引入与动态引入的区别:当网速慢、页面加载时------
- 静态引入 :所有组件打包在同一个 bundle 中,会等所有组件都加载完成后一起展示;
- 动态引入(异步) :每个异步组件独立加载,先加载完成的先展示,可加快首屏渲染速度。
6.9 Suspense 组件
如果采用异步引入,可能会造成一个问题:当加载过于慢时会导致用户以为页面仅有这些内容。为此可以采用 <Suspense> 组件解决。
<Suspense> 底层采用的是具名插槽 :当组件加载完毕 时就展示该组件(default 插槽);当组件还在加载中 时展示指定内容(fallback 插槽)。
html
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<!-- 异步组件加载完成后展示的内容 -->
<template v-slot:default>
<Child/>
</template>
<!-- 异步组件加载中展示的内容 -->
<template v-slot:fallback>
<h3>稍等,加载中...</h3>
</template>
</Suspense>
</div>
</template>
<script>
// import Child from './components/Child' // 静态引入
import { defineAsyncComponent } from 'vue'
const Child = defineAsyncComponent(() => import('./components/Child')) // 异步引入
export default {
name: 'App',
components: { Child },
}
</script>
补充:
Suspense在 Vue 3.x 中长期处于实验性状态(experimental),Vue 3.5+ 中已标记为稳定,但日常开发仍建议优先使用异步组件 + 加载状态组件(如骨架屏)的方案。
6.10 setup 语法糖(<script setup>)
原版写计数器:
vue
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
let counter = ref(100)
function add() {
counter.value++
}
return { counter, add }
}
}
</script>
<template>
<h2>计数器: {{counter}}</h2>
<button @click="add">计数器加1</button>
</template>
使用 setup 语法糖写计数器:
vue
<script setup>
import { ref } from 'vue'
// data
let counter = ref(100)
// methods
function add() {
counter.value++
}
// watch
// computed
// ...
</script>
<template>
<h2>计数器:{{counter}}</h2>
<button @click="add">计数器加1</button>
</template>
语法糖的优势:
- 无需写
export default {}、setup(){}、return {...}; - 组件无需 components 注册,只需 import 即可直接使用;
- 顶层变量/函数自动暴露给模板。
使用语法糖后如何获取 setup() 中的 props 和 context 参数:
vue
<script setup>
// 1. props ------ 使用 defineProps 定义组件的 props(自动推断类型;TS 中需显式定义类型)
const props = defineProps({
title: String,
// 其他 props 定义...
});
console.log(props.title);
// 2. emits ------ 使用 defineEmits 定义组件可以发出的事件
const emit = defineEmits(['update:modelValue', 'otherEvent']);
// 使用 emit
emit('update:modelValue', newValue);
// 3. 插槽与属性 ------ 使用 useSlots / useAttrs 访问插槽和 attrs
import { useSlots, useAttrs } from 'vue';
const slots = useSlots();
const attrs = useAttrs();
console.log(slots.default());
console.log(attrs.id);
</script>
6.10.1 父组件向子组件传值(defineProps)
父组件给子组件传递数据的写法与 Vue2 一致;子组件通过 defineProps({...}) 接收,参数是一个对象,对象中键为属性名,值为数据类型(或详细配置)。该函数返回一个 Proxy 对象,存储父组件传过来的属性名和值,使用时直接写属性名即可。
vue
<!-- App.vue(父组件) -->
<script setup>
// setup 语法糖下局部组件无需注册,直接可以使用
import SonCom from './son-com.vue'
</script>
<template>
<div class="father">
<h2>父组件App</h2>
<!-- 1. 绑定属性 -->
<SonCom message="father message"/>
</div>
</template>
vue
<!-- son-com.vue(子组件) -->
<script setup>
// 2. defineProps 接收数据
const props = defineProps({
message: String
})
console.log(props) // Proxy {message: 'father message'}
</script>
<template>
<div class="son">
<h3>子组件Son</h3>
<div>父组件传入的数据 - {{ message }}</div>
</div>
</template>
为属性设置更多细节(类型校验、默认值、必填等):
vue
<script setup>
const props = defineProps({
names: {
type: Array,
default: () => [],
// required: true, // 是否必传
// validator: (value) => value.length > 0 // 自定义校验
}
});
</script>
6.10.2 子组件向父组件传值(defineEmits)
父组件在子组件标签中绑定自定义事件(写法与 Vue2 一致);子组件通过 defineEmits([...]) 声明事件(参数是数组,元素为事件名),返回函数 emit;通过 emit 在子组件中调用父组件传来的函数,需要传递的数据以参数形式发送到父组件。
vue
<!-- App.vue(父组件) -->
<script setup>
import SonCom from './son-com.vue'
// 参数用于接收子组件传来的数据
const getMessage = (msg) => {
console.log(msg)
}
</script>
<template>
<div class="father">
<h2>父组件App</h2>
<!-- 1. 绑定事件 -->
<SonCom @get-message="getMessage" />
</div>
</template>
vue
<!-- son-com.vue(子组件) -->
<script setup>
// 2. 通过 defineEmits() 声明事件(相当于 Vue2 的 this.$emit)
const emit = defineEmits(['get-message'])
const sendMsg = () => {
// 触发自定义事件,传数据给父组件
emit('get-message', 'this is son message')
}
</script>
<template>
<div class="son">
<div>子组件Son</div>
<button @click="sendMsg">触发自定义事件</button>
</div>
</template>
6.11 ref 属性 ------ 模板引用
思想与 Vue2 的 ref 属性类似,用于获取 DOM 元素或子组件实例。
vue
<script setup>
import { ref, onMounted } from 'vue'
import TestCom from './test-com.vue'
// 1. 调用 ref 函数 -> ref 对象
const h1Ref = ref(null)
const comRef = ref(null)
// 组件挂载完毕之后才能获取
onMounted(() => {
console.log(h1Ref.value) // DOM 元素
console.log(comRef.value) // 子组件实例
})
</script>
<template>
<!-- 2. 通过 ref 标识绑定 ref 对象 -->
<h1 ref="h1Ref">我是dom标签h1</h1>
<TestCom ref="comRef" />
</template>
注:模板实例需要在组件挂载完毕(onMounted)后才能获取到。
setup 语法糖下获取子组件内部数据 ------ defineExpose:
在 <script setup> 语法糖下,通过 ref 获取到组件对象后,该组件内部的数据是默认不开放给父组件 的;若想获取子组件的数据,需要在子组件中使用 defineExpose({...}) 将需要暴露的值写上。
vue
<!-- test-com.vue(子组件) -->
<script setup>
import { ref } from 'vue'
const name = ref('test name')
const setName = () => {
name.value = 'test new name'
}
// 通过该函数把组件内部的值暴露给父组件
defineExpose({
name,
setName
})
</script>
<template>
<div>我是test组件</div>
</template>
补充(Vue 3.5+):新写法
useTemplateRef('h1Ref')可以替代"ref + 同名变量"的繁琐声明,模板中ref="h1Ref"与const h1Ref = useTemplateRef('h1Ref')自动关联。
6.12 数据共享仓库:Pinia
Pinia 是 Vue 的专属最新状态管理库,是 Vuex 的替代品。
优势:
- 提供更加简单的 API(去掉了 mutation);
- 提供组合式风格的 API,与 Vue3 新语法统一;
- 去掉了 modules 的概念,每一个 store 都是一个独立的模块;
- 搭配 TypeScript 一起使用提供可靠的类型推断。
安装与配置:
bash
# 创建项目、安装依赖(npm i)后:
npm install pinia
js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia' // 1. 导入 createPinia
const pinia = createPinia() // 2. 执行方法得到实例
createApp(App)
.use(pinia) // 3. 把 pinia 实例加入到 app 应用中
.mount('#app')
定义 store(组合式写法,推荐):
创建 src/stores/counter.js:
js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// 定义数据 (state)
const count = ref(0)
// 定义修改数据的方法 (action 同步+异步)
const increment = () => {
count.value++
}
// 定义 getter(实际运用 computed)
const doubleCount = computed(() => count.value * 2)
// 以对象的方式 return 供组件使用
return { count, increment, doubleCount }
})
在组件中使用:
vue
<script setup>
// 1. 导入 use 打头的方法
import { useCounterStore } from '@/stores/counter'
// 2. 执行方法得到 store 实例对象
const counterStore = useCounterStore()
console.log(counterStore)
</script>
<template>
<button @click="counterStore.increment">{{ counterStore.count }}</button>
</template>
getter 与异步 action:
js
// store 中
const doubleCount = computed(() => count.value * 2) // getter
const list = ref([])
const getList = async () => {
const res = await axios.get(API_URL)
list.value = res.data.data.channels
}
vue
<script setup>
import { useCounterStore } from '@/store'
import { onMounted } from 'vue'
const counterStore = useCounterStore()
onMounted(() => { counterStore.getList() })
</script>
<template>
<button @click="counterStore.increment">{{ counterStore.doubleCount }}</button>
<ul>
<li v-for="item in counterStore.list">{{ item }}</li>
</ul>
</template>
响应式丢失问题与 storeToRefs:
⚠️ 若数据运用解构赋值 会使得响应式丢失!!!方法解构则无碍!
vue
<script setup>
import { useCounterStore } from '@/stores/counter'
import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
console.log(counterStore)
// ❌ 直接解构赋值(响应式丢失)
// const { count, doubleCount } = counterStore
// ✔ 方法包裹(保持响应式更新)
const { count, doubleCount } = storeToRefs(counterStore)
// 触发 action
onMounted(() => { counterStore.getList() })
</script>
<template>
<button @click="counterStore.increment">{{ count }}</button>
{{ doubleCount }}
</template>
注:方法直接解构赋值即可 (方法本身没有响应式概念),如
const { increment } = counterStore;只有 state / getter 数据需要用storeToRefs包裹。
6.13 路由(vue-router)
6.13.1 获取路由的 query 参数
js
import { useRoute, useRouter } from 'vue-router';
export default {
setup() {
const route = useRoute(); // 当前路由实例(相当于 this.$route)
const router = useRouter(); // 路由器实例(相当于 this.$router)
const userId = route.query.id;
const userName = route.query.name;
console.log(userId); // 输出查询参数 id
console.log(userName); // 输出查询参数 name
// 也可以使用 router.currentRoute.value.query
// const userId = router.currentRoute.value.query.id;
// const userName = router.currentRoute.value.query.name;
return {};
}
}
使用场景:
router.push({ path: '/user', query: { id: 1, name: '张三' } })跳转后通过route.query读取。
6.13.2 获取路由的 params 参数
动态路由配置:
js
{
path: '/user/:id',
component: UserComponent
}
组件中获取参数:
js
import { useRoute } from 'vue-router';
export default {
setup() {
const route = useRoute();
const userId = route.params.id;
return { userId };
}
}
使用场景:
router.push({ path: '/user/123' })跳转后通过route.params.id读取123。
6.13.3 动态路由(不同账户登录后有不同的权限路由)
【1】权限校验(账户密码)------ 采用 Element Plus 中的 <el-form> 组件绘制登录页面
<el-form> 组件主要需要以下三个属性:
ref:用于绑定该组件实例;:model:用于绑定数据对象;:rules:用于绑定校验规则。
<el-form> 下的子组件:
<el-form-item>:主要需要prop属性,用于绑定规则校验的字段名;<el-input>:主要需要指令v-model,用于绑定对应的数据对象中的具体数据;<el-button>:用于提交,主要写一个点击事件,该事件参数为 el-form 组件中ref属性绑定的值。
html
<template>
<el-form ref="ruleFormRef" :model="temp" :rules="rules" label-width="120px" class="demo-ruleForm">
<el-form-item label="用户名" prop="account">
<el-input v-model="temp.account"/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="temp.password"/>
</el-form-item>
<el-form-item>
<el-button type="success" @click="submit(ruleFormRef)">登录</el-button>
</el-form-item>
</el-form>
</template>
数据对象与校验规则:
js
// 初始化表单 数据对象
const temp = reactive({
account: '',
password: ''
})
// 表单校验
const rules = reactive({
account: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 18, message: '长度在2-18个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 3, max: 18, message: '长度在3-18个字符', trigger: 'blur' },
]
})
登录提交:
js
const submit = (formEl) => {
if (!formEl) return;
formEl.validate(async valid => {
if (valid) { // 校验成功
// 发送请求
let { data: { uid, token, msg } } = await login(temp);
if (uid) {
// token 缓存
setToken(token);
// 跳转
router.push('/home')
}
} else { // 失败
console.log('error submit!')
}
})
}
【2】根据 token 发请求 ------ 对 axios 的封装
① 初次封装 :新建 src/http/index.js,创建 axios 实例(包含 baseURL、timeout 配置)、请求拦截器(将登录后获取的 token 设置在请求头 header 中)、响应拦截器(对返回的状态码进行对应的处理)。
js
import axios from 'axios'
import { getToken } from '../utils/auth'
// 处理访问的 url
const service = axios.create({
baseURL: "http://127.0.0.1:7003/",
timeout: 1000
})
// 请求拦截器 --- 在发送请求之前做的事
service.interceptors.request.use(config => {
config.headers['token'] = getToken();
return config;
})
// 响应拦截器 --- 对响应数据的处理
service.interceptors.response.use(response => {
return response
}, err => {
return Promise.reject(err);
})
export default service;
② 二次封装 :在 src/http/http.js 中封装对应后端接口的请求方法。
js
import axios from '.'
// 登录
export const login = data => axios.post(`itlove/login`, data);
// 获取 getResources(权限资源)
export const getResources = () => axios.get(`getResources`);
③ 调用请求接口的方法:对应登录接口,账户密码正确后会拿到 token,将 token 进行本地存储;在请求拦截器中从本地获取 token 添加到请求头 headers 中。登录对应账户后,后端根据账户权限返回对应的路由信息(该信息可通过后端接口获取)。
【3】处理获取到的权限路由数据
④ 将获取到的权限路由数据存到 Pinia 中,在路由配置文件中设置路由守卫 beforeEach:
- 每当发生路由跳转时判断 Pinia 是否有数据:有则代表已将该账户的权限数据缓存,直接放行(避免不断点击路由重复发请求);
- 没有则代表数据没缓存,发起获取权限数据的接口,并存放到 Pinia 中;
- 拿到权限数据后,将该数据处理成路由格式(
{path: xxx, name: xxx, component: () => import(xxx), children: xxx}),传给router.addRoute()。
js
// 路由守卫(router/index.js)
router.beforeEach(async (to, from, next) => {
if (to.path === '/login') {
next();
} else {
if (mainStore() && mainStore().nav.length === 0) { // 没有缓存
// 1、发送请求
const { data: res } = await getResources();
// 2、缓存数据
mainStore().setNav(res);
// 3、服务端返回的数据转换为路由数据
const dynamicRoutes = addDynamicRoutes(res);
// 4、动态路由配置,传入路由格式的数据
router.addRoute(dynamicRoutes)
next({ path: to.path });
} else {
next();
}
}
})
js
// 将后端返回的权限数据转成路由配置
function addDynamicRoutes(res) {
// 1、home 添加 children
const homeRoutes = routes.filter(v => v.path === '/home')[0];
homeRoutes.children = [];
// 2、追加配置
res.forEach(item => {
homeRoutes.children.push({
path: item.path,
name: item.name,
component: () => import(`../views/home/content/${item.component}/index.vue`)
})
})
return homeRoutes;
}
js
// store/index.js(Pinia 缓存权限数据)
import { defineStore } from 'pinia'
export const mainStore = defineStore('main', {
state: () => ({
nav: []
}),
actions: {
setNav(v) {
this.nav = v;
}
}
})
【4】退出登录
需要清空 Pinia 以及 token,避免下一次请求时返回的是上一次的数据。
js
function logout() {
// 清空 pinia 数据
mainStore().$reset()
// 清空 token
removeToken()
// 跳转登录页
router.push('/login')
}
6.14 插槽(Slot)
6.14.1 默认插槽
vue
<!-- A.vue(子组件) -->
<template>
<div>
<h1>子:A组件</h1>
<header>
<div>头部</div>
<slot></slot> <!-- 默认插槽(匿名插槽) -->
</header>
<footer>
<div>底部</div>
<slot></slot>
</footer>
</div>
</template>
vue
<!-- Home.vue(父组件) -->
<script setup>
import A from '../components/A.vue'
</script>
<template>
<div>
<!-- 标签体内容会填充到子组件的 <slot></slot> 位置 -->
<A>这是xxxx数据</A>
</div>
</template>
6.14.2 具名插槽
vue
<!-- A.vue(子组件) -->
<template>
<div>
<h1>子:A组件</h1>
<header>
<div>头部</div>
<slot name='xxx'></slot> <!-- 具名插槽 xxx -->
</header>
<footer>
<div>底部</div>
<slot name='yyy'></slot> <!-- 具名插槽 yyy -->
</footer>
</div>
</template>
vue
<!-- Home.vue(父组件) -->
<script setup>
import A from '../components/A.vue'
</script>
<template>
<div>
<A>
<!-- 完整写法:v-slot:插槽名 -->
<template v-slot:xxx>
这是xxxxx数据
</template>
<template v-slot:yyy>
这是yyyyy数据
</template>
</A>
</div>
</template>
简写:v-slot:xxx 简写为 #xxx
vue
<template>
<div>
<A>
<template #xxx>这是xxxx数据</template>
<template #yyy>这是yyyyy数据</template>
</A>
</div>
</template>
6.14.3 作用域插槽
子组件通过插槽向父组件传数据,父组件在插槽内容中接收。
vue
<!-- A.vue(子组件) -->
<script setup>
import { ref } from 'vue'
let list = ref([
{ id: 1, name: '张三', age: 18 },
{ id: 2, name: '张大三', age: 22 },
{ id: 3, name: '张小三', age: 33 },
{ id: 4, name: '里斯', age: 55 },
]);
</script>
<template>
<div>
<header>
<slot name='xxx'></slot>
<slot name='yyy'></slot>
</header>
<section>
<div>中间</div>
<!-- 作用域插槽:通过 :data='item' 把数据传给插槽使用者 -->
<div v-for='item in list' :key='item.id'>
<slot :data='item'></slot>
</div>
</section>
<footer>
<div>底部</div>
<slot name='xxx'></slot>
</footer>
</div>
</template>
vue
<!-- Home.vue(父组件) -->
<script setup>
import A from '../components/A.vue'
</script>
<template>
<div>
<A>
<template #xxx>这是xxxxx数据</template>
<!-- v-slot='{data}' 解构接收子组件传出的数据 -->
<template v-slot='{data}'>
{{ data.name }} --> {{ data.age }}
</template>
<template #yyy>这是yyyyy数据</template>
</A>
</div>
</template>
简写: 默认插槽的作用域写法也可写作 #default='{data}'(显式指名默认插槽):
vue
<template>
<div>
<A>
<template #xxx>这是xxxxx数据</template>
<template #default='{data}'>
{{ data.name }} --> {{ data.age }}
</template>
<template #yyy>这是yyyyy数据</template>
</A>
</div>
</template>
6.14.4 动态插槽
插槽名可以是动态的(由变量控制)。
vue
<!-- Home.vue(父组件) -->
<script setup>
import A from '../components/A.vue'
// 可动态更改 str 的值来切换插槽
let str = ref('xxx');
</script>
<template>
<div>
<A>
<!-- 动态插槽名:#[str] -->
<template #[str]>这是xxxxx数据</template>
<template #default='{data}'>
{{ data.name }} --> {{ data.age }}
</template>
<template #yyy>这是yyyyy数据</template>
</A>
</div>
</template>
6.15 插件(Plugin)
插件是自包含的代码,通常用来为 Vue 添加全局功能。Vue3 中有以下几种常见写法。
6.15.1 写法 1:install()
① 在 js 文件中定义插件:
js
// plugin_libs/plug_1.js
// 第一个插件
export default {
install: (app, options) => {
// 添加全局实例的方法
app.config.globalProperties.$xxplug = key => {
// 插件方法的业务逻辑,就可以写在这里
console.log('第一个插件')
}
}
}
app参数即 app 实例,options参数即 main.js 中.use()该插件所传入的第二个参数。
② 在 main.js 中引入并使用该插件(可传入参数):
js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 插件
import plug_1 from './plugin_libs/plug_1'
createApp(App)
.use(plug_1, '第一个插件,main.js 调用的,.use方法的第二个参数')
.use(store)
.use(router)
.mount('#app')
③ 在组件中使用:
js
import { getCurrentInstance } from 'vue'
export default {
name: 'HelloWorld',
props: { msg: String },
setup() {
// proxy 可以在开发环境、生产环境下都能拿到组件实例(推荐)
const { proxy } = getCurrentInstance();
proxy.$xxplug('我是老尚,欢迎关注一对一零基础课')
// ctx 只在开发环境好用,生产环境会报错,不推荐使用
// const { ctx } = getCurrentInstance();
// ctx.$xxplug('...')
}
}
注 :
ctx(即getCurrentInstance().ctx)只在开发环境有用,在生产环境下会报错,因此直接解构proxy代替ctx。
6.15.2 写法 2:将该插件文件作为函数导出
① 在 js 文件中导出一个函数:
js
// plugin_libs/plug_2.js
export function plugFn(key) {
console.log('第二个插件:' + key)
}
② 在组件中导入并调用该函数即可:
js
import { getCurrentInstance } from 'vue'
// 插件
import { plugFn } from '../plugin_libs/plug_2'
export default {
setup() {
plugFn('我是老尚') // 直接调用
}
}
6.15.3 写法 3:通过 provide 和 inject
① 在插件文件中定义插件方法并对外暴露两个方法:
js
// plugin_libs/plug_3.js
import { provide, inject } from 'vue'
// 私有方法
const _fn = key => {
console.log('第三个插件')
}
// 向后代组件传
export function provideFn() {
provide('onlyId', _fn);
}
// 子组件,从父组件接收
export function injectFn() {
return inject('onlyId')
}
② 在父组件引入并调用 provideFn:
js
// App.vue
import { provideFn } from './plugin_libs/plug_3'
export default {
setup() {
provideFn(); // 向所有后代组件提供 _fn 方法
}
}
③ 在子组件引入并调用 injectFn:
js
// children.vue(子组件)
import { injectFn } from '../plugin_libs/plug_3'
export default {
name: 'children',
setup() {
let msg = '子组件';
// 所有的子组件,都能拿到 _fn 方法
const fns = injectFn();
fns('第三个插件,我是老尚')
return { msg }
}
}
html
<template>
<div><h1>{{msg}}</h1></div>
</template>
7. Vue3 的其他改变
7.1 全局 API 调整
Vue3.0 中对这些 API 做出了调整:将全局的 API(即 Vue.xxx)调整到应用实例(app)上:
| 2.x 全局 API(Vue) | 3.x 实例 API(app) |
|---|---|
Vue.config.xxxx |
app.config.xxxx |
Vue.config.productionTip |
移除 |
Vue.component |
app.component |
Vue.directive |
app.directive |
Vue.mixin |
app.mixin |
Vue.use |
app.use |
Vue.prototype |
app.config.globalProperties |
说明:
Vue.config.productionTip(生产提示)在 3.x 中被移除;Vue.prototype改为app.config.globalProperties。这也让 Vue3 的 API 对 tree-shaking 更友好(按需打包)。
7.2 其他破坏性变更
(1)data 选项应始终被声明为一个函数。(与 Vue2 一致,但必须写成函数形式,防止多个组件实例共享同一份数据)
(2)过渡类名的更改:
css
/* Vue2.x 写法 */
.v-enter,
.v-leave-to {
opacity: 0;
}
.v-leave,
.v-enter-to {
opacity: 1;
}
css
/* Vue3.x 写法:v-enter 更名为 v-enter-from,v-leave 更名为 v-leave-from */
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.v-leave-from,
.v-enter-to {
opacity: 1;
}
(3)移除 keyCode 作为 v-on 的修饰符,同时也不再支持 config.keyCodes。 (Vue3 中应使用事件名而非按键码,如 @keyup.enter 可保留,但 @keyup.13 不再支持)
(4)移除 v-on.native 修饰符:
子组件的 emits 数组中未指定 的事件(如 click),会被识别为原生事件 ,父组件直接 @click 绑定即可,无需再使用 .native 修饰符:
html
<!-- 父组件中绑定事件 -->
<my-component
v-on:close="handleComponentEvent"
v-on:click="handleNativeClickEvent"
/>
js
// 子组件中声明自定义事件
export default {
emits: ['close'] // click 没在 emits 中指定,则代表原生事件
}
(5)移除过滤器(filter):
过滤器虽然看起来很方便,但它需要一种自定义语法,打破了大括号内表达式"只是 JavaScript"的假设,这不仅有学习成本,而且有实现成本!建议用方法调用 或计算属性去替换过滤器。
js
// Vue2: {{ message | capitalize }}
// Vue3 建议:
// 方法调用:{{ capitalize(message) }}
// 计算属性:computed(() => capitalize(message))
(6)其他移除/变更(补充):
- 移除实例方法
$on、$off、$once、$children(事件总线需借助 mitt 等第三方库); v-if与v-for同时用于同一元素时,v-if的优先级更高 (Vue2 中v-for优先);- 全局注册组件由
Vue.component改为app.component; filters、keyCode、.native等语法移除(如上)。
8. 补充:Vue3 常用特性速览
8.1 组件通信方式总结
| 通信场景 | 方式 | 说明 |
|---|---|---|
| 父 → 子 | props |
通过 defineProps 接收(语法糖)或 setup(props) |
| 子 → 父 | 自定义事件 emit |
通过 defineEmits 声明并触发(语法糖)或 context.emit |
| 父 ↔ 子(双向) | v-model |
见 8.2,也可自定义 update:xxx 事件 |
| 任意组件 | 全局事件总线 | mitt(Vue3 移除了 $on/$off) |
| 祖 → 孙 | provide / inject |
跨层级通信,无需逐层传 props |
| 任意组件 | Pinia |
官方状态管理库,替代 Vuex |
| 页面间 | vue-router 传参 |
query / params |
| 子组件向插槽传数据 | 作用域插槽 |
<slot :data="item"> + 父组件 v-slot="{data}" |
| 父组件访问子组件 | ref + defineExpose |
获取子组件实例及其暴露的数据/方法 |
8.2 v-model 与 defineModel
组件上使用 v-model(Vue3):
Vue3 中 v-model 等价于 :modelValue + @update:modelValue:
vue
<!-- 父组件 -->
<Child v-model="count" />
<!-- 等价于 -->
<Child :modelValue="count" @update:modelValue="count = $event" />
vue
<!-- 子组件(Vue3.3 及以前) -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
多个 v-model:
vue
<Child v-model:title="title" v-model:content="content" />
Vue 3.4+ 的 defineModel() 宏(简化写法):
vue
<script setup>
// 定义一个 v-model 绑定,返回一个 ref
const model = defineModel()
// 也可以带默认值与校验
// const model = defineModel({ default: '' })
</script>
<template>
<input v-model="model" />
</template>
8.3 模板语法与指令要点
(1)v-if 与 v-for 优先级 :Vue3 中同一元素上 v-if 优先于 v-for(与 Vue2 相反),因此不要在同一元素上同时使用 ,建议用 v-for 包裹一层或改用 v-show。
(2)key 的使用 :v-for 中务必使用 key;Vue3 中 key 不再自动生成在 v-bind 上。
(3)事件修饰符 :.stop、.prevent、.once、.capture、.self、.passive 等与 Vue2 一致;v-on 可直接缩写为 @。
(4)v-model 修饰符 :.lazy、.number、.trim 保留。
(5)v-bind 合并规则变化 :Vue3 中 class / style 的合并更智能;动态参数也支持 v-bind:[attrName]。
(6)<script setup> 顶层 await :配合 Suspense 使用,组件可写成 async。
8.4 其他新特性速览(补充)
| 特性 | 版本 | 说明 |
|---|---|---|
defineModel() |
Vue 3.4+ | 简化组件 v-model 的双向绑定 |
useTemplateRef() |
Vue 3.5+ | 更简洁的模板引用声明方式 |
watchPostEffect / watchSyncEffect |
Vue 3.5+ | 指定时机执行的 watchEffect 变体 |
onWatcherCleanup() |
Vue 3.5+ | 在侦听器回调中注册清理函数 |
useId() |
Vue 3.5+ | 生成唯一 id(用于表单标签关联等) |
data() 选项 |
--- | Vue3 中仍然支持选项式 API,但组件须为函数 |
Fragments(多根节点) |
--- | 模板不再强制要求单一根元素 |
Teleport |
--- | 将内容传送到指定 DOM 位置(见 6.7) |
Suspense |
--- | 异步组件加载占位(见 6.9) |