# Vue3文档
Vue3的基础内容都是基于Vue2来的,但Vue3响应式原理变了,使用Proxy重写了底层代码,并且使用组合式API开发思想,代码写起来更像原生,使逻辑更灵活的组合与复用。Vue3还基于TS编写,让Vue2一直难以兼容TS的问题也得到改变,在学习Vue3时也要先熟悉TS语法。组合式API和TS让Vue3与Vue2的开发习惯有着很大的差异,但只要上手写起来,相信很快就能熟练的,毕竟它还是基于Vue的。
# 组合式API
组合式API思想就是把原本分散在data、created、methods等相关的逻辑,通过setup集中到一起,在其中把每个功能代码块划分在一个逻辑区域,不用再写个变量再去data中找。并且还能像函数一样抽离公共setup片段,让代码更好的封装和重复使用。
在 Vue3 + Vite 搭建的单文件组件(SFC)项目中,直接在 <script setup>
中书写原生js代码,然后在Vue中使用就可以了,不需要再像过渡版那样再通过return把数据返回了。
<script setup>
import { ref } from 'vue'
// 通过ref定义一个响应式变量
const isShow = ref(false)
// 定义一个函数
const toggleShow = () => {
isShow.value = false
}
</script>
<template>
<div @click="toggleShow">点击</div>
<div v-if="isShow">显示内容</div>
</template>
# 响应式基础
Vue3声明响应式数据需要使用 ref
和 reactive
来定义数据类型。ref
用来声明基础数据类型,reactive
用来声明引用类型(对象、数组和 Map、Set等)。
# reactive
使用reactive声明引用类型响应式数据时只有在声明的数据上才具有响应性,解构、赋值给其他值、或传入函数都会丢失,这也是reactive的局限,所以在使用reactive声明时要考虑是否涉及这几种情况。
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
const arr = reactive(['a', 'b', 'c'])
let n = state.count // n失去响应性连接
n++ // 不影响原始的 state
// 解构 count 失去了响应性连接
let { count } = state
count++ // 不会影响原始的 state
// 重新赋值整个对象会失去响应性
state = {
count: 5
}
state.count++ // 不会生效
arr.splice(1, 0) // arr失去响应性
arr = [] // arr失去响应性
// 该函数接收一个普通数字,并且
callSomeFunction(state.count) // 将无法跟踪 state.count 的变化
</script>
<template>
<!-- 在html中直接使用 -->
<span>{{reactive.count}}</span>
</template>
# ref
ref定义基础数据类型,获取和设置需要通过 .value
,但在html上会自动解包不需要.value。ref赋值、传参或者解构不会像reactive失去响应性,而且定义引用类型数据操作时也不会失去响应性,所以基于这一点,我们主要使用ref来声明响应式数据。
ref为什么要 .value
,js不能直接检查普通变量的修改,所以把它设计成一个getter/setter对象,利于Vue进行检查每个ref是否修改。
<script setup>
import { ref } from 'vue'
// 基础类型
const count = ref(0)
const bool = ref(true)
const str = ref('xxx')
const arr = ref([1, 2, 3])
const obj = ref({
name: 'xxx',
age: 18
})
// ref获取和设置需要通过.value
count.value // 0
count.value++ // 1
obj.value.age // 18
</script>
<template>
<!-- 在html中直接使用,自动解包 -->
<span>{{count}}</span>
</template>
# ref解包
把 ref 赋值给 reactive 对象会自动解包,会相互影响,改变哪个都是响应式的。赋值给数组或者Map这种原生集合类型时,ref不会自动解包。
<script setup>
import { ref } from 'vue'
const object = {
foo: ref(1)
}
const { foo } = object
console.log(foo) // 不需要.value
const count = ref(0)
const state = reactive({
count
})
// ref在响应式对象中也会自动解包
state.count // 0
state.count = 1
console.log(count) // 1 count会受影响
// ref作为数组和集合类型的元素不会自动解包
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
</script>
<template>
<!-- 使用表达式不会自动解包,解构或者直接使用都可以 -->
<span>{{object.foo + 1}}</span>
<!-- 解构出来可以 -->
<span>{{foo + 1}}</span>
<!-- 直接使用也可以 -->
<span>{{object.foo}}</span>
</template>
# 响应式API
一些针对响应式数据进行处理的工具函数。
# toRefs()、toRef()
要想解构reactive对象,可以使用 toRefs()
和 toRef()
两个函数转为ref,使数据继续保持响应式。
<script setup>
import { reactive, toRefs, toRef } from 'vue'
const state = reactive({
count: 0,
name: 'xxx'
})
// 将 state 转成一个全是 ref 的对象,然后解构
let { count } = toRefs(state)
count.value++
console.log(state.count) // 1
// 将某个属性转为ref
const name = toRef(state, 'name')
console.log(name.value)
</script>
# shallowRef()、shallowReactive()
当给ref赋值为对象时,ref会对深层次的对象进行响应,可以使用 shallowRef()
只对最外层属性进行响应,从而节省性能。
同理shallowReactive()
、shallowReadonly()
也是让最外层属性是响应性的,深层次的属性改变不进行影响。
# readonly()
接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。只读代理是深层的,对任何嵌套属性的访问都将是只读的。
<script setup>
import { readonly } from 'vue'
const state = readonly({
count: 0,
name: 'xxx'
})
</script>
# 获取this
在<script setup>
中无法像Vue2那样直接使用 this 来获取实例,需要通过getCurrentInstance
方法来获取。
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
console.log(instance)
// instance?.appContext.config.globalProperties 全局实例
console.log(instance?.appContext.config.globalProperties)
// instance?.proxy 当前组件实例
console.log(instance?.proxy)
// 使用
instance?.proxy?.$message.success('操作成功')
</script>
# 生命周期
Vue3生命周期有所改变,没有了 created
钩子,因为setup就相当于created一样。destroyed变为了unmounted。
onBeforeMount
: 在组件被挂载之前被调用。onMounted
: 在组件挂载完成后执行。onBeforeUpdate
: 在组件即将因为响应式状态变更而更新其 DOM 树之前调用。onUpdated
: 在组件因为响应式状态变更而更新其 DOM 树之后调用。onBeforeUnmount
: 在组件实例被卸载之前调用。onUnmounted
: 在卸载组件实例之前调用。onErrorCaptured
: 在捕获了后代组件传递的错误时调用。
KeepAlive缓存组件中的两个声明周期钩子
onActivated
: 组件被插入到 DOM 中时调用。onDeactivated
: 当组件从 DOM 中被移除时调用。
开发环境独有的两个声明
onRenderTracked
: 当组件渲染过程中追踪到响应式依赖时调用。onRenderTriggered
: 当响应式依赖的变更触发了组件渲染时调用。
SSR服务端渲染独有的两个声明
onServerPrefetch
: 在组件实例在服务器上被渲染之前调用。
使用:
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log(`the component is now mounted.`)
})
</script>
# 组件注册
# 全局注册
import { createApp } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
const app = createApp({})
app.component('MyComponent', HelloWorld)
# 局部注册
以前需要引入组件后通过components
注册,在<script setup>
中引入组件后不在需要components
注册就可以使用。组件名默认是以文件名命名,如果想自定义组件名可以使用插件 vite-plugin-vue-setup-extend,使用PascalCase
全大驼峰格式进行命名,以区分组件和普通元素标签。
# 异步组件
使用 defineAsyncComponent
异步加载组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。
<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() => import('./components/AdminPageComponent.vue') )
</script>
<template>
<AdminPage />
</template>
# 计算属性
计算属性 computed
依赖响应式(ref, reactive)进行计算,返回一个计算属性 ref,他和一般的 ref 类似依然是响应式的,调用需要使用.value。计算属性传参方式有两种,分为函数方式和对象方式,默认是传入一个函数。
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'xxx',
books: ['a', 'b', 'c']
})
// 一个计算属性 ref
const isDisabled = computed(() => {
return !author.books.length
})
const bookLength = computed(() => author.books.length)
isDisabled.value // false
</script>
<template>
<button :disabled="isDisabled">{{ isDisabled }}</button>
</template>
计算属性的 getter 和 setter,当输入一个含有get和set的对象时,就可以通过 getter 和 setter 读取和设置对应的值。getter的值应该和计算属性返回的值一样,最好不要做其他操作。
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newVal) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newVal.split(' ')
}
})
fullName.value // John Doe
fullName.value = 'Xiao Ming'
firstName.value = 'Xiao'
lastName.value = 'Ming'
</script>
# 监听器
监听器 watch
对响应式数据(ref, reactive)进行侦听,监测数据是否改变。它可以监听 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组。
# 监听一个值
<script setup>
import { ref, watch } from 'vue'
const msg = ref('xxx')
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(msg, (newVal, oldVal) => {
console.log(newVal)
console.log(oldVal)
})
// getter 函数
// 通过函数返回一个新的值再对其进行监听
watch(() => x.value + y.value, (newVal, oldVal) => {
console.log(newVal)
console.log(oldVal)
}
)
</script>
# 监听多个值
<script setup>
import { ref, watch } from 'vue'
const msg = ref('xxx')
const text = ref('123')
const x = ref(0)
const y = ref(0)
// 多个ref
// 通过数组来监听,返回的值newVal, oldVal也分别是个数组
watch([msg, text], (newVal, oldVal) => {
console.log(newVal) // ['xxx11', '12311']
console.log(oldVal) // ['xxx', '123']
})
// ref 和 getter 函数组合
watch([msg, () => x.value + y.value], (newVal, oldVal) => {
console.log(newVal) // ['xxx11', sum]
console.log(oldVal) // ['xxx11', 0]
}
)
// 调用异步方法
watch(msg, async () => {
const response = await fetch('xxx')
console.log(response)
}, { immediate: true })
</script>
# 监听reactive响应式对象
<script setup>
import { reactive, watch } from 'vue'
const obj = reactive({ count: 0, size: 'small' })
// 监听整个对象
// 隐式地创建一个深层侦听器,监听所有
watch(obj, (newVal, oldVal) => {
// 在嵌套的属性变更时触发
// 注意:`newVal` 此处和 `oldVal` 是相等的
// 因为它们是同一个对象!
})
// 监听对象中的某个值
// 需要通过getter 函数返回,不能直接监听
watch(
() => obj.count,
(newVal, oldVal) => {
console.log(newVal) // 1
console.log(oldVal) // 0
}
)
// 监听对象中的多个值
watch(
[() => obj.count, () => obj.size],
(newVal, oldVal) => {
console.log(newVal) // []
console.log(oldVal) // []
}
)
</script>
# 深层侦听器
默认监听一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发。如果通过对getter函数返回的对象进行监听,则不会及时触发,需要使用 deep
选项进行强制转成深层侦听器。
谨慎使用
深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
<script setup>
import { reactive, watch } from 'vue'
const person = reactive({
name: 'xxx',
student: {
grade: 1,
class: 2
}
})
// 通过getter 函数返回的对象
watch(
() => obj.student,
(newVal, oldVal) => {
// 注意:`newVal` 此处和 `oldVal` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)
</script>
# 即时回调的侦听器
watch 仅当数据源变化时,才会执行回调。如果希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。通过传入 immediate
选项来强制侦听器的回调立即执行。
watch(source, (newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })
# watchEffect
相比较 watch
手动去监听某些属性,watchEffect
可以允许我们自动跟踪回调的响应式依赖,只要在watchEffect回调函数中用到某个数据,它就会被触发,并且不需要使用immediate
就可以立即就执行。
<script setup>
import { ref, watchEffect } from 'vue'
const todoId = ref(1)
// 会立即就执行todoId为1的请求,当todoId改变时也会自动监听发送请求
watchEffect(async () => {
const response = await fetch( `http://xxxx.xxx/?todoId=${todoId.value}` )
console.log(response)
})
todoId.value = 2 // 继续请求
</script>
对于这种只有一个依赖项的例子来说,watchEffect()
的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect()
可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect()
可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
TIP
watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。
# 监听触发时机
监听器默认都是在组件更新之前被调用,如果想在侦听器回调中访问更新之后的 DOM,需要在第三个参数中添加 flush: 'post'
选项。
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
// watchEffect的另一种写法
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
# 停止监听
同步创建的监听器会自动停止,如果是异步创建的需要手动给停止掉,以防内存泄漏。
<script setup>
import { watch, watchEffect } from 'vue'
const msg = ref('xxx')
setTimeout(() => {
const unwatch = watch(msg, () => {})
const unwatchEffect = watchEffect(() => {})
}, 100)
// 手动停止
unwatch()
unwatchEffect()
</script>
# 列表渲染
相比较Vue2,template使用列表渲染,key直接绑定在template就行了。
<template v-for="todo in todos" :key="todo.name">
<li>{{ todo.name }}</li>
</template>
# 获取DOM
通过ref获取,绑到元素上就代表元素,绑到子组件上获取的是子组件的实例。
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
child.value // <Child /> 组件的实例
})
</script>
<template>
<input ref="input" />
<Child ref="child" />
</template>
获取 v-for 列表上的ref
ref 数组并不保证与源数组相同的顺序
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = ref([])
onMounted(() => {
console.log(itemRefs.value)
})
</script>
<template>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</template>
获取动态DOM元素上的ref,通过一个函数返回el元素然后赋值给一个变量,比如多个表单获取对应的ref进行验证。
<script setup>
import { ref, onMounted } from 'vue'
const formList = ref([/*...*/])
const refList = ref([])
// 通过函数获取ref列表,但是可能会存在多获取一遍ref的问题
const getRefs = (el) => {
refList.value.push(el)
}
</script>
<template>
<!-- <input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }"> -->
<!-- 要通过一个函数把el返回出来 -->
<a-form v-for="item in formList" :ref="el => getRefs(el)">
<a-form-item></a-form-item>
</a-form>
</template>
解决动态获取ref会多获取一遍问题:
<script setup>
import { ref, onMounted } from 'vue'
const formList = ref([/*...*/])
const refList = ref([])
const getRefs = (el, index) => {
refList.value[index] = el
}
</script>
<template>
<!-- 要通过一个函数把el返回出来 -->
<a-form v-for="(item, index) in formList" :ref="el => getRefs(el, index)">
<a-form-item></a-form-item>
</a-form>
</template>
新用法(3.5+):使用useTemplateRef
代替了 ref
。
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('my-input')
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="my-input" />
</template>
# nextTick使用
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}
# Props传参
父组件向子组件传参,子组件通过 defineProps()
宏接收参数。接收参数可以直接在模板上使用,如果要在<script setup>
中使用需要通过一个变量来接收。3.5+可以直接使用解构来接收。
<script setup>
// 赋值给一个变量进行使用
const props = defineProps(['msg', 'count'])
console.log(props.msg)
// 对象形式
const props = defineProps({
propA: String,
propB: Number,
propC: [Number, String],
propD: {
type: String,
required: true
},
propE: {
type: Number,
default: 100
},
propF: {
type: Arrary,
default: () => []
},
propG: {
type: Object,
default: () => {}
},
// 自定义类型校验函数
propH: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
}
})
</script>
<template>
<h4>{{ msg }}</h4>
<h4>{{ count }}</h4>
</template>
# Prop名字格式
一般,defineProps接收参数名使用camelCase
小驼峰写法,在组件上传参时使用kebab-case
形式进行传参。布尔类型传值时,不传默认为false,直接写参数名默认为true,不用再写true和false。
<!-- 父组件 -->
<template>
<MyComponent greeting-message="hello" :dept-id="1" disabled />
</template>
<!-- 子组件 -->
<script setup>
const props = defineProps({
greetingMessage: String,
deptId: Number,
disabled: Boolean
})
console.log(props.disabled) // true
</script>
# 动态传参
在vue2.6.0后新增了属性绑定和事件名的动态参数写法,用 []
包起来,其值为字符串类型,或者null。
<script setup>
import { ref, computed } from 'vue'
const attributeName = ref('msg')
const eventName = ref('focus') // focus、click、change等
// 复杂的属性名可以通过计算属性获取
const someAttr = computed(() => {
return '...'
})
</script>
<template>
<div>
<!-- 动态属性 -->
<a :[attributeName]="url"> ... </a>
<a :[someAttr]="url"> ... </a>
<!-- 动态绑定事件 -->
<a @[eventName]="doSomething">
</div>
</template>
# 多个props
有时候传参时需要传入一个对象中的多个多个值,一个一个的写太麻烦,可以使用 v-bind
直接传入整个对象。然后子组件就可以接收所有参数。
<script setup>
import { reactive, onMounted } from 'vue'
const obj = reactive({
id: 1,
name: 'xxx'
})
</script>
<template>
<div>
<SubComponent :id="obj.id" :name="obj.name"></SubComponent>
<!-- 简写 -->
<SubComponent v-bind="obj"></SubComponent>
</div>
</template>
# 解构Props
虽然props解构出来的参数不具有响应性,但Vue3.5+被解构出来的props依然可以被追踪到,父组件的数据被更改,传入的值依然会改变,所以可以直接使用解构来获取props了。并且通过解构还可以设置props的默认值。
<script setup>
const { msg, count } = defineProps(['msg', 'count'])
console.log(msg)
// 设置默认值
const { title = 'hello world' } = defineProps(['title'])
</script>
<template>
<h4>{{ msg }}</h4>
<h4>{{ count }}</h4>
</template>
# Props的使用和监听
Vue不允许直接修改传入的props参数,只能重新赋值进行使用。如果想监听某个prop的变化,可以使用getter函数或者computed计算属性。
<script setup>
const props = defineProps(['msg', 'count'])
// 需要通过getter 函数返回,不能直接监听
watch(() => props.count, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
// 解构用法
const { msg, count } = defineProps(['msg', 'count'])
watch(() => count, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
// 计算属性更改
const msgVal = computed(() => props.msg)
const countVal = computed(() => count)
console.log(msgVal.value)
console.log(countVal.value)
</script>
# 暴露属性与方法
Vue2中可以通过$parent
和$children
方法父子组件实例上的方法和属性,Vue3中关闭此操作,只能通过defineExpose
编译宏进行手动暴露,然后在父组件中通过模板ref进行获取。
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const count = ref<number>(12)
const getName = () => {
console.log(111)
}
defineExpose({
a: count, // 属性
getName // 方法
})
</script>
父组件使用:
<script setup lang="ts">
import { ref } from 'vue'
import SubItem from './components/SubItem.vue'
const refVue = ref<InstanceType<typeof SubItem>>()
console.log(refVue.value) // { a: count, getName }
</script>
<template>
<SubItem ref="refVue" msg="You did it!" />
</template>
# 自定义事件
自定义事件,需要通过 defineEmits()
宏来定义事件名后才能使用,在模板上可以直接使用$emit()
来触发事件,在<script setup>
中需要赋值给一个变量来调用。为了统一,都通过变量调用事件,不要直接在html上使用。
<!-- 子组件 -->
<script setup>
// 数组写法
defineEmits(['inFocus', 'submit'])
// 对象写法,可以对函数进行校验
const emit = defineEmits({
submit(payload) {
// 通过返回值为 `true` 还是为 `false` 来判断
// 验证是否通过
}
})
</script>
事件传递通用写法:
<!-- 子组件 -->
<script setup>
// 赋值给一个变量使用
const emit = defineEmits(['CLOSE_DIALOG_EVENT'])
function buttonClick() {
emit('CLOSE_DIALOG_EVENT', { msg: 'xxx' })
}
</script>
<template>
<button @click="buttonClick">关闭</button>
</template>
<!-- 父组件 -->
<template>
<div>
<SubComponent @CLOSE_DIALOG_EVENT="closeDialog"></SubComponent>
</div>
</template>
# 中央事件主线
Vue2 通过再创建一个Vue实例当做桥梁来进行组件之间的相互传参,Vue3也不再支持此方法,我们通过引入 mitt
插件来实现兄弟组件之间的事件传递。
安装:
npm install --save mitt
在main.ts中引入:
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './assets/main.css'
import mitt from 'mitt'
const emitter = mitt()
// TS注册
// 必须要扩展ComponentCustomProperties类型才能获得类型提示
declare module 'vue' {
export interface ComponentCustomProperties {
$Bus: typeof emitter
}
}
const app = createApp(App)
app.use(createPinia())
app.use(router)
// 挂载到全局属性上
app.config.globalProperties.$Bus = emitter
app.mount('#app')
组件中使用:
<script setup lang="ts">
import { ref, getCurrentInstance } from 'vue'
// 获取vue实例
const instance = getCurrentInstance()
// 监听一个
instance?.proxy?.$Bus.emit('GET_EMITE_EVENT', { a: 1, b: 2})
instance?.proxy?.$Bus.on('GET_EMITE_EVENT', (params) => {
console.log(params) // { a: 1, b: 2}
})
// 监听多个
instance?.proxy?.$Bus.emit('GET_ID_EVENT1', 1)
instance?.proxy?.$Bus.emit('GET_ID_EVENT2', 2)
instance?.proxy?.$Bus.on('*', (type, params) => {
console.log(type) // GET_ID_EVENT1/GET_ID_EVENT2
console.log(params) // 1/2
})
// 清除
instance?.proxy?.$Bus.emit('SEND_MSG_EVENT', 'xxx')
const sendMsg = (msg: string) => {
console.log(msg)
}
instance?.proxy?.$Bus.on('SEND_MSG_EVENT', sendMsg)
instance?.proxy?.$Bus.off('SEND_MSG_EVENT', sendMsg)
// 清除全部
instance?.proxy?.$Bus.all.clear()
</script>
# 组件使用v-model
3.4+版本使用方式进行了更改,不再使用 modelValue 与 update:modelValue 进行双向绑定,直接使用defineModel宏。
Vue2组件实现 v-model 双向绑定通过 model
字段来定义传入的props和事件名,以此来实现组件上数据的双向绑定,Vue3中不需要使用 model
字段,直接使用 defineModel
宏接收就可以了,接收的值就是v-model的值,直接更改这个值就自动更新父组件的值,它是modelValue
与 update:modelValue
的简写。
<!-- // 子组件获取和提交 -->
<script setup>
// defineModel宏会自动接收v-model的值
const msgModel = defineModel()
// 更改v-model值
const updateValue = () => {
msgModel.value = 'hello world'
}
</script>
<template>
<input v-model="msgModel" />
<button @click="updateValue">双向绑定</button>
</template>
<!-- // 父组件传入 -->
<script setup lang="ts">
import { ref } from 'vue'
import TheWelcome from '../components/TheWelcome.vue'
const msg = ref('xxxxx')
</script>
<template>
<main>
<p>{{ msg }}</p>
<TheWelcome v-model="msg" />
</main>
</template>
# 自定义参数名
自定义参数名,直接在v-model
后面绑定对应的名称就可以了,并且可以同时自定义多个双向绑定。
<!-- // 子组件 -->
<script setup>
// 接收多个v-model
const title = defineModel('title')
const id = defineModel('id')
const bindValue = () => {
title.value = 'hello world'
id.value = 2
}
</script>
<template>
<button @click="bindValue">双向绑定</button>
</template>
<!-- // 父组件 -->
<script setup lang="ts">
import { ref } from 'vue'
import TheWelcome from '../components/TheWelcome.vue'
const msg = ref('xxxxx')
const id = ref(1)
</script>
<template>
<main>
<p>{{ msg }}</p>
<TheWelcome v-model:title="msg" v-model:id="id" />
</main>
</template>
# 自定义修饰符
v-model
后面可以跟一些修饰符,比如 .trim
.lazy
等,我们也可以自定义修饰符,比如定义一个capitalize
修饰符,让传入的值第一个字符串变为大写。
<MyComponent v-model.capitalize="myText" />
组件的 v-model
上所添加的修饰符,可以通过defineModel接收的第二个参数进行获取。
<script setup>
const [myText, modifiers] = defineModel()
console.log(modifiers) // { capitalize: true }
</script>
<template>
<input type="text" v-model="myText" />
</template>
所以根据第二个参数 modifiers
返回的对象,我们给defineModel传入一个对象,通过get和set两个选项就可以对这个修饰符判断进行逻辑处理。
<MyComponent v-model.capitalize="myText" />
<script setup>
const [myText, modifiers] = defineModel({
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
}
})
</script>
<template>
<input type="text" v-model="myText" />
</template>
带参数的修饰符
<MyComponent v-model:title.capitalize="myText" v-model:id="userId"/>
# 透传 Attributes
透传属性就是在自定义组件上绑定的属性(除被用过的props
和emits
传值外的其他属性,没有被用的不算),如class
、style
、id
或绑定的事件都会被子组件内的元素所继承。
透传过去的属性,对于 class
和 style
会和子组件内的元素进行合并,v-on
绑定的事件透传过去会被触发,内部元素也绑了事件则都会被触发。
如果子组件又直接套了个组件,没有其他元素,透传属性会直接继承到最里一层的组件。
<MyButton class="large" @click="onClick" />
<!-- 子组件 -->
<button class="large" @click="onClick"></button>
# 获取透传属性
在模板上可以直接通过 $attrs
获取,在 <script setup>
中需要通过 useAttrs()
方法进行获取。
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log(attrs)
</script>
<template>
<div>
<!-- DOM上直接通过$attrs获取 -->
<span>attribute: {{ $attrs }}</span>
</div>
</template>
只有一个根节点时透传属性会默认作用到组件的根节点身上,如果想要左右在组件里的某个元素上,我们可以使用v-bind
绑在某个元素身上。
<template>
<div class="box">
<button v-bind="$attrs">Click Me</button>
</div>
</template>
如果有多个根节点,要使用v-bind
进行绑定某个根节点,否则透传会报错。
<template>
<header v-bind="$attrs"></header>
<main></main>
<footer></footer>
</template>
# 禁用透传属性
<script setup>
defineOptions({
inheritAttrs: false
})
</script>
# 插槽
插槽通过 <slot>
来分发内容,给一个占位符,在指定的位置添加特殊的元素,而不是在组件创建时就固定好内容,允许在不同的地方调用组件时添加定制化的内容。
<!-- FancyButton -->
<template>
<div>
<slot></slot>
</div>
</template>
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import FancyButton from './FancyButton.vue'
const msg = ref('xxx')
</script>
<template>
<div>
<FancyButton>
<span>我是插槽内容:{{msg}}</span>
</FancyButton>
</div>
</template>
在父组件中添加就只能获取父组件的作用域,无法获取插槽所在子组件的作用域。
# 默认内容
在 slot
内添加默认的内容,父组件添加内容就不显示。
<template>
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
</template>
# 具名插槽(多个插槽)
添加多个插槽时,要为每个插槽指定一个特殊属性 name
。
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<!-- 默认为 default -->
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
父组件添加内容,使用 v-slot
指定对应插槽名,并且只能添加在 <template>
上,default可以不用写,没有被具名插槽包裹的内容全部视为默认插槽内容。
<template>
<BaseLayout>
<template v-slot:header>
<h1>我是头部插槽内容</h1>
</template>
<template v-slot:default>
<p>我是默认插槽内容</p>
<p>111111</p>
</template>
<template v-slot:footer>
<p>我是尾部插槽内容</p>
</template>
</BaseLayout>
</template>
具名插槽 v-slot
可以简写成 #
,因此 <template v-slot:header>
可以简写为 <template #header>
。
<template>
<BaseLayout>
<template #header>
<h1>我是头部插槽内容</h1>
</template>
<template #default>
<p>我是默认插槽内容</p>
<p>111111</p>
</template>
<template #footer>
<p>我是尾部插槽内容</p>
</template>
</BaseLayout>
</template>
# 条件插槽
我们可以使用 $slots
来获取父组件传入的插槽进行逻辑判断。
<template>
<div class="container">
<header v-if="$slots.header">
<slot name="header"></slot>
</header>
<main v-if="$slots.default">
<slot></slot>
</main>
<footer v-if="$slots.footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
# 动态插槽
<template>
<BaseLayout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</BaseLayout>
</template>
# 作用域插槽
在父组件中获取子组件中的数据,可以在子组件 <slot>
上绑定子组件数据,传给父组件的 <template>
上,这样在父组件定义插槽内容就可以获取子组件的内容了。默认插槽可以通过v-slot
指令接收,具名插槽直接使用插槽名进行接收。
<!-- MyComponent 子组件 -->
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: 'xxx',
age: 18
})
</script>
<template>
<span>
<!-- 子组件就相当于props把值传入到父组件中 -->
<slot v-bind:user="user"> {{ user.lastName }} </slot>
<!-- 简写 -->
<slot :user="user"> {{ user.lastName }} </slot>
</span>
<span>
<slot name="footer" :user="user"></slot>
</span>
</template>
<!-- 父组件使用 -->
<!-- 定义一个slotProps的props对象 -->
<template>
<MyComponent>
<!-- 默认插槽 -->
<template v-slot="slotProps">
{{ slotProps.user }}
</template>
<template #default="slotProps">
{{ slotProps.user }}
</template>
<!-- 具名插槽 -->
<template #footer="slotProps">
{{ slotProps.user }}
</template>
<!-- 使用ES6解构 -->
<template #default="{ user }">
{{ user }}
</template>
<template #footer="{ user }">
{{ user }}
</template>
</MyComponent>
</template>
# 插槽复用
在一个循环列表里定义一个具名插槽,可以实现循环来使用同一个插槽。
<ul>
<li v-for="todo in todos" :key="todo.id" >
<!-- 我们为每个 todo 准备了一个插槽, 将 `todo` 对象作为一个插槽的 prop 传入。 -->
<slot name="todo" :todo="todo">
<!-- 默认内容 -->
{{ todo.text }}
</slot>
</li>
</ul>
使用:
<todo-list :todos="todos">
<template #todo="{ todo }">
<span v-if="todo.isComplete">✓</span>
{{ todo.text }}
</template>
</todo-list>
# 依赖注入
props嵌套太多,逐级传入的话太麻烦,使用provide
和 inject
进行依赖注入,父组件使用 provide
提供依赖,任何层级子组件都可以使用 inject
获取注入数据。
父组件提供依赖,可以同时提供多个。
<script setup>
import { ref, reactive, provide } from 'vue'
const text = ref('text')
const msg = ref('xxx')
const count = ref(123)
const user = reactive({
name: 'xxx',
age: 18
})
const getData = () => {
console.log(111)
}
// 注入变量
provide('message', msg)
provide('user', user)
// 注入只读属性
provide('count', readonly(count))
provide('text', text)
// 注入函数
provide('getData', getData)
</script>
子组件进行接收
<script setup>
import { inject } from 'vue'
// 接收属性
const message = inject('message')
const user = inject('user')
const count = inject('count')
// 默认值
const text = inject('text', '我是默认值')
// 接收函数
const getData = inject('getData')
// 默认执行函数,使用工厂函数来创建默认值,true表示默认值应该被当作一个工厂函数。
const value = inject('key', () => new ExpensiveClass(), true)
</script>
<template>
<button @click="getData">{{ location }}</button>
</template>
应用层 Provide,提供一个全局依赖,任何组件都能获取到注入。
import { createApp } from 'vue'
const app = createApp({})
app.provide('message', 'hello!')
# 递归组件
Vue3中组件没有了name属性,组件引入后直接使用就行,那递归组件也是同样如此,文件名是什么,就直接使用递归组件的文件名作为组件进行循环递归。如果不想直接使用文件名,我们也可以自定义一个名字使用,就是再使用一个 script
标签导出名称,或者使用官方提供的插件 unplugin-vue-define-options
。
父组件:
<script setup>
import { ref, reactive } from 'vue'
import TreeVue from '@/components/Tree.vue'
// 树形数据
interface Tree {
title: string,
checked: boolean,
children?: Tree[]
}
const treeData = reactive<Tree[]>([
{
title: '文件1-1',
checked: false,
children: [
{
title: '文件2-1',
checked: false,
children: []
}
]
},
{
title: '文件1-2',
checked: false,
children: []
}
])
</script>
<template>
<TreeVue :treeData="treeData"></TreeVue>
</template>
递归组件:
<!-- Tree 组件 -->
<script setup lang="ts">
interface Tree {
title: string,
checked: boolean,
children?: Tree[]
}
const props = defineProps<{
treeData: Tree[]
}>()
</script>
<!-- 组件重命名 -->
<script lang="ts">
export default {
name: 'TreeItem'
}
</script>
<template>
<div class="tree" v-for="(item, index) in treeData" :key="index">
<input v-model="item.checked" type="checkbox"> <span>{{ item.title }}</span>
<TreeItem v-if="item?.children?.length" :treeData="item?.children"></TreeItem>
</div>
</template>
<style scoped>
.tree {
width: 100%;
margin-left: 20px;
}
</style>
# 缓存组件
使用 <KeepAlive>
缓存组件或者路由。
<script setup>
import { onActivated, onDeactivated } from 'vue'
// 生命周期
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>
<template>
<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
<component :is="activeComponent" />
</KeepAlive>
<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
<component :is="view" />
</KeepAlive>
<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
<component :is="view" />
</KeepAlive>
<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
<component :is="view" />
</KeepAlive>
</template>
在组合式API中组件会自动以文件名为该组件的 name
,KeepAlive使用 include
去找组件名时可能和我们在路由中定义的名称不一样,导致无法缓存,所以要在需要缓存的组件中自定义组件命才能使用。
# 自定义组件名
自定义组件名可以向上面一样重新定义一个 script
或者使用插件 vite-plugin-vue-setup-extend
,一般我们使用 vite-plugin-vue-setup-extend
插件来定义页面和组件,直接在 script
标签上定义 name
更容易进行区分。
# 安装
npm i vite-plugin-vue-setup-extend -D
配置vite.config.ts
import { defineConfig } from 'vite'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
plugins: [ VueSetupExtend() ]
})
使用
<script setup lang="ts" name="Home">
</script>
# 组合式函数
把相应的逻辑函数抽离成一个公共函数达到逻辑复用,然后引入使用,既可以在其他组件中使用,也可以引入其他组合式函数,这也就是Vue3使用基础js进行编程的好处,以最基础的js函数进行抽离,达到逻辑复用,回归本质。通过组合式函数,我们可以实现自定义hook。
按照惯例,组合式函数约定用驼峰命名法命名,并以“use”作为开头。返回值约定始终返回一个包含多个 ref 的普通对象,而不是 reactive 响应式对象,这样在获取是可以解构出响应的ref数据。
// mouse.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 }
}
在组件中引入使用:
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
再比如封装一个请求函数,传入url请求地址返回成功和错误状态。并且还可以用 watchEffect()
和 toValue()
搭配实现url更改后立即就调用。
// fetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = () => {
data.value = null
error.value = null
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
// 在watchEffect中执行
watchEffect(() => {
fetchData()
})
return { data, error }
}
在组件中重复使用:
<script setup>
import { useFetch } from './fetch.js'
const url = ref('/initial-url')
const { data, error } = useFetch(url)
// 重新触发 fetch
url.value = '/new-url'
</script>
传入参数处理:
封装组合式函数时,传入的参数可能是一个响应式数据,也可能是一个普通值,也可能是一个函数返回值,我们可以使用 toValue()
来处理传入的参数,将值、refs 或 getters 规范化为值。如果参数是 ref,它会返回 ref 的值;如果参数是函数,它会调用函数并返回其返回值。否则,它会原样返回参数。
import { toValue } from 'vue'
function useFeature(maybeRefOrGetter) {
// 如果 maybeRefOrGetter 是一个 ref 或 getter,
// 将返回它的规范化值。
// 否则原样返回。
const value = toValue(maybeRefOrGetter)
}
通过抽取组合式函数改善代码结构:
<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'
const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>
# 自定义指令
Vue3在<script setup>
中注册局部指令时,任何以 v
开头的驼峰式命名的变量都可以被用作一个自定义指令,注意是变量,一般定义为一个对象。而全局注册还是通过 directive
进行。
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
全局注册:
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
全局注册多个:
在index.ts中引入指令文件,通过插件统一进行注册,最后在main.ts中使用。
自定义一个指令
// directive/modules/index.ts
/**
* 自动获取焦点指令
* @description 打开页面时输入表单自动获取焦点
* @author changz
* @param {xxx} - xxx
* @example
* <a-input v-focus></a-input>
* */
import type { DirectiveBinding, VNode } from 'vue'
export default {
mounted(el: HTMLInputElement, binding: DirectiveBinding, vnode: VNode) {
if (el.tagName === 'input' || el.tagName === 'textarea') {
el.focus()
} else {
const inputDom = el.querySelector('input') ?? el.querySelector('textarea')
inputDom?.focus()
}
}
}
在index.ts引入指令,通过插件获取到Vue进行注册,然后在main中引入使用该插件。
// directive/index.ts
/**
* @description 定义多个全局自定义指令
* @author changz
* @example 在main.js引入之后use挂载
* */
import type { App, Directive } from 'vue'
// 导入自定义指令
import focus from './modules/focus'
import watermark from './modules/watermark'
import permission from './modules/permission'
export default {
install(Vue: App) {
Vue.directive('focus', focus as Directive)
Vue.directive('watermark', watermark as Directive)
Vue.directive('permission', permission as Directive)
}
}
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './stores'
import directive from './directive'
const app = createApp(App)
app.use(store)
app.use(router)
app.use(directive)
app.mount('#app')
钩子函数:
const vDirective = {
// 在绑定元素的 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) {}
}
钩子参数:
<script setup>
import { ref } from 'vue'
const arg = ref('msg')
// 在模板中启用 v-example
const vExample = {
mounted(el, binding, vnode, prevVnode) {
// el:指令绑定到的元素。这可以用于直接操作 DOM。
console.log(el) // <div></div>
// binding:一个对象,包含以下属性。
// 传递给指令的值。
console.log(binding.value) // 例如在 v-example:foo="2" 中,值是 2。
// 之前的值,仅在 beforeUpdate 和 updated 中可用。
console.log(binding.oldValue) // 无论值是否更改,它都可用。
// 传递给指令的参数 (如果有的话)。
console.log(binding.arg) // 例如在 v-example:foo 中,参数是 "foo"。
// 一个包含修饰符的对象 (如果有的话)。
console.log(binding.modifiers) // 例如在 v-example:foo 中,修饰符对象是 { foo: true }。
// 使用该指令的组件实例。
console.log(binding.instance)
// 指令的定义对象。
console.log(binding.dir)
// vnode:代表绑定元素的底层 VNode。
console.log(vnode)
// prevNode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
console.log(prevNode)
}
}
</script>
<template>
<div v-example:foo="2"></div>
<!-- 动态指令 -->
<div v-example:[arg]="3"></div>
<!-- 多个参数 -->
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
</template>
# 插件
Vue的插件为整个Vue实例添加全局功能,可以添加全局方法、属性、全局组件或自定义指令。编写一个对象,通过提供一个 install()
方法注册,然后使用 use
来使用和安装install提供的实例和参数。
定义一个插件:
// plugins/example.js
const myPlugin = {
install(app, options) {
// app vue实例
console.log(app)
// options use传入的参数
console.log(options)
// 配置此应用
// 注入一个全局可用的方法
app.config.globalProperties.$translate = (params) => {
console.log(params)
}
// 注入一个全局属性
app.config.globalProperties.attrNmae = 'xxx'
// 注册一个全局组件
app.component('MyComponent', {
data() {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
// 注册一个全局指令
app.directive('focus', {
/* ... */
})
// 提供一个全局依赖注入
app.provide('example', options)
}
}
安装插件:
import { createApp } from 'vue'
const app = createApp({})
app.use(myPlugin, {
/* 可选的选项 */
})
# 过渡效果
Vue3过渡效果代替使用的组件分别是 <Transition>
和 <TransitionGroup>
其使用方法和Vue2相同。Transition内只能使用一个根元素,TransitionGroup用于多个元素。
<template>
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>
</template>
<style>
/* 进入时开始和结束的css样式 */
.v-enter-from {}
.v-enter-to {}
/* 进入时开始到结束的过渡效果 */
.v-enter-active {
transition: all 0.5s ease;
}
/* 离开时开始和结束的css样式 */
.v-leave-from {}
.v-leave-to {}
/* 离开时开始到结束的过渡效果 */
.v-leave-active {
transition: opacity 0.5s ease;
}
/* 使用动画 */
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(1);
}
}
</style>
使用动画:
<template>
<Transition name="bounce">
<p v-if="show" style="text-align: center;">
Hello here is some bouncy text!
</p>
</Transition>
</template>
<style>
/* 使用动画 */
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(1);
}
}
</style>
自定义类名,或者使用animate.css的样式:
enter-from-class
enter-to-class
enter-active-class
leave-from-class
leave-to-class
leave-active-class
<template>
<!-- 假设你已经在页面中引入了 Animate.css -->
<Transition
name="custom-classes"
enter-active-class="animate__animated animate__tada"
leave-active-class="animate__animated animate__bounceOutRight"
>
<p v-if="show">hello</p>
</Transition>
</template>
列表动画 TransitionGroup
:
<template>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item">
{{ item }}
</li>
</TransitionGroup>
</template>
<style>
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>
# Teleport
使用 <Teleport>
将其内部的组件或DOM元素挂载到其他元素上去。其所处的环境还是在当前的组件里,不会影响组件间的任何逻辑关系,只是DOM元素是显示在被挂载的元素里。
比如一个modal弹窗,使用是组件中控制显示隐藏,但我们需要让他针对全窗口显示,只需要使用 <Teleport>
把它放到body上就行了。
<template>
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div class="modal-mask" v-if="open">
<div class="modal-container" v-if="open">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</div>
</Teleport>
</template>
<style>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
transition: opacity 0.3s ease;
}
.modal-container {
width: 300px;
margin: auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}
</style>
# Suspense
TIP
Suspense是一项实验性功能。它不一定会最终成为稳定功能,并且在稳定之前相关 API 也可能会发生变化。
<Suspense>
是一个内置组件,获取其中整个组件树中所有异步(异步请求或异步组件)的处理结果,并根据这个结果来渲染加载状态。比如一个页面布局有三个组件,每个组件中都请求加载数据,加载时都会有加载动画在不同的时候加载完成,这样就很不美观。使用Suspense就会解决这种问题,只显示一个加载状态,就像 Promise.all()
等待所有加载请求完。
在<script setup>
中需要使用await
让组件自动成为一个异步依赖。
<script setup>
const res = await fetch(...)
const posts = await res.json()
</script>
<template>
{{ posts }}
</template>
加载中状态:
<template>
<Suspense>
<!-- 具有深层异步依赖的组件 -->
<Dashboard />
<!-- 在 #fallback 插槽中显示 “正在加载中” -->
<template #fallback>
Loading...
</template>
</Suspense>
</template>
# 渲染函数 & JSX
Vue 提供了一个 h() 函数用于创建 vnodes:
import { h } from 'vue'
const vnode = h(
'div', // type
{ id: 'foo', class: 'bar' }, // props
[
/* children */
]
)
# 组合式API结合TS
# 为ref / reactive标注类型
使用ref定义时,一般ts会自动推导类型。
<script setup lang="ts">
import { ref } from 'vue'
import type { Ref } from 'vue'
const title = ref<string>('首页')
const status: Ref<undefined | number> = ref('2020')
// 简写
const title = ref('首页')
const status = ref<undefined | number>(0)
// 不给初始值会自动转为包含 undefined 的联合类型
// 推导得到的类型:Ref<number | undefined>
const status = ref<number>()
</script>
在定义reactive对象时,一般ts会自动推导类型,但在使用时,比如赋值或者传参可能无法识别类型,所以最好在创建时为其进行类型标注。
<script setup lang="ts">
import { reactive } from 'vue'
interface StateData {
name: string;
count: number;
type: undefined | '';
date?: string;
[key: string]: any
}
const state = reactive<StateData>({
name: 'xxx',
count: 0,
type: undefined,
data: '2023-01-01',
status: 0
})
const tableData = reactive<StateData[]>([])
tableData.push(state)
</script>
# 为computed标注类型
与ref类似,一般简单类型computed都会自动推导出,我们可以为其定义类型。
const double = computed<number>(() => {
// 若返回值不是 number 类型则会报错
})
# 为props标注类型
props使用ts类型进行标注时传入一个泛型对象,只需要标注其类型就可以,基于类型声明,也可以通过一个接口单独声明。
<script setup lang="ts">
// 传入泛型进行声明
const { title, bar } = defineProps<{
title: string
bar?: number
}>()
// 声明props接口
interface Props {
foo: string
bar?: number
}
const { title, bar } = defineProps<Props>()
</script>
如果需要设置默认值,3.4及更低版本需要使用自定义宏 withDefaults
传入第二个参数来支持,3.4+可以通过响应式解构来添加默认值。
<script setup lang="ts">
// 低版本定义默认值
const props = withDefaults(defineProps<{
msg: string
arr: string[]
}>(), {
msg: 'hello',
arr: () => []
})
// 接口写法
interface Props {
msg: string
arr: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
arr: () => ['one', 'two']
})
// 3.4+定义默认值
const { msg = 'hello', arr = ['one', 'two'] } = defineProps<{
msg: string
arr: string[]
}>()
const { msg = 'hello', arr = ['one', 'two'] } = defineProps<Props>()
</script>
# 为事件标注类型
通过一个函数给 defineEmits
传入一个对象来进行类型校验。
<!-- 子组件 -->
<script setup lang="ts">
import { reactive, ref, getCurrentInstance } from 'vue'
interface MsgData {
name: string
id: number
}
const formData = reactive<MsgData>({
name: '',
id: 0
})
const emit = defineEmits<{
'sendSome': [], // 什么都不传
'sendMsg': [id: number], // 传递一个参数
'sendMulMsg': [id: number, name: string], // 传递多个参数
'sendObjMsg': [obj: MsgData], // 传递一个对象
'sendOptMsg': [id?: number], // 可选参数
}>()
const sendEvent = () => {
emit('sendMsg', 1)
emit('sendMulMsg', 1, 'test')
emit('sendObjMsg', formData)
emit('sendOptMsg')
}
// 低版本写法
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
父组件接收:
<template>
<div class="index">
<!-- 子组件 -->
<Child @sendMsg="getMsg" @sendMulMsg="getMulMsg" @sendObjMsg="getObjMsg" @sendOptMsg="getOptMsg"></Child>
</div>
</template>
<script setup lang="ts">
const getMsg = (id: number) => {
console.log(id)
}
const getMulMsg = (id: number, name: string) => {
console.log(id)
console.log(name)
}
const getObjMsg = (obj: any) => {
console.log(obj)
}
const getOptMsg = () => {
console.log(1111)
}
</script>
# 为模板引用标注类型
ref模板引用标明类型:
<script setup lang="ts">
import { ref, useTemplateRef, onMounted } from 'vue'
// const inputRef = ref<HTMLInputElement | null>(null)
// useTemplateRef代替ref
const inputRef = useTemplateRef<HTMLInputElement>(null)
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="inputRef" />
</template>
为组件模板引用标明类型:
<script setup lang="ts">
import { ref, useTemplateRef, InstanceType, onMounted } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
// const worldRef = ref<InstanceType<typeof HelloWorld> | null>(null)
const worldRef = useTemplateRef<InstanceType<typeof HelloWorld>>(null)
// 或者使用ComponentPublicInstance
// const worldRef = ref<ComponentPublicInstance | null>(null)
const worldRef = useTemplateRef<ComponentPublicInstance | null>(null)
</script>
<template>
<component is="HelloWorld" ref="worldRef" />
</template>
# 为 provide / inject 标注类型
provide 和 inject 通常会在不同的组件中运行。要正确地为注入的值标记类型,Vue 提供了一个 InjectionKey 接口,它是一个继承自 Symbol 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型:
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'
const key = Symbol() as InjectionKey<string>
provide(key, 'foo') // 若提供的是非字符串值会导致错误
const foo = inject(key) // foo 的类型:string | undefined
建议将注入 key 的类型放在一个单独的文件中,这样它就可以被多个组件导入。
Router路由 →