环境准备
项目创建
Vite创建
使用Vite创建项目
Vite 需要 Node.js 版本 14.18+,16+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。
bash
npm create vite@latest
yarn create vite
pnpm create vite
然后按照提示操作即可!这种方式创建的Vue3
项目只默认集成了Vite
,像Router
、Pinia
都需要自己手动集成,如果需要就在Select a variant
项选择Customize with create-vue
,此时会按照create-vue来创建,根据你的需求选择需要哪些依赖。
bash
➜ npm create vite@latest
✔ Project name: ... vite-project
✔ Select a framework: › Vue
✔ Select a variant: › Customize with create-vue ↗
.....
Vue脚手架【推荐】
执行命令,这一指令将会安装并执行 create-vue,它是 Vue
官方的项目脚手架工具。
bash
npm init vue@latest
Vue.js - The Progressive JavaScript Framework
✔ Project name: ... <your-project-name> # 项目名称
✔ Add TypeScript? ... No / Yes # 添加ts
✔ Add JSX Support? ... No / Yes # 添加jsx支持
✔ Add Vue Router for Single Page Application development? ... No / Yes # 添加 router
✔ Add Pinia for state management? ... No / Yes # 添加 Pinia
✔ Add Vitest for Unit testing? ... No / Yes # 添加 vitest
✔ Add an End-to-End Testing Solution? ... No / Cypress / Playwright # 添加端到端测试
✔ Add ESLint for code quality? ... No / Yes # 添加Eslit
✔ Add Prettier for code formatting? ... No / Yes # 添加Prettier
Scaffolding project in /Users/fanliu/Desktop/vue-project...
Done. Now run:
cd vue-project
npm install
npm run format
npm run dev
根据提示,进入项目目录安装依赖,执行命令npm run dev
启动项目
添加vue环境变量
在使用组件时报错
新建一个环境变量文件,或者在env.d.ts
文件添加vue
文件申明
ts
declare module '*.vue' {
import { ComponentOptions } from 'vue'
const componentOptions: ComponentOptions
export default componentOptions
}
VsCode插件安装
安装volar
插件支持Vue3
开发,第一第二个都需要安装。
如果安装了vetur
插件,它是支持Vue2
语法的,需要先将插件禁用,在写Vue2
的时候可以打开。
组合式 API:setup()
setup
定义方式有两种,
setup函数模式
第一种script
标签内定一个setup
函数,在函数中返回的对象会暴露给模板和组件实例。
html
<script>
export default {
setup() {
let str = '1'
return {
str
}
}
}
</script>
<template>
<span> {{str}} </span>
</template>
setup语法糖【推荐】
在script
标签上添加setup
属性,再通过lang
属性设置语言为ts
。
推荐使用ts来开发Vue3,在开发提示上更友好,尽管类型定义很麻烦。
html
<script setup lang="ts">
let str = '1'
</script>
<template>
<span> {{str}} </span>
</template>
ref全家桶
ref()
ref()
接收一个内部值,返回一个响应式的、可更改 的 ref
对象,此对象只有一个指向其内部值的属性 .value
,被ref()
包装的数据需要通过.value
的形式赋值。
在Vue2
中,只有被data
函数包裹起来的数据才会是响应式的,同理ref()
函数也可以将变量变成响应式。
如果只是普通的通过let const
定义的变量,当数据修改后是无法在视图上更新。
html
<template>
<span> 姓名 {{ name }} </span>
<span> 年龄 {{ age }} </span>
<button @click="change">修改</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
let name = 'xg' // 死数据,修改后无法更新视图
let age = ref(18) // 响应式数据 通过.value方式修改
function change() {
name = '小鬼'
age.value = 20
}
</script>
为 ref() 标注类型
还可以通过泛型方式约束ref
数据类型,默认可以通过[[../TypeScript#类型推断|类型推断|]]推测出,也可以使用[[../TypeScript#泛型(Generics)|泛型]]。
ts
<script setup lang="ts">
import { ref } from 'vue'
type Person = {
name: string
age: number
}
const person = ref<Person>({
name: 'xg',
age: 18
})
</script>
也可以通过Ref定义类型
ts
<script setup lang="ts">
import { ref } from 'vue'
import type { Ref } from 'vue'
type Person = {
name: string
age: number
}
const person: Ref<Person> = ref({
name: 'xg',
age: 18
})
function change() {
person.value.name = '小鬼'
person.value.age = 20
}
</script>
isRef()
判断数据是否为一个ref
对象
ts
<script setup lang="ts">
import { ref, isRef } from 'vue'
let name = 'xg' // 死数据,修改后无法更新视图
let age = ref(18) // 响应式数据 通过.value方式修改
console.log(isRef(age)) // true
console.log(isRef(name)) // false
</script>
shallowRef()
shallowRef
创建的数据只会做浅层响应式 处理。也就是说,只有当数据值本身发生改变 时,视图会更新,但如果对象的属性发生变化,视图不会更新。
当创建的值是引用类型的时候,才会发生浅层响应式,如果是基础类型则不会。原理就是在调用的时候,只是返回value,不会通过toReactive做响应处理。
ts
<script setup lang="ts">
import { shallowRef } from 'vue'
const person = shallowRef({
name: 'xg',
age: 18
})
function change() {
person.value.name = '泰裤辣' // ❌ 视图不会改变
person.value = {
name: '小鬼',
age: 20
}
}
</script>
和Ref一起使用
如果和Ref
一起使用,shallowRef
会被影响。
ts
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
const person1 = shallowRef({
name: 'xg'
})
const person2 = ref({
name: 'ikun'
})
function change() {
person1.value = { // shallowRef 会被 ref影响 视图数据也会更新
name: 'shallowRef'
}
person2.value.name = 'ref'
}
</script>
和Ref区别
ref
数据是深层次的,无论哪一层数据改变都可以通过.value
形式修改,修改后的数据可以同步更新到视图上。shallowRef
是浅层次的,只能通过修改整个对象去改变数据,如果通过.value
形式是无法更新视图。shallowRef
不能和Ref
同时使用,不然会影响shallowRef
造成视图更新。
tirggerRef()
强制更新DOM。使用shallowRef
不会处罚页面更新,加上tirggerRef()
就可以。
其实 ref 的原理就是由 shallowRef + tirggerRef 组成,也会有 ref 和 shallowRef 同时使用时会影响 shallowRef 造成视图更新,因为 ref 内部会调用一次 tirggerRef
ts
<script setup lang="ts">
import { shallowRef, triggerRef } from 'vue'
const person = shallowRef({
name: 'xg',
age: 18
})
function change() {
person.value.name = '泰裤辣'
triggerRef(person) // 强制更新视图
}
</script>
customRef()
创建一个自定义的 ref
,通过回调函数接收track
和trigger
,要求返回的对象里实现get
和set
。也就是说将数据相应的收集、触发过程,交给我们手动实现,并在过程中执行一些操作。
ts
<script setup lang="ts">
import { ref, shallowRef, customRef } from 'vue'
function MyRef<T>(value: T) {
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
value = newValue
trigger()
}
}
})
}
const person = MyRef({
name: 'ikun'
})
function change() {
person.value.name = 'customRef'
}
</script>
ref获取DOM
和2一样,ref
支持获取到对应的DOM
对象,在使用的时候DOM
上的ref
名称要和定义时保持一致。
html
<template>
<span ref="spanRef">这是一个span</span>
<button @click="getDOM">获取</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const spanRef = ref<HTMLSpanElement>() // 要和DOM保持一致
function getDOM() {
console.log('spanRef', spanRef.value?.innerText) // spanRef 这是一个span
}
</script>
reactive全家桶
reactive()
用于创建相应式数据,返回一个对象的响应式代理,也就是使用Proxy,一般用来绑定复杂数据类型,比如数组、对象。
如果用ref创建数组、对象等复杂数据,其实源码里也是调用的reactive
ts
<script setup lang="ts">
import { reactive } from 'vue'
const person = reactive({
name: 'ikun',
age: 18
})
</script>
reactive
源码约束了数据类型
不允许绑定基础数据类型,不然会报错
ts
<script setup lang="ts">
import { reactive } from 'vue'
const person = reactive('inkun') // ❌ 类型"string"的参数不能赋给类型"object"的参数。
</script>
使用reactive
修改数据,无须通过.value
形式
ts
import {reactive} from 'vue'
const person = reactive({
name: 'ikun',
age: 18
})
function change() {
person.name = '🐔你太美'
}
为 reactive() 标注类型
reactive
可以直接使用类型约束
不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。
ts
<script setup lang="ts">
import { reactive } from 'vue'
interface Person {
name: string
age: number
}
const person: Person = reactive({
name: 'ikun',
age: 18
})
function change() {
person.name = '🐔你太美'
}
</script>
异步赋值数组
在异步情况下,给数组赋值如果是全覆盖会出现proxy
丢失问题,造成数据不是相应式的。
ts
<script setup lang="ts">
import { reactive } from 'vue'
let person = reactive<string[]>([])
setTimeout(() => {
person = ['xg', 'ly', 'hcy', 'jys'] // 模拟后台返回数据
console.log('person', person)
}, 2000)
</script>
打印出来的数据没有Proxy
代理,而且页面也没有更新。是因为新的数组将reactive
定义的数组完全给覆盖掉了,导致Proxy
也丢失。
解决办法
使用解构赋值 + push
,在不破坏原数组的基础上添加新的数据
ts
<script setup lang="ts">
import { reactive } from 'vue'
let person = reactive<string[]>([])
setTimeout(() => {
const list = ['xg', 'ly', 'hcy', 'jys'] // 模拟后台返回数据
person.push(...list)
console.log('person', person)
}, 2000)
</script>
和ref区别
reactive
和ref
两都用于创建响应式数据,它们有以下几个主要区别:
- 数据类型 :
reactive
函数接收复杂类型Array、Object、Map、Set,并将其转换为响应式对象,并且对象中所有属性都将变成响应式的。ref
函数可以接收任意类型,并将其包装在一个特殊的响应式引用对象中,响应式引用对象只有一个.value
属性,该属性包含实际的值。 - 访问属性 :
reactive
创建的响应式对象可以直接访问其属性,就像访问普通对象一样,不需要额外的.value
。而ref
创建的响应式引用对象必须通过.value
属性来访问修改。
shallowReactive()
和shallowRef
一样,也是将数据浅层响应式处理,如果是深层次数据只会改变值,不会更新视图。
ts
<script setup lang="ts">
import { shallowReactive } from 'vue'
const person = shallowReactive({
singer: {
name: 'hcy'
},
dancer: {
name: 'ikun'
},
})
function edit() {
person.dancer.name = '🐔你太美' // 数据变了 但是视图没有更新
console.log('person', person)
}
</script>
这里的响应式处理只到第一层,也就是singer
这一层,如果修改第一层数据还是会触发视图更新。
ts
<script setup lang="ts">
import { shallowReactive } from 'vue'
const person = shallowReactive({
singer: {
name: 'hcy'
},
dancer: {
name: 'ikun'
},
num: 1
})
function edit() {
person.dancer.name = '🐔你太美' // 视图也会更新
person.num = 2 // 修改第一层
console.log('person', person)
}
</script>
和reactive一起使用
它同样会有shallowRef
的问题,如果一起使用会影响数据变化
html
<template>
<span>shallowReactive{{ person }}</span>
<br />
<span>reactive{{ person2 }}</span>
<button @click="edit">修改</button>
</template>
<script setup lang="ts">
import { shallowReactive, reactive } from 'vue'
const person = shallowReactive({
singer: {
name: 'hcy'
},
dancer: {
name: 'ikun'
}
})
const person2 = reactive({
name: 'xg',
age: 18
})
function edit() {
person2.name = 'reactive'
person.dancer.name = '🐔你太美' // 受 reactive 影响 也会更新视图
console.log('person', person)
}
</script>
isReactive()
检查一个对象是否是由 reactive()
或 shallowReactive()
创建的,返回一个布尔值。
ts
<script setup lang="ts">
import { shallowReactive, reactive, isReactive } from 'vue'
const person = shallowReactive({
singer: {
name: 'hcy'
},
dancer: {
name: 'ikun'
}
})
const person2 = reactive({
name: 'xg',
age: 18
})
function edit() {
isReactive(person)
isReactive(person2)
}
</script>
to全家桶
toRef()
从对象中获取某个数据并将它转换为响应式,如果数据是非响应式的转变后不会更新视图,是响应式数据在转变的时候会更新视图。
toRef
有三种形式,第一种直接收一个参数,等同于ref
。
ts
<script setup lang="ts">
import { toRef } from 'vue'
let name = 'ikun'
let newName = toRef(name) // 直接转换数据 和ref一样效果
</script>
第二种方式接收一个函数,在函数内部返回需要转换的变量,返回的是一个只读 的 ref
,当访问 .value
时会调用此 getter
函数
用来和组件props结合使用,关于禁止对 props 做出更改的限制依然有效,尝试将新的值传递给 ref 等效于尝试直接更改 props,这是不允许的。
ts
<script setup lang="ts">
import { toRef } from 'vue'
let name = 'ikun'
let fnName = toRef(() => {
console.log('我被调用了')
return name
})
function edit() {
console.log('fnName', fnName)
fnName.value = '1' // ❌ 无法为"value"赋值,因为它是只读属性。
}
</script>
第三种方式接收两个参数,第一个参数为需要转换的对象数据。第二个参数为对象的key
,此时会从对象里获取到key
的数据后将其转换并返回。当返回的数据修改时,也会更新源对象里的数据。
主要用途用来将响应式数据从主体里结构出来,赋给某个独立的对象。
ts
<script setup lang="ts">
import { toRef } from 'vue'
const person = {
name: 'ikun',
age: 18
}
const newAge = toRef(person, 'age')
function edit() {
newAge.value = 3 // 此时数据变了,但是视图上数据没有更新。
console.log('newAge', newAge) // newAge Ref<3>
console.log('person', person) // person {name: 'ikun', age: 3} person对象数据也会改变
}
</script>
上面结构的都是普通数据,所以在修改数据后页面是不会进行更新的,如果转换的数据是响应式就可以。
ts
<script setup lang="ts">
import { toRef, reactive } from 'vue'
const person = reactive({
name: 'ikun',
age: 18
})
const newAge = toRef(person, 'age')
function edit() {
newAge.value = 3 // 页面数据也会改变
console.log('newAge', newAge) // newAge Ref<3>
}
</script>
源码理解
为什么会这样? 因为toRef
源码里在创建ref
对象时并没有处理依赖收集和触法以来的过程,只做了值的改变,所以普通对象无法更新视图。
这样做的好处就是如果转换的是reactive
,它内部已经有了收集、更新操作,如果在toRef
里面再触发的话,会出现重复更新问题。
ts
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue?: T[K]
): ToRef<T[K]> {
const val = object[key]
return isRef(val)
? val
: (new ObjectRefImpl(object, key, defaultValue) as any)
}
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
constructor(
private readonly _object: T,
private readonly _key: K,
private readonly _defaultValue?: T[K]
) {}
get value() {
const val = this._object[this._key]
return val === undefined ? (this._defaultValue as T[K]) : val
}
set value(newVal) {
this._object[this._key] = newVal
}
}
toRefs()
作用和toRef
一样,只不过可以处理多个,一般都用来解构/展开返回reactive
里的数据。
ts
<script setup lang="ts">
import { toRefs, reactive } from 'vue'
const person = reactive({
name: 'ikun',
age: 18
})
const {name, age} = toRefs(person)
function edit() {
name.value = '🐔你太美'
age.value = 3
}
</script>
toRaw()
将一个相应式数据转换为一个原始对象,这个响应式数据可以是由 reactive()
、readonly()
、shallowReactive()
或者shallowReadonly()
创建的代理对应的原始对象。
ts
<script setup lang="ts">
import { toRaw, reactive } from 'vue'
const person = reactive({
name: 'ikun',
age: 18
})
const newPerson = toRaw(person)
function edit() {
console.log(newPerson) // {name: 'ikun', age: 18}
console.log(person) // Reactive<Object> 这里式proxy代理包装了一层
}
</script>
computed计算属性
computed
计算属性,当依赖的属性值发生变化时,才会触发他的更改。如果依赖的值不发生变化的时候,使用的就是缓存中的属性值。
在使用上和Vue2
没有区别,支持两种写法,一种传入一个对象自己实现get
和set
。一种是直接传入对象,返回修改后的数据。
ts
<script setup lang="ts">
import { ref, computed } from 'vue'
let firstName = ref('i')
let lastName = ref('kun')
let name1 = computed({
get() {
return firstName.value + '🐔' + lastName.value
},
set(newVla) {
;[firstName.value, lastName.value] = newVla.split('🐔')
}
})
let name2 = computed(() => {
return firstName.value + '🐔' + lastName.value
})
</script>
为 computed() 标注类型
computed()
会自动从其计算函数的返回值上推导出类型,同时也支持泛型指定类型。
ts
let name2 = computed<string>(() => {
return firstName.value + '🐔' + lastName.value
})
watch侦听器
监听一个或者多个响应式数据,并在数据变化的时候调用所给的回调函数。
监听单个数据
ts
import { ref, watch } from 'vue'
let name = ref('ikun')
watch(name, (newVal, oldVal) => {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
})
监听多个数据,此时的newVla
和oldVal
都变成数组。
ts
import { ref, watch } from 'vue'
let name = ref('ikun')
let name2 = ref('ikun2')
watch([name, name2], (newVal, oldVal) => {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
})
监听对象某个属性,使用get
函数返回属性。
ts
import { reactive, watch } from 'vue'
const person = reactive({
name: 'ikun',
age: 18
})
watch(
() => person.value.name,
(newVal, oldVal) => {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
}
)
当监听ref
创建的对象时,需要开启deep
选项,而reactive
内部已经默认打开了,所以不需要开启。
ts
import { ref, watch } from 'vue'
const person = ref({
name: 'ikun',
age: 18
})
watch(
() => person.value.name,
(newVal, oldVal) => {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
},
{
deep: true
}
)
第三个可选参数是一个对象,支持以下这些选项:
immediate
:在侦听器创建时立即触发回调。第一次调用时旧值是undefined
。deep
:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。flush
:调整回调函数的刷新时机。pre
:组件更新前执行sync
:同步触发执行post
:组件更新后执行
onTrack / onTrigger
:调试侦听器的依赖。
watchEffect 【新增】
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新运行该函数。
在watchEffect
里用到谁只会监听谁
ts
import { ref, watchEffect } from 'vue'
const name1 = ref('ikun')
watchEffect(() => {
console.log('name1', name1)
})
第二个参数为可选对象参数
flush
:调整回调函数的刷新时机。pre
:组件更新前执行sync
:同步触发执行post
:组件更新后执行
清除副作用
在触发监听前会调用一个函数处理逻辑,当使用的数据发生变化前会先执行该函数,这里可以处理一些防抖、请求等操作。
比如这个例子,当todoId
发生变化后,会先执行onCleanup
函数将loading
设为true
。然后再去请求接口,将后台返回的数据赋值给data
。
ts
import { ref, watchEffect } from 'vue'
const todoId = ref(1)
const data = ref(null)
const loading = ref(false)
watchEffect(async (onCleanup) => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
onCleanup(() =>{
loading.value = true
})
data.value = await response.json()
})
停止侦听器
watchEffect
会返回一个函数,执行该函数会停止后续的监听操作。
watch 同样可以使用
ts
import { ref, watchEffect } from 'vue'
const name1 = ref('ikun')
const stop = watchEffect(() => {
console.log('name1', name1)
})
stop()
和watch区别
watch
需要明确指定要观察的数据源 ,不会监听回调中的任何数据,适用于需要精确控制的情况,可以在回调函数中处理新旧数据的变化情况。watchEffect
会在同步执行过程中自动追踪依赖的数据,适用于需要自动化处理的情况,无需手动指定依赖项,但无法访问旧数据。
总的来说,如果需要在特定的数据发生变化时执行特定的操作,使用watch
;如果只需要追踪数据的变化并在变化时执行一段代码,使用watchEffect
。
组件
组件定义和使用
全局组件
在main.ts
中注册后,全局都可以使用,不需要单独引入
ts
// main.ts
import HomeTop from '@/components/home/HomeTop.vue'
app.component('HomeTop', HomeTop)
// home.vue
<template>
<div id="main">
<!-- 顶部 -->
<HomeTop></HomeTop>
</div>
</template>
局部组件
在组件内引入,引入后不需要注册 ,直接在template
中使用
vue
<template>
<div id="main">
<!-- 顶部 -->
<HomeTop></HomeTop>
</div>
</template>
<script setup lang="ts">
import HomeTop from '@/components/home/HomeTop.vue'
</script>
生命周期 【改动】
和Vue2
不同的是,3移除了一些2中的勾子如 beforeMount
、beforeUpdate
、destroyed
等,同时新增了一些勾子如onBeforeUpdate
、onBeforeUnmount
onBeforeMount()
:件被挂载之前被调用onMounted()
:组件挂在完成后调用onBeforeUpdate()
:组件DOM更新之前调用onUpdated()
:组件DOM更新之后调用onBeforeUnmount()
:组件实例卸载之前调用onUnmounted()
:组件实例卸载之后调用onErrorCaptured()
:子组件发生错误调用onActivated()
:若组件实例是keep-alive
缓存的一部分,当组件被插入到 DOM 中时调用。onDeactivated()
:若组件实例是keep-alive
缓存的一部分,当组件从 DOM 中移除时调用onMounted()
:组件挂在完成后
组件通信
父传子
父组件传递 父组件通过属性绑定,如果是动态属性需要添加:
html
<template>
<div id="main">
<!-- 顶部 -->
<HomeTop :title='title' name='zs'></HomeTop>
</div>
</template>
<script setup lang="ts">
import HomeTop from '@/components/home/HomeTop.vue'
const title = 'homeTop'
</script>
非TS方式
子组件接收参数 通过defineProps
,它是一个编译宏命令,并不需要显式地导入。在模版中可以直接使用属性title
,而在js
中使用就需要加一个参数接收,然后通过props.title
方式使用。
在defineProps
也可以像vue2那样定义参数类型、默认值
html
<!-- HomeTop.vue -->
<template>
<h4>{{ title }}</h4>
<span>{{ name }}</span>
</template>
<script setup>
const props = defineProps({
title: {
type: string,
default: 'hello word'
},
list: {
type: Array,
default: () => {
return [1,2,3]
}
},
name: String,
})
console.log(props.title)
</script>
TS方式
子组件接收参数 如果使用了TS,也就是在script
标签里指定lang=ts
,可以在泛型里直接约束参数,当然也可以使用interface
接口定义类型。
html
<!-- HomeTop.vue -->
<template>
<h4>{{ title }}</h4>
<span>{{ name }}</span>
</template>
<script setup lang="ts">
const props = defineProps<{
title: string,
name: string,
list?: number[] // 可选参数
}>()
console.log(props.title)
</script>
使用ts,失去了为 props 声明默认值的能力。这可以通过 withDefaults
编译器宏解决:
html
<!-- HomeTop.vue -->
<template>
<h4>{{ title }}</h4>
<span>{{ name }}</span>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
title: string,
name: 'ikun',
list?: () => [1, 2, 3] // 默认值
}>())
console.log(props.title)
</script>
子传父
子组件向父组件传值,子组件中定义方法,在父组件中调用。
非TS方式
子组件传递 在子组件中通过defineEmits
派发一个事件,同样,defineEmits
也是一个宏函数,不需要定义即可使用。用一个参数接收,然后通过emit('on-click')
触发派送
html
<!-- HomeTop.vue -->
<template>
<button @click='send'>派发给父组件 </button>
</template>
<script setup>
const emit = defineEmits(['on-click'])
const send = () => {
emit('on-click', 'zs')
}
</script>
在父组件中,接收子组件传递过来的自定义方法,进行触发。 父组件接收
html
<template>
<div id="main">
<!-- 顶部 -->
<HomeTop @on-click='getName'></HomeTop>
</div>
</template>
<script setup>
import HomeTop from '@/components/home/HomeTop.vue'
// 接收参数
const getName = (vale) => {
console.log(value, '父组件接收参数')
}
</script>
TS方式
子组件传递 在ts环境下,给defineEmits
添加泛型,可以对传递的参数类型进行声明。其余的和非TS方式一样
html
<!-- HomeTop.vue -->
<template>
<button @click='send'>派发给父组件 </button>
</template>
<script setup lang="ts">
const emit = defineEmits<{
(e: 'on-click', name: string): void
}>()
const send = () => {
emit('on-click', 'zs')
}
</script>
兄弟组件
在Vue2
中兄弟组件通信常见的方式是通过Vue
实现一个Event Bus
,而在Vue3
中是移除了,可以查看[[Vue3中那些实用的小技巧]]这篇文章。
父组件调用子组件方法
ref方式
子组件中定义属性和方法
html
<!-- HomeTop.vue -->
<template>
<button @click='send'>派发给父组件 </button>
</template>
<script setup>
const name = 'zs'
const getName = () => {
return name
}
</script>
在父组件中通过ref
来接收,注意的是要在DOM渲染完之后,才能接收子组件的实例。如果是通过click等事件触发后再获取,就不需要等DOM渲染
html
<template>
<div id="main">
<!-- 顶部 -->
<HomeTop ref='homeTopRef'></HomeTop>
</div>
</template>
<script setup >
import HomeTop from '@/components/home/HomeTop.vue'
// 这里 homeTopRef 要和子组件上定义的 ref 值一样
const homeTopRef = ref(null)
// DOM渲染完成后获取
nextTick(() => {
homeTopRef.value.name
})
</script>
defineExpose 【新增】
defineExpose
可以向父组件暴露属性方法
html
<!-- HomeTop.vue -->
<template>
<button @click='send'>派发给父组件 </button>
</template>
<script setup lang="ts">
const name = 'zs'
const getName = () => {
return name
}
defineExpose({
name,
getName
})
</script>
和第一种方式一样,也是通过ref
来接收。在TS环境下,还可以通过泛型约束限制类型,获取更好的提示。
html
<template>
<div id="main">
<!-- 顶部 -->
<HomeTop ref='homeTopRef' @on-click='getName'></HomeTop>
</div>
</template>
<script setup lang="ts">
import HomeTop from '@/components/home/HomeTop.vue'
const homeStopwatchRef = ref<InstanceType<typeof HomeStopwatch>>()
// DOM渲染完成后获取
nextTick(() => {
homeTopRef.value?.name
})
</script>
插槽Slots 【改动】
在子组件中使用一个占位符,父组件可以在这个占位符填充内容内容。
匿名插槽
子组件中插入一个插槽
html
<template>
<div>
<slot></slot>
</div>
</template>
父组件中使用
html
<Dialog>
取消
<Dialog/>
默认内容
子组件定义插槽,在slot
中定义默认内容
html
<template>
<div>
<slot>
名称 <!-- 默认内容 -->
</slot>
</div>
</template>
父组件使用时,没有提供任何插槽内容时,子组件就显示默认内容。如果提供插槽内容,子组件就有显示插槽内容。
html
<Dialog><Dialog/>
具名插槽
一个子组件可以存放多个插槽,为了区分需要使用具名插槽,给每一个插槽取个名字。
没有提供 name 的
<slot>
出口会隐式地命名为default
子组件定义多个具名插槽
html
<template>
<div>
<slot name="header"></slot>
</div>
<div>
<slot name="footer"></slot>
</div>
</template>
父组件在使用的时候按照name
在指定地方插入内容
html
<BaseLayout>
<template v-slot:header>
<span>header</span>
</template>
<template v-slot:footer>
<span>footer</span>
</template>
</BaseLayout>
v-slot
有对应的简写 #
,因此 <template v-slot:header>
可以简写为 <template #header>
动态插槽
插槽可以是一个变量名
html
<BaseLayout>
<template v-slot:[dynamicSlotName]>
...
</template>
<template #[dynamicSlotName]>
...
</template>
</BaseLayout>
作用域插槽
子组件在插槽中绑定数据,向父组件传递参数给<slot>
中使用,也就是vue2
中的slot-scope
html
<template>
<div>
<slot :userName="name" :text="text"> </slot>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('ikun')
const text = ref('🐔你太美')
</script>
父组件通过一个对象接收
html
<MyComponent v-slot="slotProps">
我叫{{ slotProps.userName }} 你真的{{ slotProps.text }}
</MyComponent>
<!-- or -->
<MyComponent v-slot="{userName, text}">
我叫{{ userName }} 你真的{{ text }}
</MyComponent>
具名作用域插槽
顾名思义,就是有具名插槽和作用域一起使用,也是最常用的一种方式
html
<template>
<div>
<slot name='header' :userName="name"></slot>
</div>
<span>
<slot name='main' :text="text"></slot>
</span>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('ikun')
const text = ref('🐔你太美')
</script>
父组件在具名插槽后面通过一个对象接收
html
<MyComponent>
<template #header="slotProps"> 我叫{{ slotProps.userName }} </template>
<template #main="{ text }"> 你真的{{ text }}</template>
</MyComponent>
provide/inject依赖注入
和Vue2
用途一样,都是用来解决组件嵌套较深的情况下,不方便组件间的通信。
provide
用来提供数据,inject
用来接收prodive
数据。
父组件使用provide
提供数据
ts
import { provide, ref } from 'vue'
// 格式
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
const message = ref('hello')
provide('message', message)
子组件接收
ts
import { inject, type Ref } from 'vue'
const message = inject<Ref<string>>('message')
如果提供的值是一个 ref
,注入进来的会是该 ref 对象
,而不会自动解包为其内部的值。
全局注入
除了在组件通信上使用,还可以作为全局的依赖注入
ts
// main.ts
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
defineAsyncComponent异步组件 【新增】
在大型项目中,可能需要将应用分割成小一些的代码块,从而减少主包的体积,加快页面相应速度,这个时候就可以用异步组件defineAsyncComponent
。
html
<template>
<div class="container"></div>
<AsyncComp></AsyncComp>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./components/MyComponent.vue'))
</script>
最后得到的 AsyncComp
是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props
和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
加载与错误状态
支持在高级选项处理加载成功和夹在失败状态。
ts
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./components/MyComponent.vue),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
suspense 【新增】
实验性功能 :
<Suspense>
是一项实验性功能。它不一定会最终成为稳定功能,并且在稳定之前相关 API 也可能会发生变化。
Suspense
是一个内置组件,用来协调组件在异步加载时渲染的处理。通过它可以实现骨架屏,当组件内容加载时先显示骨架屏内容,加载完毕后显示组件内容。
它有两个插槽,但都直接收一个子节点。
defalut
:插槽里的内容节点尽可能的展现出来fallback
:如果default
没有展示,就显示。
html
<Suspense>
<template #default>
<AsyncComp />
</template>
<template #fallback>
<div>loading...</div>
</template>
</Suspense>
当AsyncComp
组件内容没有加载出来前,先展示loading
,加载完毕后,再展示AsyncComp
。通常配合defineAsyncComponent
来使用。
keep-alive
和Vue2
使用一样,用来在需要的时候缓存组件实例,避免组件的一个重复加载渲染,提高页面性能。
html
<KeepAlive include="a,b" exclude="c" :max="10">
<component :is="view" />
</KeepAlive>
teleport 【新增】
teleport
能够将模版渲染至指定的DOM节点,不受父级style
、v-show
影响,但data
、prop
数据依旧能够共用。teleport
只改变渲染DOM的结构,它不会影响组件间的逻辑关系。
它和keep-alive
一样都是内置组件,可以直接使用。
- 使用
to
属性来控制需要传送的目标节点,可以是一个CSS选择器、也可以是一个 DOM 元素对象。 - 使用
disabled
属性来禁用
teleport挂载时,传送的 to 目标必须已经存在于 DOM 中。理想情况下,这应该是整个 Vue 应用 DOM 树外部的一个元素。如果目标元素也是由 Vue 渲染的,你需要确保在挂载 teleport之前先挂载该元素。
html
<Teleport to="body" disabled="true" >
...
</Teleport>
v-model 【改动】
Vue
组件数据是单向流,子组件不能直接修改父组件里面的数据。而v-model
是一个语法糖,通过props
和emit
组合而成。
- 使用
props
默认绑定一个modelValue
值到子组件 - 当数据变化时,通过
update:modeValue
监听改变后修改数据
父组件绑定数据
html
<template>
<MyComponent v-model="isShow"></MyComponent>
</template>
其实相当于,默认是绑定modelValue
数据
html
<MyComponent
:modelValue="isShow"
@update:modelValue="newValue => isShow = newValue"
/>
在子组件里通过defineProps
接收,在修改的时候通过defineEmits
触发emit
,父组件修改数据
html
<template>
<div v-show="modelValue">显示内容</div>
<button @click="change">修改</button>
</template>
<script setup lang="ts">
const propData = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits(['update:modelValue'])
function change() {
emit('update:modelValue', !propData.modelValue.valueOf())
}
</script>
多个v-mode绑定
可以同时绑定多个,在子组件内部按名称接收修改。
html
<MyComponent v-model:isShow="isShow" v-model:message="message"></MyComponent>
html
<template>
<div v-show="isShow">显示内容</div>
<br />
{{ isShow }}
<button @click="change">修改</button>
<br />
<span>{{ message }}</span>
</template>
<script setup lang="ts">
const propData = defineProps<{
isShow: boolean
message: string
}>()
const emit = defineEmits(['update:isShow', 'update:message'])
function change() {
emit('update:isShow', !propData.isShow.valueOf())
emit('update:message', '子组件改变了数据')
}
</script>
v-mode修饰符
v-mode
内置了一些修饰符如.trim
,.number
和 .lazy
,同时也支持自定义修饰符。
html
<MyComponent v-model:isShow.capitalize="isShow" v-model:message="message"></MyComponent>
子组件默认通过modelModifiers
来接收,如果是多个v-mode
,则通过绑定的数据名+Modifiers
接受,例如:messageModifiers
html
<template>
<div v-show="isShow">显示内容</div>
<br />
{{ isShow }}
<button @click="change">修改</button>
<br />
<span>{{ message }}</span>
</template>
<script setup lang="ts">
const propData = defineProps<{
isShow: boolean
message: string
messageModifiers?: {
default: () => {}
}
}>()
const emit = defineEmits(['update:isShow', 'update:message'])
function change() {
emit('update:isShow', !propData.isShow.valueOf())
emit('update:message', '子组件改变了数据')
console.log('modelModifiers', propData.messageModifiers) // {capitalize: true}
}
</script>
自定义指令【改动】
Vue
内置里一系列指令,除此之外还可以自定义指令。
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义,钩子函数会接收到指令所绑定元素作为其参数。
在模版中直接使用
vue
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
将一个自定义指令全局注册
ts
// main.ts
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
指令钩子
ts
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
钩子的参数
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
:使用该指令的组件实例。dir
:指令的定义对象。
vnode
:代表绑定元素的底层VNode
。prevNode
:之前的渲染中代表指令所绑定元素的VNode
。仅在beforeUpdate
和updated
钩子中可用。
举例来说,像下面这样使用指令:
html
<div v-example:foo.bar="baz">
binding
参数会是一个这样的对象:
js
{
arg: 'foo',
modifiers: { bar: true },
value: /* `baz` 的值 */,
oldValue: /* 上一次更新时 `baz` 的值 */
}
函数简写
如果指令只需要在 mounted
和 updated
上实现,可以通过函数简写的形式。
html
<script setup>
// 在模板中启用 v-focus
const vFocus = (el) => {
el.focus()
}
</script>
<template>
<input v-focus />
</template>
对象字面量
如果指令需要多个值,在模版上使用时可以传递一个对象,指令在接受的时候可以通过bingding.value.[attr]
方式访问。
html
<script setup>
// 在模板中启用 v-focus
const vDemo = (el, binding) => {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
}
</script>
<template>
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
</template>
为Directive标注类型
如果为binding.value
定义类型呢?也就是指令的参数。
有两种方式,一种通过Directive
传递泛型,还可以通过DirectiveBinding
指定bingding.value
类型
ts
import { ref } from 'vue'
import type { Directive, DirectiveBinding } from 'vue'
let value = ref<string>('')
type Move = {
color: string
text: string
}
const vMove = (el: HTMLElement, binding: DirectiveBinding<Move>) => {
console.log('binding', binding.value.color)
}
const vHasShow: Directive<HTMLElement, Move> = (el, binding) => {
console.log('binding', binding.value.text)
}
自定义Hooks
听着很复杂,其实就是和Vue2
中的mixins
作用是一样的,用来抽离封装一些公共代码逻辑。
mixins
存在一些问题,就是引入的mixins
会覆盖的问题。
组件的data
、methods
、filters
如果和mixins
里的data
、methods
、filters
同名会被覆盖掉
mixins
的生命周期调用比组件快
还有一点是变量的来源不明确,不利于阅读,维护起来很麻烦。
尽管Vue3
中还保留了mixin,但官方推荐还是不要使用,推荐使用组合式函数代替,也就是hook
。
不推荐 Mixins 在 Vue 3 支持主要是为了向后兼容,因为生态中有许多库使用到。在新的应用中应尽量避免使用 mixin,特别是全局 mixin。
Vue3
则提供hooks
函数,通过引入Vue
的各种钩子实现封装,如onMounted
、ref
、watch
等等,这些都是独立,并不会影响到组件。
Vue
官方提供了一个hooks
库VueUse,里面提供了很多各种各样的hook
。
nextTick
和Vue2
用途一样,等DOM更新完毕后再执行。nextTick()
可以在状态改变后立即使用,以等待 DOM
更新完成。可以传递一个回调函数作为参数,或者 await
返回的 Promise
。
ts
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
count.value++
// DOM 还未更新
console.log(document.getElementById('counter').textContent) // 0
await nextTick()
// DOM 此时已经更新
console.log(document.getElementById('counter').textContent) // 1
}