Vue3 快速上手
第一章:Vue3 简介
一、介绍
- 2020 年 9 月 18 日,Vue.js 发布 3.0 版本,代号:One Piece(海贼王)

- 耗时 2 年多、2600+次提交、30+个RFC、600+次PR、99位贡献者
- github 上的 tags 地址:https://github.com/vuejs/vue-next/releases/tag/v3.0.0
二、Vue3 带来了什么
性能的提升
打包大小减少 41%
初次渲染快 55%,更新渲染快 133%
内存减少 54%
......
源码的升级
使用 Proxy 代替 defineProperty 实现响应式
重写虚拟 DOM 的实现和 Tree-Shaking
......
拥抱 TypeScript
- Vue3 可以更好的支持 TypeScript
新的特性
Composition API(组合 API)
setup 配置
ref 与 reactive
watch 与 watchEffect
provide 与 inject
.....
新的内置组件
- Fragment
- Teleport
- Suspense
其他改变
配套的工程化工具也进行了更新:Vue-cli --> create-vue、Vuex --> pinia
新的生命周期钩子
data 选项应始终被声明为一个函数
移除 keyCode 支持作为 v-on 的修饰符
......
三、创建 Vue3.0 工程
1. vue-cli 创建
官方文档:Vue CLI
# 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
# 安装或者升级你的@vue/cli
npm install -g @vue/cli
# 创建
vue create vue_test
# 启动
cd vue_test
npm run serve2. create-vue 创建
create-vue 创建的项目使用的构建工具是 vite。
1)什么是 vite?新一代前端构建工具。
2)优势如下
- 开发环境中,无需打包操作,可快速的冷启动。
- 轻量快速的热重载(HMR)。
- 真正的按需编译,不再等待整个应用编译完成。


3)对比
| 特性 | Vue CLI | create-vue |
|---|---|---|
| 构建工具 | Webpack | Vite |
| 启动速度 | 较慢(10-30秒) | 极快(1-2秒) |
| 热更新 | 较慢 | 极快 |
| Vue 版本 | Vue 2/3 | Vue 3 |
| 官方推荐 | 已不推荐,进入维护模式 | ✅ 推荐 |
4)创建项目
# 创建工程
npm create vue@latest
# 进入工程目录
cd <project-name>
# 安装依赖
npm install
# 运行
npm run dev第二章:Composition API
https://cn.vuejs.org/guide/extras/composition-api-faq.html
一、是什么
使用传统 Options API 中,新增或者修改一个需求,就需要分别在 data、methods、computed 里查找并修改。


而 Composition API 让我们可以更加优雅的组织我们的代码、函数。让相关功能的代码更加有序的组织在一起。


二、响应式 API 核心
1. 拉开序幕的 setup
1)基本使用
Vue3.0 中一个新的配置项,值为一个函数。setup 是所有 Composition API(组合 API)“ 表演的舞台 ”。组件中所用到的:数据、方法等等,均要配置在 setup 中。
setup 函数的两种返回值
- 若返回一个对象,则对象中的属性、方法,在模板中均可以直接使用。(重点关注)
- 若返回一个渲染函数:则可以自定义渲染内容。(了解)
使用细节
尽量不要与 Vue2.x 配置混用
Vue2.x 配置(data、methos、computed ...)中可以访问到 setup 中的属性、方法。
但在 setup 中不能访问到 Vue2.x 配置(data、methos、computed...)。
因为 setup 中,this 是 undefined。
如果有重名,setup 优先。
setup 不能是一个 async 函数,因为返回值不再是 return 的对象,而是 Promise,模板看不到 return 对象中的属性。
后期也可以返回一个 Promise 实例,但需要 Suspense 和异步组件的配合。
setup 的两个注意点
setup 执行的时机
- 在 beforeCreate 之前执行一次,且 this 是 undefined。
setup 的参数
- props:值为对象,包含组件外部传递过来,且组件内部声明接收了的属性。
- context:上下文对象
- attrs:值为对象,包含组件外部传递过来,但没有在 props 配置中声明的属性,相当于
this.$attrs。 - slots:收到的插槽内容,相当于
this.$slots。 - emit:分发自定义事件的函数,相当于
this.$emit。
- attrs:值为对象,包含组件外部传递过来,但没有在 props 配置中声明的属性,相当于
jsexport default { props: { // 在父组件中给子组件传递了一个 msg props,子组件需要配置 props 来接收。如果不配置,不能通过 setup 中的第一个参数获取到,只能通过 setup 中的第二个参数获取到。且 Vue 会在浏览器控制台发出警告,让你配置这个 msg: String }, emits: ["myEvent"] // 在父组件中给子组件传递了一个 myEvent 自定义事件,虽然不配置 emits 选项,子组件也可以使用 emit 方法触发此事件,但 Vue 会在浏览器控制台发出警告,让你配置这个 setup(props, context) { // 在setup函数中,你可以访问props和context console.log(props.msg) console.log(context.attrs) console.log(context.slots) context.emit('myEvent') } }
2)语法糖
<script setup> 语法糖
在 SFC 中的 script 标签添加 setup 标记之后:
① 不再需要写 export default {}。因为里面的代码会被编译成组件 setup() 函数的内容。
② 不再需要 return。因为顶层的绑定会被暴露给模板。
③ 组件无需注册,可以直接使用。
④ 与普通的 <script> 只在组件被首次引入的时候执行一次不同。<script setup> 中的代码会在每次组件实例被创建的时候执行。
<script setup>
// message、logMessage 可以在 Vue 模板里直接使用
const message = 'this is message'
const logMessage = ()=>{
console.log(message)
}
</script>使用 setup 语法糖之后,必备的其他函数
下面的函数都是宏函数,可以直接使用,使用前无须导入。
defineProps() 和 defineEmits()
vue<script setup> // const props = defineProps(['name', 'age']) const props = defineProps({ name: { type: String, default: '' }, age: { type: Number, default: 0 } }) const emit = defineEmits(['changeAge']) function changeAge() { emit('changeAge', 200) } </script> <template> <h2>ShowInfo: {{ name }} - {{ age }}</h2> <button @click="changeAge">修改age</button> </template>defineExpose()
使用
<script setup>的组件,顶层属性默认是私有的。通过模板 ref 或者 $parent 能够获取到组件的公开实例,但不会暴露任何在<script setup>中声明的绑定。通过 defineExpose 编写暴露方法与属性在<script setup>组件中需要暴露出去的 property。vue<script setup> import { ref } from 'vue' const a = 1 const b = ref(2) defineExpose({ a, b }) </script>
2. 定义响应式数据
1)ref 函数
(1) 作用:定义一个响应式的数据。
(2) 语法:const xxx = ref(initValue)
创建一个包含响应式数据的引用对象(reference 对象,简称 ref 对象)。
JS 中操作数据:
xxx.value模板中读取数据:不需要 .value,直接:
<div>{{xxx}}</div>因为 vue3 会在解析模板的时候,发现是 RefImpl 的对象,会自动从 value 中读数据。
(3) 备注
- 接收的数据可以是:基本类型、也可以是对象类型。
- 基本类型的数据:响应式依然是靠
Object.defineProperty()的 get 与 set 完成的。 - 对象类型的数据:内部“ 求助 ”了 Vue3.0 中的一个新函数 —— reactive 函数。
(4) 使用 ref 获取元素对象
可以将这个引用绑定到模板中的元素或组件的 ref 属性上,然后在 setup 函数中访问这个元素或组件。
例如:创建一个引用并绑定到一个输入元素上:
import { ref, onMounted } from 'vue'
export default {
setup() {
const inputRef = ref(null)
onMounted(() => {
console.log(inputRef.value) // 打印出绑定的元素
})
return {
inputRef
}
}
}然后在模板中这样使用这个引用:
<input ref="inputRef" />inputRef 是一个响应式引用,它被绑定到一个输入元素上。当组件被挂载后,onMounted 钩子函数会被调用,可以在这个函数中访问 inputRef.value 来获取这个输入元素。
注意:需要在 setup 函数的返回值中包含 inputRef,这样它才能在模板中被访问。
2)reactive 函数
1)作用:定义一个对象类型的响应式数据(基本类型不要用它,要用 ref 函数)。
2)语法:const 代理对象 = reactive(源对象) 接收一个对象(或数组),返回一个代理对象(Proxy 的实例对象,简称 proxy 对象)
- reactive 定义的响应式数据是“深层次的”。
- 内部基于 ES6 的 Proxy 实现,通过操作代理对象来对源对象内部数据进行修改与增强。
reactive 对比 ref
- 从定义数据角度对比:
- ref 用来定义:基本类型数据。
- reactive 用来定义:对象(或数组)类型数据。
- 备注:ref 也可以用来定义对象(或数组)类型数据,它内部会自动通过 reactive 转为代理对象。
- 从原理角度对比:
- ref 通过
Object.defineProperty()的 get 与 set 来实现响应式(数据劫持)。 - reactive 通过使用 Proxy 来实现响应式(数据劫持), 并通过 Reflect 操作源对象内部的数据。
- ref 通过
- 从使用角度对比:
- ref 定义的数据:操作数据需要
.value,读取数据时模板中直接读取不需要.value。 - reactive 定义的数据:操作数据与读取数据均不需要
.value。
- ref 定义的数据:操作数据需要
3)toRef & toRefs
作用:创建一个 ref 对象,其 value 值指向另一个对象中的某个属性。
语法:const name = toRef(响应式对象,'属性')
应用:要将响应式对象中的某个属性单独提供给外部使用时。
toRefs:与 toRef 功能一致,但可以批量创建多个 ref 对象,这些对象的值会跟踪响应式对象的所有属性。它接收一个响应式对象作为参数,并返回一个新的对象,这个对象的每个属性都是一个 ref,这些 ref 的值会跟踪响应式对象的相应属性。例如,const personRefs = toRefs(user) 会返回一个新的对象,这个对象中的每个属性都是一个 ref,这些 ref 的值会跟踪 person 对象的相应属性。
export default {
setup() {
const user = reactive({
name: 'job',
age: 18
})
return {
...toRefs(user)
}
}
}
/*
return {
name: user.name, // 错误,非响应式。等价于 name: 'job'
age: ref(user.name) // 错误,非响应式。等价于 age: ref(8)
}
*/4)注意
口诀
解构对象要 toRefs
提取 value 失响应
unref toRaw 变普通
替换 reactive 需谨慎
函数传参传 ref 对象
扩展运算符也会丢
Props 解构要小心
异步赋值用 ref 包// ----------------------------------------------- 1. 解构对象要 toRefs, 勿要直接解构
const state = reactive({
count: 0,
name: 'Alice'
});
// ❌ 解构后失去响应式
const { count, name } = state;
count++; // ❌ 不会触发更新
name = 'Bob'; // ❌ 不会触发更新
// ----------------------------------------------- 2. 提取 ref 的 .value
const count = ref(0);
// ❌ 提取值后失去响应式
let num = count.value;
num++; // ❌ 不会影响 count
console.log(count.value); // 0(未改变)
// ----------------------------------------------- 3. 直接赋值替换响应式对象
let state = reactive({
count: 0
});
// ❌ 直接赋值,失去响应式
state = { count: 100 }; // 新对象不是响应式
// ✅ 方式1:修改属性
state.count = 100;
// ✅ 方式2:使用 Object.assign
Object.assign(state, { count: 100 });
// ✅ 方式3:ref 可以整体替换
const state = ref({
count: 0
});
state.value = { count: 100 }; // 响应式
// ----------------------------------------------- 4. 使用扩展运算符
const state = reactive({
count: 0,
name: 'Alice'
});
// ❌ 扩展后失去响应式
const newState = { ...state };
newState.count++; // ❌ 不会影响 state
// ----------------------------------------------- 5. 直接解构 props
const props = defineProps(['count', 'name']);
// ✅ 保持响应式
const { count, name } = toRefs(props);
watch(count, () => {
// ✅ 响应式
});
// ----------------------------------------------- 6. 异步后重新赋值 reactive
let data = reactive({ list: [] });
async function fetchData() {
const result = await fetch('/api/data');
// ❌ 重新赋值,失去响应式
data = reactive({ list: result });
}
// 方式1:修改属性
const data = reactive({ list: [] });
async function fetchData() {
const result = await fetch('/api/data');
data.list = result; // ✅ 响应式
}
// 方式2:使用 ref
const data = ref({ list: [] });
async function fetchData() {
const result = await fetch('/api/data');
data.value = { list: result }; // ✅ 响应式
}最佳实践
- ref 可以整体替换。
data.value = { count: 100 }; - reactive 只修改属性,不替换整体。
- 解构时用 toRefs。
3. 计算属性与监视
1)computed 函数
与 Vue2.x 中 computed 配置功能一致。
import { computed } from 'vue'
setup(){
// ...
// 计算属性——简写
let fullName = computed(()=>{
return person.firstName + '-' + person.lastName
})
// 计算属性——完整
let fullName = computed({
get(){
return person.firstName + '-' + person.lastName
},
set(value){
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
return {
fullName
}
}2)watch 函数
与 Vue2.x 中 watch 配置功能一致。
两个小“坑”:
- 监视 reactive 定义的响应式数据时:oldValue 无法正确获取、强制开启了深度监视(deep 配置失效)。
- 监视 reactive 定义的响应式数据中某个属性时:deep 配置有效。
// 情况一:监视ref定义的响应式数据
watch(sum, (newValue, oldValue) => {
console.log('sum变化了', newValue, oldValue)
}, {immediate:true})
// 情况二:监视多个ref定义的响应式数据
watch([sum, msg], (newValue, oldValue) => {
console.log('sum或msg变化了', newValue, oldValue)
})
/* 情况三: 监视reactive定义的响应式数据
若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!!
若watch监视的是reactive定义的响应式数据,则强制开启了深度监视
*/
watch(person, (newValue, oldValue) => {
console.log('person变化了', newValue, oldValue)
}, {immediate:true,deep:false}) // 此处的deep配置不再奏效
// 情况四:监视reactive定义的响应式数据中的某个属性
watch(()=>person.age, (newValue, oldValue) => {
console.log('person的job变化了', newValue, oldValue)
}, {immediate:true,deep:true})
// 情况五:监视reactive定义的响应式数据中的某些属性
watch([()=>person.age, ()=>person.name], (newValue, oldValue) => {
console.log('person的job变化了', newValue, oldValue)
}, {immediate:true,deep:true})
// 特殊情况
// 无法正确获得oldValue!!
watch(()=>person.job, (newValue, oldValue) => {
console.log('person的job变化了', newValue, oldValue)
}, {deep:true}) // 此处由于监视的是reactive定义的对象中的某个属性,所以deep配置有效注意:watch 监视源只能是 getter/effect 函数、ref、reactive 对象或数组这些类型。
如果 watch 监视的是 ref (原始类型) ,不用写 value;而如果是 ref (对象) 需要 value 或者开启 deep 配置。
3)watchEffect 函数
作用:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
watch 的套路:既要指明监视的属性,也要指明监视的回调。
watchEffect 的套路:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
watchEffect 有点像 computed:
- 但 computed 注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
- 而 watchEffect 更注重的是过程(回调函数的函数体),所以不用写返回值。
// watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
const stop = watchEffect(()=>{
const x1 = sum.value
const x2 = person.age
console.log(x1, x2)
console.log('watchEffect配置的回调执行了!')
})
// 当不再需要此侦听器时
stop()4. 生命周期
官方文档:https://cn.vuejs.org/api/options-lifecycle.html
https://cn.vuejs.org/guide/essentials/lifecycle.html


Vue3.0 中可以继续使用 Vue2.x 中的生命周期钩子,但有有两个被更名:
- beforeDestroy 改名为 beforeUnmount
- destroyed 改名为 unmounted
Vue3.0 也提供了 Composition API 形式的生命周期钩子,与 Vue2.x 中钩子对应关系如下:
- beforeCreate ==> setup()
- created ==> setup()
- beforeMount ==> onBeforeMount
- mounted ==> onMounted
- beforeUpdate ==> onBeforeUpdate
- updated ==> onUpdated
- beforeUnmount ==> onBeforeUnmount
- unmounted ==> onUnmounted
Composition API 的生命周期钩子定义多次的时候,会按照顺序依次执行。而原来的选项式 API 可以使用混入来实现多次执行。
5. 自定义 hook / 组合式函数
官方文档:https://cn.vuejs.org/guide/reusability/composables.html
什么是 hook?本质是一个函数,把 setup 函数中使用的 Composition API 进行了封装。类似于 vue2.x 中的 mixin。
优势:复用代码,让 setup 中的逻辑更清楚易懂。
1)例子一
如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的:
<template>
Mouse position is at: {{ x }}, {{ y }}
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>但是,如果我们想在多个组件中复用这个相同的逻辑呢?我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中:
// 文件位置:./src/hook/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}下面是它在组件中使用的方式:
<template>
Mouse position is at: {{ x }}, {{ y }}
</template>
<script setup>
import { useMouse } from '../hook/useMouse'
const { x, y } = useMouse()
</script>一个组合式函数中可以调用一个或多个其他的组合式函数。
举例来说,我们可以将添加和清除 DOM 事件监听器的逻辑也封装进一个组合式函数中:
// useEvent.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
// 如果你想的话,
// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}有了它,之前的 useMouse() 组合式函数可以被简化为:
// useMouse.js
import { ref } from 'vue'
import { useEventListener } from 'hook/useEvent'
export function useMouse() {
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (event) => {
x.value = event.pageX
y.value = event.pageY
})
return { x, y }
}2)例子二
import { ref, watch } from "vue";
export default function useTitle(titleValue) {
// 定义ref的引入参数
const title = ref(titleValue);
// 监听title的改变
watch(title, (newValue) => {
document.title = newValue;
}, {
immediate: true
});
// 返回ref值
return title;
}三、其它 Composition API
1. shallowRef 与 shallowReactive
shallowReactive:只处理对象第一层属性的响应式(浅响应式)。
shallowRef:只处理基本数据类型的响应式、对象最外层(根)的响应式。
const person = shallowReactive({
name: "张三", // <-- 响应式 person.name="Alice"
age: 18, // <-- 响应式 person.age=22
job: { // <-- 响应式 person.job={}
a: 100 // <-- 非响应式 person.job.a=66
}
})
const score = shallowRef(0) // <-- 响应式 score=100
const person = shallowRef({ // <-- 响应式 person={}
name: "张三", // <-- 非响应式 person.name="Alice"
age: 18, // <-- 非响应式 person.age=22
job: { // <-- 非响应式 person.job={}
a: 100 // <-- 非响应式 person.job.a=66
}
})什么时候使用?
- 如果有一个对象数据,结构比较深,但变化时只是外层属性变化 ===> shallowReactive。
- 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生成新的对象来替换 ===> shallowRef。
2. readonly 与 shallowReadonly
应用场景:不希望数据被修改时。
readonly:让一个响应式数据变为只读的(深只读)。
接受的类型:普通对象、reactive 返回的对象、ref 的对象。
shallowReadonly:让一个响应式数据变为只读的(浅只读)。
3. toRaw 与 markRaw
toRaw
- 作用:只能将一个由 reactive 生成的响应式对象转为普通对象。
- 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
markRaw
作用:标记一个对象,使其永远不会再成为响应式对象。
应用场景
① 有些值不应被设置为响应式的,例如复杂的第三方类库等。
② 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。
4. customRef
作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。
实现防抖效果:
<template>
<input type="text" v-model="keyword">
<h3>{{keyword}}</h3>
</template>
<script>
import {ref,customRef} from 'vue'
export default {
name:'Demo',
setup(){
// let keyword = ref('hello') // 使用Vue准备好的内置ref
// 自定义一个myRef
function myRef(value,delay){
let timer
// 通过customRef去实现自定义
return customRef((track,trigger)=>{
return {
get(){
track() // 告诉Vue这个value值是需要被“追踪”的
return value
},
set(newValue){
clearTimeout(timer)
timer = setTimeout(()=>{
value = newValue
trigger() // 告诉Vue去更新界面
},delay)
}
}
})
}
let keyword = myRef('hello',500) // 使用程序员自定义的ref
return {
keyword
}
}
}
</script>5. 响应式数据的判断
isRef:检查一个值是否为一个 ref 对象。
isReactive:检查一个对象是否是由
reactive()或shallowReactive()创建的代理。如果是 readonly,但是由 reactive 创建的另一个代理,它也会返回 true。
javascriptimport { reactive, readonly, isReactive } from 'vue'; const original = reactive({ a: 1 }); const readOnlyVersion = readonly(original); console.log(isReactive(original)); // true,因为 original 是由 reactive 创建的 console.log(isReactive(readOnlyVersion)); // true,尽管 readOnlyVersion 是只读的,但它基于由 reactive 创建isReadonly:用于判断对象是否由
readonly()或shallowReadonly()创建。isProxy:检查一个对象是否是由
reactive()、readonly()、shallowReactive()或shallowReadonly()创建的代理。
6. unref
unref 方法用于获取一个 ref 引用中的 value 值。如果传递给 unref 的参数是一个 ref 对象,那么它会返回该 ref 对象内部的值。如果传递的参数不是 ref 对象,那么它会直接返回参数本身。
import { ref, unref, isRef } from 'vue';
// 创建一个 ref 对象
const count = ref(10);
// 使用 unref 获取 ref 对象的值
const value = unref(count);
console.log(value); // 输出:10
// 如果传递的不是 ref 对象,unref 直接返回参数本身
const notRef = 20;
const value2 = unref(notRef);
console.log(value2); // 输出:207. triggerRef
triggerRef 方法用于手动触发与 shallowRef 相关的副作用。
在 Vue 中,shallowRef 是一种浅层的响应式引用,它只对引用本身的变化做出反应,而不会对其内部对象的变化做出反应。有时,我们可能需要手动触发与 shallowRef 相关的副作用,这时就可以使用 triggerRef 方法。
const shallow = shallowRef({
greet: 'Hello, world'
})
// 触发该副作用,第一次应该会打印 "Hello, world"
watchEffect(() => {
console.log(shallow.value.greet)
})
// 这次变更不应触发副作用,因为这个 ref 是浅层的(修改不是响应式的)
shallow.value.greet = 'Hello, universe'
const changeInfo = () => {
shallow.value.greet = "Hello, yxts"
// 手动触发
triggerRef(shallow) // 打印 "Hello, yxts"
};8. 依赖注入
官方文档:https://cn.vuejs.org/guide/components/provide-inject.html
https://cn.vuejs.org/api/composition-api-dependency-injection.html

作用:实现祖与后代组件间通信。
套路:父组件有一个 provide 选项来提供数据,后代组件有一个 inject 选项来开始使用这些数据。
具体写法:
祖组件中
javascriptsetup(){ // ...... let car = reactive({name:'奔驰',price:'40万'}) provide('car',car) // ...... }后代组件中
javascriptsetup(props,context){ // ...... const car = inject('car', {name:'yxts'}) // 第二个参数为兜底的值 return {car} }
四、自定义指令
1. 定义语法
在 <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以当作自定义指令使用。
<script setup>
// 在模板中启用 v-highlight
const vHighlight = {
mounted: (el) => {
el.classList.add('highlight')
}
}
</script>
<template>
<p v-highlight>This sentence is important!</p>
</template>将一个自定义指令全局注册到应用层级也是一种常见的做法:
const app = createApp({})
// 使 v-highlight 在所有组件中都可用
app.directive('highlight', {
/* ... */
})简写
<div v-color="color"></div>
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})2. 指令钩子 / 生命周期
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding) {},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding) {}
}- el:指令绑定到的元素。这可以用于直接操作 DOM。
- binding:一个对象,包含以下属性。
- value:传递给指令的值。例如在
v-my-directive="1 + 1"中,值是 2。 - oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
- arg:传递给指令的参数 (如果有的话)。例如在
v-my-directive:foo中,参数是"foo"。 - modifiers:一个包含修饰符的对象 (如果有的话)。例如在
v-my-directive.foo.bar中,修饰符对象是{ foo: true, bar: true }。 - instance:使用该指令的组件实例。
- value:传递给指令的值。例如在
3. 实战
1)项目中真实使用 - 01
(1) 新建 @/directive 文件夹。用来存放自定义指令。
(2) 在 @/directive 下建 focus.js 文件。
export default function directiveFocus(app) {
// 注册一个名为 "focus" 的自定义指令
app.directive("focus", {
// 生命周期钩子:指令绑定的元素挂载到页面时触发
mounted(el) {
// 如果元素存在,则调用 `focus()` 方法使其获得焦点
el?.focus();
}
});
}(3) 在 @/directive 下建 index.js 文件,用于汇总自定义指令文件。
import directiveFocus from "./focus";
export default function useDirectives(app) {
directiveFocus(app);
}(4) 在 main.js 中引入自定义指令。
import useDirectives from "./directive";
useDirectives(app)2)项目中真实使用 - 02
[1] 前置知识
以前我们 createAPP(app).use(router).use(store).mount('#app'),其实 use 函数可以接收一个对象或函数。
如果是对象:要求有一个 install 属性,值为函数。默认会给这个函数传递一个 app 对象。
如果是函数:会直接执行这个函数。默认会给这个函数传递一个 app 对象。[2] 进阶写法
此版为【项目中真实使用 - 01】的进阶写法。
// index.js
import directiveFocus from "./focus";
export default function directives(app) {
directiveFocus(app);
}import directives from "./directive";
createAPP(app)
.use(directives)
.use(router)
.use(store)
.mount('#app')第三章:新的组件
一、Fragment
在 Vue2 中:组件必须有一个根标签。
在 Vue3 中:组件可以没有根标签,内部会将多个标签包含在一个 Fragment 虚拟元素中。
好处:减少标签层级,减小内存占用。
二、Teleport
Teleport 是一种能够将我们的组件中某片段 html 结构移动到指定位置的技术。
<teleport to="移动位置">
<div v-if="isShow" class="mask">
<div class="dialog">
<h3>我是一个弹窗</h3>
<button @click="isShow=false">关闭弹窗</button>
</div>
</div>
</teleport>移动位置(to 属性)可以填写 html、body、CSS 选择器字符串。
三、Suspense
1. 作用:等待异步组件时渲染一些额外内容,让应用有更好的用户体验。
2. 使用步骤
1)异步引入组件
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent 方法来实现此功能:
defineAsyncComponent 方法接收一个返回 Promise 的加载函数。正好 import() 函数返回的是 Promise 对象。
import {defineAsyncComponent} from 'vue' // 静态引入
// import Child from './components/Child.vue'; // 之前的导入方式,使用 es6 模块化语法,是同步的
const Child = defineAsyncComponent(()=>import('./components/Child.vue')) // 动态引入 / 异步引入2)使用 Suspense 包裹组件,并配置好 default 与 fallback
<template>
<div class="app">
<h3>我是App组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中.....</h3>
</template>
</Suspense>
</div>
</template>当 Child 组件正在加载时,用户会看到“加载中.....”。当 Child 组件加载完成并准备好被渲染时,<Suspense> 会自动切换到 default 插槽,显示 Child 组件。
四、动态组件
动态组件是使用 component 组件,通过一个特殊的属性 is 来实现。
<template>
<component :is="currentComponent"></component>
</template>
<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
export default {
components: {
ComponentA,
ComponentB
},
data() {
return {
currentComponent: 'ComponentA'
}
}
}
</script>这个 currentComponent 值需要什么内容?
- 全局注册:可以是通过 component 查找注册的组件。
- 局部注册:在一个组件对象里的 components 对象中注册的组件。
如果是动态组件,我们可以给它传值和监听事件吗?只需要将属性和监听事件放到 component 上来使用。
<template>
<component
:is="currentComponent"
:message="message"
@custom-event="handleCustomEvent"
></component>
</template>
<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
export default {
components: {
ComponentA,
ComponentB
},
data() {
return {
currentComponent: 'ComponentA',
message: 'Hello from parent'
}
},
methods: {
handleCustomEvent(data) {
console.log('Custom event received:', data)
}
}
}
</script>在子组件中,可以这样使用:
<template>
<div>
<p>{{ message }}</p>
<button @click="emitEvent">Emit Event</button>
</div>
</template>
<script>
export default {
props: ['message'],
methods: {
emitEvent() {
this.$emit('custom-event', 'Data from ComponentA')
}
}
}
</script>第四章:vue-router
使用 vue-router 的版本为 4.x。
一、快速入门
1)创建路由需要映射的组件。
2)通过 createRouter 创建路由对象,并且传入 routes 和 history 模式;
- routes:路由映射。组件和路径映射关系的 routes 数组。
- history:创建基于 hash 或者 history 的模式。
// 导入vue-router的相关方法
import { createRouter, createWebHashHistory } from 'vue-router'
// 导入创建的组件
import Home from '../pages/Home.vue'
import About from '../pages/About.vue'
// 配置路由的映射
const routes = [
{ path: '/home', component: Home },
{ path: '/about', component: About }
]
// 创建router对象
const router = createRouter({
routes,
history: createWebHashHistory()
})
export default router3)使用 app 注册路由对象(use 方法);
// 在主文件中引入router
import router from './router'
// 使用router并挂载到Vue实例
createApp(App).use(router).mount('#app')4)路由使用:通过 <router-link> 和 <router-view>。
<template>
<div class="nav">
<router-link to="/home">首页</router-link>
<router-link to="/about">关于</router-link>
</div>
<router-view></router-view>
</template>二、零碎知识
1. 切换路由模式
Hash 模式(默认)。
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(), // createWebHistory(),
routes: [
// 你的路由配置
]
})2. SFC 拿到 route / router
JavaScript 中:
export default {
created() {
console.log(this.$route.params.id); // vue2
},
setup() { // vue3
const router = useRouter();
const route = useRoute();
console.log(route.params.id);
}
}Vue 模板中:
<template>
<div>
<h2>用户界面(vue2):{{ $route.params.id }}</h2>
<h2>用户界面(vue3):{{ route.params.id }}</h2>
</div>
</template>3. NotFound
{
path: '/:pathMatch(.*)',
component: () => import('../pages/NotFound.vue')
}当使用 Vue Router 的通配符路由(如 path: '*', 或 path: '/:pathMatch(.*)')时,pathMatch 会捕获用户访问的未匹配路径。
<h2>Not Found: {{ $route.params.pathMatch }}</h2> <!-- Not Found: user/haha/123 -->还可以在 path: '/:pathMatch(.*)' 后加 *
{
path: '/:pathMatch(.*)*',
component: () => import('../pages/NotFound.vue')
}两者区别
<h2>Not Found: {{ $route.params.pathMatch }}</h2> <!-- Not Found: ["user", "haha", "123"] -->三、动态添加路由
Vue Router 文档:动态路由
1. 添加一级路由
router.addRoute({ path: '/about', component: About })2. 添加嵌套路由
要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute(),这将有效地添加路由,就像通过 children 添加的一样:
router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })这等效于:
router.addRoute({
name: 'admin',
path: '/admin',
component: Admin,
children: [{ path: 'settings', component: AdminSettings }],
})3. 删除路由
通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,再添加路由:
javascriptrouter.addRoute({ path: '/about', name: 'about', component: About }) // 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的 router.addRoute({ path: '/other', name: 'about', component: Other })通过调用
router.addRoute()返回的回调:javascriptconst removeRoute = router.addRoute(routeRecord) removeRoute() // 删除路由如果存在的话当路由没有名称时,这很有用。
通过使用
router.removeRoute()按名称删除路由:javascriptrouter.addRoute({ path: '/about', name: 'about', component: About }) // 删除路由 router.removeRoute('about')需要注意的是,如果你想使用这个功能,但又想避免名字的冲突,可以在路由中使用 Symbol 作为名字。
当路由被删除时,所有的别名和子路由也会被同时删除。
4. 查看现有路由
Vue Router 提供了两个功能来查看现有的路由:
router.hasRoute():检查路由是否存在。参数是路由 name。router.getRoutes():获取一个包含所有路由记录的数组。
四、路由守卫
1. beforeEach
它有返回值
- false:取消当前导航。
- 不返回或者 undefined:进行默认导航。
- 返回一个路由地址:
- 可以是一个 string 类型的路径。
- 可以是一个对象,对象中包含 path、query、params 等信息。
可选的第三个参数:next(不推荐使用)
-在 Vue2 中我们是通过 next 函数来决定如何进行跳转的;
但是在 Vue3 中我们是通过返回值来控制的,不再推荐使用 next 函数,这是因为开发中很容易调用多次 next。
2. 完整的导航解析流程
官方文档:完整的导航解析流程
用户点击链接 / 编程式导航
↓
1. 导航被触发
↓
2. 离开旧页面 → beforeRouteLeave (失活组件, 即将离开的组件) 询问用户是否要离开此页面
↓
3. 全局前置守卫 → beforeEach 权限验证
↓
4. 重用组件更新 → beforeRouteUpdate (如果有) (路径变了, 但组件实例被复用) `/user/1` → `/user/2`: User 组件被重用, 只是参数变了, 如果是从 `/home` 跳到 `/user/1`, 是不会触发它的 (而是触发 beforeRouteEnter) 。
↓
5. 路由配置守卫 → beforeEnter (路由独享的守卫) 单独给某个组件内做权限验证
↓
6. 解析异步组件 (如果有) 如果组件是异步加载的, 此时会加载组件
↓
7. 进入新页面 → beforeRouteEnter (激活组件, 即将进入的新组件)
↓
8. 全局解析守卫 → beforeResolve (和 router.beforeEach 类似) 解析守卫刚好会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。在这里做最后的检查
↓
9. 导航确认 ✅ 导航确认,准备更新视图
↓
10. 全局后置钩子 → afterEach (导航确认后调用, 不接受next参数, 无法改变导航) 修改页面标题、发送分析数据
↓
11. DOM 更新 Vue 开始渲染新组件,旧组件被销毁
↓
12. beforeRouteEnter 的 next 回调执行 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入第五章:其他
一、全局 API 的转移
Vue2.x 有许多全局 API 和配置。
例如:注册全局组件、注册全局指令等。
javascript// 注册全局组件 Vue.component('MyButton', { data: () => ({ count: 0 }), template: '<button @click="count++">Clicked {{ count }} times.</button>' }) // 注册全局指令 Vue.directive('focus', { inserted: el => el.focus() }Vue3.0 中对这些 API 做出了调整
将全局的 API,即:
Vue.xxx调整到应用实例上。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 应用实例(app)什么鬼?
javascriptimport { createAPP } from 'vue' import APP from './App.vue' const app = createApp(App) app.mount('#app')
二、其他改变
data 选项应始终被声明为一个函数。
过渡类名的更改
Vue2.x 写法
css.v-enter, .v-leave-to { opacity: 0; } .v-leave, .v-enter-to { opacity: 1; }Vue3.x 写法
css.v-enter-from, .v-leave-to { opacity: 0; } .v-leave-from, .v-enter-to { opacity: 1; }
移除 keyCode 作为 v-on 的修饰符,同时也不再支持
config.keyCodes移除
v-on:xxx.native修饰符。使用自定义事件必须用 emits 选项来规定。父组件中绑定事件
vue<my-component v-on:close="handleComponentEvent" v-on:click="handleNativeClickEvent" />子组件中声明自定义事件
vue<script> export default { // 第一种写法【常用】 emits: ['close'] // 第二种写法: 自定义事件的参数和验证 (了解) // 自定义事件接受的数据必须满足函数提供的校验,如果返回 fales 就会在控制台警告 emits: { addOne: null, // 无要求 subOne: null, // 无要求 addTen: function(payload) { // addTen 事件传递的参数必须等于 10 if (payload === 10) { return true } return false; } }, } </script>
移除过滤器(filter)
过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。
……