目录
挂载全局属性和方法
挂载属性和方法可以使用globalProperties
属性来进行挂载
main.ts
import * as echarts from 'echarts'
// 例如挂载echarts到全局
app.config.globalProperties.$echarts = echarts
创建应用实例
每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例:
import { createApp } from 'vue'
const app = createApp({
/* 根组件选项 */
})
多个应用实例
应用实例并不只限于一个。createApp
API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。
const app1 = createApp({
/* ... */
})
app1.mount('#container-1')
const app2 = createApp({
/* ... */
})
app2.mount('#container-2')
v-bind一次绑定多个值
相当于
<div :id="container" :class="wrapper"></div>
只不过使用v-bind
绑定对象比较方便,适合绑定的属性比较多时
如果 attribute 的名称与绑定的 JavaScript 值的名称相同,那么可以进一步简化语法,省略 attribute 值:
<!-- 与 :id="id" 相同 -->
<div :id></div>
<!-- 这也同样有效 -->
<div v-bind:id></div>
v-bind用在样式中
v-bind
其实还有一个妙用,就是可以用在css
样式中。v-bind(js变量)
<template>
<div class="box"></div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const redColor = ref('red')
</script>
<style scoped>
.box {
width: 500px;
height: 500px;
/* v-bind() 参数可以直接写js变量,能够识别 */
background-color: v-bind(redColor);
}
</style>
Vue指令绑定值
-
指令绑定值都是一样的道理,这里以
v-bind
举例 -
js
表达式可以被使用的应用场景:插值表达式中和Vue
指令中(这个很重要,记死) -
什么是
js
表达式呢?很简单,只要结果返回的是一个值,能被函数return
出去,则它就是个js
表达式
属性绑定中的值,如果是需要计算得出来的,则可以使用es6
的模板字符串进行处理和拼接,上面箭头标注的地方,这样大大提升了属性绑定的灵活性。当然也可以通过计算属性计算所得,甚至是三元表达式处理所得。直接绑定个方法都可以
<template>
<div class="box">
<!-- 属性绑定:可以绑定静态资源,变量,计算属性,方法,三元,动态拼接等等都可以 -->
<Son
num="99"
:color="red"
:class="`${red}-num` + '1'"
:data="data"
:count="flag ? 100 : 0"
:getRes="getRes"
></Son>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import Son from './About.vue'
const red = ref('red')
const flag = ref(true)
const data = computed(() => {
return '123'
})
const getRes = () => {
return 'result'
}
</script>
唯一需要注意的一点:就是绑定函数时,每次组件更新时都会重新调用一次,所以尽可能避免在绑定的函数中出现副作用(副作用就是除了函数实现自身主要功能以外的操作,例如操作dom
,ajax
请求等)
我自己尝试了下,只要①子组件标签上绑定的函数带(),并且②父组件触发了update生命周期,此函数就会执行! 所以确实不应该在绑定的函数里出现副作用,不然开销太大了
父组件
<template>
<Son :fn="fn()"></Son>
<button @click="num++">父组件按钮{{ num }}</button>
</template>
<script setup lang="ts">
import Son from './Son.vue'
import { ref, onUpdated } from 'vue'
const num = ref(0)
const fn = () => {
console.log('父组件方法执行')
}
onUpdated(() => {
console.log('父组件更新')
})
</script>
子组件
<template>
<div>
<h2>子组件内容</h2>
</div>
</template>
Vue指令绑定属性
- 上面讲了怎么动态绑定值。这里讲怎么绑定动态的属性
动态属性的约束
- 应该是一个字符串或者是
null
。不支持在里面计算,如果复杂的,最好用计算属性
注意:直接在动态属性中写模板字符串,也是支持的。因为它也是个字符串(只不过里面可以写变量)
也可以直接在动态属性中写个函数,前提是函数的返回值要满足计算属性名的类型
函数返回值需要满足计算属性名的类型(上面的限制中说只能是字符串或者是null
,但是写数字类型也是能出来的)
Dom更新时机
- 适合用于改变数据后,想要立马对改变后的最新的数据进行使用的情况。因为
$nexttick
函数中包裹的逻辑会在下一个Dom
声明周期中调用,可以把它理解成一个延迟的回调函数
<template>
<button id="counter" @click="increment">{{ count }}</button>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
count.value++
// DOM 还未更新 正常情况应该是执行完这个函数后Dom才会更新
console.log((document.getElementById('counter') as HTMLElement).textContent) // 0
// 可以打个断点,暂停一下函数,F12检查看一下Dom中的值,确实是0,然后点击继续执行后,函数执行过了,Dom中的值才变为了1
debugger
await nextTick()
// DOM 此时已经更新。此时我们就可以直接使用count变量最新的值了
console.log((document.getElementById('counter') as HTMLElement).textContent) // 1
}
</script>
<style scoped>
.box {
width: 500px;
height: 500px;
background: skyblue;
}
</style>
”可写的“计算属性
- 计算属性是只读的,不允许修改。但是有的时候我们有让计算属性可写的需求。就可以使用计算属性的对象形式来进行修改
最典型的用法就是给组件绑定一个v-model
父组件
<template>
<p>父组件内:{{ data }}</p>
<Son v-model="data"></Son>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Son from './About.vue'
const data = ref(100)
</script>
<style scoped>
.box {
width: 500px;
height: 500px;
background: skyblue;
}
</style>
子组件
<template>
<div>
<el-input v-model="_data"></el-input>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emits = defineEmits(['update:modelValue'])
const _data = computed({
get() {
return props.modelValue
},
set(value) {
emits('update:modelValue', value)
},
})
</script>
上面的步骤看起来很绕,其实步骤分好,就清楚了
- 对子组件绑定
v-model
,其实就相当于下面的写法
<input
:model-value="data"
@update:model-value="newValue => data = newValue"
/>
- 子组件写了一个计算属性
_data
,get
函数返回父组件传来的最新的值。而set
函数则负责向父组件抛事件(子向父组件传值)。利用父组件的update:model-value
函数对data
进行赋值(emits
传来的可是子组件_data
的值)。所以此时父组件的data
就是子组件_data
改变后的值。data
发生改变了,则子组件接收的值肯定也就发生改变了。所以父子组件就产生了数据联动
官网链接:组件 v-model | Vue.js (vuejs.org)
v-if与v-for不建议同时使用
- 在
Vue3
中,如果v-if
和v-for
都用在了同一个标签上,则v-if
会优先执行
v-for
推荐绑定一个key
值,key
值最好是字符串或者数字v-for
中可以使用对象结构
<template>
<ul>
<li v-for="{ id, name, age } in list" :key="id">
<p>{{ name }} -- {{ age }}</p>
</li>
</ul>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const list = ref([
{ id: 0, name: 'zs', age: 22 },
{ id: 1, name: 'ls', age: 23 },
{ id: 2, name: 'ww', age: 24 },
])
</script>
v-for遍历对象
v-for补充
可以使用 of
作为分隔符来替代 in
,这更接近 JavaScript 的迭代器语法:
xxxxxxxxxx <div v-for="item of items"></div>
v-for
可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n
的取值范围重复多次:
<span v-for="n in 10">{{ n }}</span>
-
注意此处
n
的初值是从1
开始而非0
。
在组件上使用 v-for
,和在一般的元素上使用没有区别 (别忘记提供一个 key
):
<MyComponent v-for="item in items" :key="item.id" />
但是,这不会自动将任何数据传递给组件,因为组件有自己独立的作用域。为了将迭代后的数据传递到组件中,我们还需要传递 props:
<MyComponent
v-for="(item, index) in items"
:item="item"
:index="index"
:key="item.id"
/>
数组变化检测
-
总结起来就是:
js
数组方法中,如果会改变原数组,则就不需要重新赋值。如果不会改变原数组,则就需要重新赋值 -
这点很重要,开发中大部分时候都是处理数组数据的
-
有时候我们不想改变原数组,但是又需要使用触发响应式的方法。则可以对原数组进行一下深拷贝,然后用深拷贝的值进行操作
事件修饰符
.stop
:阻止冒泡.prevent
:阻止默认行为.self
:只有自己能触发.capture
:使用捕获模式(冒泡是由小到大,而捕获则是由大到小).once
:只触发一次.passive
:不阻止默认行为
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>
<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>
<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>
<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>
- 还有按键事件修饰符和鼠标事件修饰符,但是用的极少。事件修饰符常用的也就阻止冒泡和阻止默认行为
v-model用在表单类标签上
v-model还可以绑定一个数组
- 如果表单类的组件需要多选时,就可以绑定一个数组。例如复选框
checkbox
,下拉选择selected
多选状态下
复选框设置真假值
v-model的修饰符
组件上v-model的参数
- 其实前面表单类的标签使用
v-model
,后面其实都是有对应的参数的,只不过默认带上了- 例如
input
标签和textarea
标签绑定v-model
。其实就是v-model:value
- 例如
checkbox
标签和radio
标签绑定v-model
。其实就是v-model:checked
- 例如
select
标签绑定v-model
。其实就是v-model:value
- 例如
组件绑定多个v-model
- 组件绑定多少个
v-model
都可以,只要参数不同,能区分开就行
自定义v-model修饰符
v-model
后面.
的部分就是修饰符。默认提供的有lazy
,number
,trim
下面我们自己写一个:
父组件:
<template>
<About v-model.flag="data"></About>
</template>
<script setup lang="ts">
import About from './About.vue'
import { ref } from 'vue'
const data = ref(100)
</script>
子组件:注意注释的内容部分
<template>
<div>
<h2>子组件内容</h2>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
modelValue: Number,
// 对象里面存放的就是修饰符(不管是自定义的还是自带的,都在里面)
modelModifiers: {
type: Object,
default: () => ({}),
},
})
// v-model的修饰符是个布尔类型
// 使用v-model的时候带这个修饰符了,则这个修饰符的值就是true,反之就是false
console.log(props.modelModifiers)
</script>
- 子组件内就可以根据这个
v-model
修饰符的有无做出相应的判断啦
把上面的子组件改造一下:
<template>
<div>
<h2>子组件内容</h2>
<el-input v-model="_data"></el-input>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps({
modelValue: Number,
modelModifiers: {
type: Object,
default: () => ({}),
},
})
const emit = defineEmits(['update:modelValue'])
const _data = computed({
get() {
return props.modelValue
},
set(value) {
// 有flag修饰符的返回数据,没flag修饰符的返回空
if (props.modelModifiers.flag) {
emit('update:modelValue', +value) // + 运算符 把字符串转为数字
} else {
emit('update:modelValue', '快把flag修饰符加上')
}
},
})
</script>
那么既有参数,又有修饰符的,子组件的defineProps
改怎么接收呢?(用的很少,了解下就行,用到了就查文档)
侦听器
数据来源
-
可以侦听的一共就以下几种:
- 单个
ref
声明的数据:直接写ref,不需要写.value - 计算属性:直接写计算属性,不需要写.value
- 经过计算出来的
getter
函数(只要里面的某一项发生改变了都会触发),需要() =>
返回,也需要.value
- 侦听reactive对象中的某个属性,那需要
() =>
返回
- 单个
5. 多个数据源组成的数组(只要里面的某一项发生改变了都会触发)
侦听普通ref
变量
-
侦听单个
ref
时,侦听普通变量,直接写ref
即可
const num = ref(100)
// 侦听ref(普通变量)
watch(num, (newValue) => {
console.log('num变化了', newValue)
})
侦听对象
-
侦听单个
ref
时,侦听对象变量,直接写ref
是侦听不到的,需要写deep
配置
const obj = ref({
name: 'zs',
age: 26,
})
// 侦听ref(对象)
watch(obj, (newValue) => {
console.log(newValue)
}, { deep: true })
-
侦听单个
reactive
时,直接写reactive
是可以侦听到的
const obj = reactive({
name: 'zs',
age: 26,
})
// 侦听reactive对象
watch(obj, (newValue) => {
console.log(newValue)
})
侦听对象中的某个属性
-
侦听单个
ref
时,侦听对象中的某个属性,直接写是侦听不到的,需要用一个返回该属性的 getter 函数
const obj = ref({
name: 'zs',
age: 26,
})
// 侦听ref(对象)中的某个属性 直接写是不行的
// watch(obj.value.name, (newValue) => {
// console.log(newValue)
// })
// 需要返回一个getter函数
watch(
() => obj.value.name,
(newValue) => {
console.log(newValue)
}
)
-
侦听单个
reactive
对象的某个属性时,直接写是侦听不到的,也是需要返回该属性的 getter 函数
const obj = reactive({
name: 'zs',
age: 26,
})
// 侦听reactive对象中的某个属性 直接写是不行的
// watch(obj.name, (newValue) => {
// console.log(newValue)
// })
// 需要返回一个getter函数
watch(
() => obj.name,
(newValue) => {
console.log(newValue)
}
)
侦听计算属性
-
侦听计算属性,直接写即可
const obj = reactive({
name: 'zs',
age: 26,
})
const name = computed(() => obj.name)
// 侦听计算属性,直接写即可
watch(name, (newValue) => {
console.log(newValue)
})
侦听getter计算函数
-
需要返回计算后的getter函数
const firstName = ref('l')
const lastName = ref('s')
// 侦听计算属性,直接写即可
watch(
() => firstName.value + lastName.value,
(newValue) => {
console.log(newValue)
}
)
侦听多个数据
-
第一个参数直接传入数组,数组中可以写多个依赖
const firstName = ref('l')
const lastName = ref('s')
// 侦听计算属性,直接写即可
watch([firstName, lastName], (newValue) => {
console.log(newValue)
})
侦听器的第三个参数
watch
的第三个参数,可以写一些配置项:deep
(深度侦听),immediate
(立即侦听,通常用于页面刚一出来,就需要执行的情况),flush: post
(在侦听器回调中能访问被 Vue
更新之后的 DOM。功能类似于nexttick
),once(侦听一次)
<template>
<el-button @click="obj.name = 'ww'">改变name{{ obj.name }}</el-button>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const obj = ref({
name: 'zs',
})
watch(obj, (newVal) => {
console.log(newVal)
},
{ deep: true } // 深度侦听
)
</script>
侦听一次
每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true
选项。
watch(
source,
(newValue, oldValue) => {
// 当 `source` 变化时,仅触发一次
},
{ once: true }
)
侦听器回调函数的执行时机
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
默认情况下,侦听器回调会在①父组件更新 (如果有) 之后、②所属组件的 DOM 更新之前被调用。
-
也就是父组件
onUpdated
生命周期之后(如果存在父组件),自己所属组件onMounted
生命周期之前 -
父组件
onUpdated
(如果存在父组件) -----> 侦听器执行 -----> 所属组件onMounted
-
其实就是会在当前组件
DOM
挂载到浏览器之前(也就是onMounted
之前)执行侦听器的回调函数
<template>
<div ref="divRef">
<h2>父组件</h2>
<el-button @click="firstName = 'w'">改变obj的name: {{ firstName }}</el-button>
<el-button @click="lastName = 'w'">改变obj的age: {{ lastName }}</el-button>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const divRef = ref<HTMLDivElement>()
const firstName = ref('l')
const lastName = ref('s')
// 应该会先执行侦听器,然后执行onMounted
watch([firstName, lastName], () => {
console.log(divRef.value)
debugger
})
</script>
可以用上面的代码进行下测试:可以看出,在侦听器回调函数中,访问的还是当前组件之前的DOM
状态
这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。
那么如果我们想访问DOM
呢?应该怎么做呢
-
在侦听器回调函数中用
nextTick
函数包裹
<template>
<div ref="divRef">
<h2>父组件</h2>
<el-button @click="firstName = 'w'"
>改变obj的name: {{ firstName }}</el-button
>
<el-button @click="lastName = 'w'">改变obj的age: {{ lastName }}</el-button>
</div>
</template>
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue'
const divRef = ref<HTMLDivElement>()
const firstName = ref('l')
const lastName = ref('s')
// 应该会先执行侦听器,然后执行onMounted
watch([firstName, lastName], () => {
nextTick(() => {
console.log(divRef.value)
debugger
})
})
</script>
测试结果:
2. 添加flush: 'post'
配置
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
3. 后置刷新的 watchEffect()
有个更方便的别名 watchPostEffect()
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
侦听异步数据
Ref
v-for 中的模板引用
- 注意:它并不能保证顺序
ref的值可以是一个函数
<template>
<el-button @click="num++">点击num+1: {{ num }}</el-button>
<el-input :ref="getIntRef"></el-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const ipt = ref<HTMLElement>()
const num = ref<number>(100)
const getIntRef = (el: HTMLElement) => {
console.log(el) // 得到的是元素引用, num值只要改变,这里就会打印
el.focus() // 会获焦
return ipt.value
}
</script>
- 只要组件更新(updated)都会触发
ref
绑定的函数
ref用在组件上
- 用在组件上,则可以直接获取组件实例
父组件:
<template>
<Son ref="instance"></Son>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import Son from './About.vue'
const instance = ref()
onMounted(() => {
console.log(instance.value)
})
</script>
- 获取组件实例后,则就可以使用子组件的方法和变量了。但是这里需要注意,只能使用子组件使用
defineExpose
向外导出的方法和变量
子组件:
<template>
<div>子组件内容...</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = ref(100)
const fn = () => {
return '123'
}
// 向外导出,外部才能够使用
defineExpose({
data,
fn,
})
</script>
- 此时父组件内的打印结果就为
只会出现子组件中导出的方法和变量
组件部分
子组件接收属性
vue3
中可以直接使用defineProps
进行父组件传递给子组件属性的接收,内置就有,不需要导入。每次父组件更新后,所有的子组件中的 props 都会被更新到最新值props
是单向流的,即数据只能自顶向下。下面的不能修改上面的,只能下面的通知上面的进行数据修改
父组件:
<template>
<Son data="100"></Son>
</template>
<script setup lang="ts">
import Son from './About.vue'
</script>
子组件:
<template>
<div>子组件内容...{{ data }}</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{data?:string}>({
data: String,
})
</script>
如果不声明defineProps
接收,则都会被$attrs
进行接收(起个兜底的作用)
在模板中可以直接使用$attrs
进行传递过来的属性的调用
<template>
<div>子组件内容...{{ $attrs.data }}</div>
</template>
如果想在script
标签内使用,则需要useAttrs
这个函数
<template>
<div>子组件内容...{{ $attrs.data }}</div>
</template>
<script setup lang="ts">
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log(attrs)
</script>
useAttrs
这个方法总是反映为最新的穿透 attribute
,但它并不是响应式的。侦听器是侦听不到useAttrs这个方法
接收数据的变化
父组件:
<template>
<About :data="data"></About>
<el-button @click="data = '内容改变'">改变data</el-button>
</template>
<script setup lang="ts">
import About from './About.vue'
import { ref } from 'vue'
const data = ref('内容')
</script>
子组件:
<template>
<div class="son" style="font-size: 20px">
<h2>子组件内容</h2>
</div>
</template>
<script setup lang="ts">
import { useAttrs, watch } from 'vue'
const attrs = useAttrs()
watch(
attrs,
(newVal) => {
console.log(newVal) // 加了deep也没用,依然侦听不到对象种数据的变化
},
{ deep: true }
)
// 如果只侦听对象中的某个属性,那可以侦听到
watch(
() => attrs.data,
(newVal) => {
console.log('data', newVal)
}
)
</script>
如果一定要侦听attrs
对象中属性数据的变化,可以使用onUpdated
生命周期
对上面的子组件进行改造
<template>
<div class="son" style="font-size: 20px">
<h2>子组件内容</h2>
</div>
</template>
<script setup lang="ts">
import { onUpdated, useAttrs } from 'vue'
const attrs = useAttrs()
onUpdated(() => {
console.log(attrs)
})
</script>
1. 侦听props中的一个数据,可以侦听到变化
watch(
() => props.num,
(newVal) => {
console.log(newVal)
}
)
2. 侦听整个props,也可以侦听到变化
watch(
props,
(newVal) => {
console.log(newVal)
},
{ deep: true }
)
3. 如果一个变量把props中的值作为初始值,去侦听这个变量。props变化了,则侦听器也侦听不到(因为组件已经渲染过了,接收到的值一直是渲染时传递过来的props的值)。子组件中可以使用侦听器侦听整个props重新赋值,或者在onUpdated生命周期中重新赋值
父组件:
<template>
<el-button @click="addArrData">给arr添加值</el-button>
<About :arr="arr"></About>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import About from './About.vue'
const arr: any = ref([])
const addArrData = () => {
arr.value = [
{ id: 0, name: 'zs' },
{ id: 1, name: 'ls' },
]
}
</script>
子组件:
<template>
<div>
<h2>子组件内容</h2>
{{ data }}
</div>
</template>
<script setup lang="ts">
import { onUpdated, ref, watch } from 'vue'
let props = defineProps({
arr: {
type: Array,
default: () => [],
},
})
const data = ref(props.arr)
onUpdated(() => {
data.value = props.arr
})
watch(
props,
(newProps) => {
data.value = newProps.arr
},
{ deep: true }
)
</script>
props是只读的
props
是只读的。子组件如果想要修改传递过来的props
值,则需要进行重新声明,把props
传递过来的值作为初始值或者计算属性进行处理
特别需要注意的一点:就是子组件对父组件传递过来的数组或者对象进行修改,会影响父组件数据的。因为是引用传递,也就是说传递过来的值是堆内存地址
父组件:
<template>
{{ list }}
<hr />
<About :list="list"></About>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import About from './About.vue'
const list = ref([
{ id: 0, name: 'zs' },
{ id: 1, name: 'ls' },
{ id: 2, name: 'ww' },
])
</script>
子组件:
<template>
<div>
<h2>子组件内容</h2>
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
<el-button @click="list[0].name = '张三'">改变zs的名字</el-button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps({
list: Array,
})
const list = ref(props.list)
</script>
结果截图:有时候父子组件耦合度比较高的话,其实用这个也挺好的(省事了,不用子组件再往父组件抛事件了)
props检验
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个
// 工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
- 除
Boolean
外的(布尔的会给个false
的默认值)未传递的可选prop
将会有一个默认值undefined
常见类型:
String
Number
Boolean
Array
Object
Date
Function
Symbol
Error
类型也可以是自定义的类或构造函数,Vue
将会通过 instanceof
来检查类型是否匹配(用的很少),例如下面的类
class Person {
constructor(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
}
可以将其作为一个 prop 的类型:
defineProps({
author: Person
})
Vue 会通过 instanceof Person
来校验 author
prop 的值是否是 Person
类的一个实例。
可以为空的类型
如果该类型是必传的但可为空,你可以用一个包含 null
的数组语法:
defineProps({
id: {
type: [String, null],
required: true
}
})
如果 type
仅为 null
而非使用数组语法,它将允许任何类型。
Boolean的类型转换
如果单独只有一个布尔类型没什么特别注意的地方
defineProps({
disabled: Boolean
})
组件可以被这样使用:
<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />
<!-- 等同于传入 :disabled="false" -->
<MyComponent />
如果有多种类型,当同时允许 String
和 Boolean
时,有一种边缘情况——只有当 Boolean
出现在 String
之前时,Boolean
转换规则才适用:
// disabled 将被转换为 true
defineProps({
disabled: [Boolean, Number]
})
// disabled 将被转换为 true
defineProps({
disabled: [Boolean, String]
})
// disabled 将被转换为 true
defineProps({
disabled: [Number, Boolean]
})
// disabled 将被解析为空字符串 (disabled="")
defineProps({
disabled: [String, Boolean]
})
看着有点抽象,我举个例子就好了,只有着一种边缘情况会这样
父组件:只有传给子组件单个值才行(像这样只写一个flag)
<template>
<div>
<h2>父组件</h2>
<table>
<Son flag></Son>
</table>
</div>
</template>
<script setup lang="ts">
import Son from './components/Son.vue'
</script>
子组件:
<template>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
</template>
<script setup lang="ts">
const props = defineProps({
// 这种flag会为true
// flag: [Boolean, String],
// 只有这种flag会为 ''
flag: [String, Boolean],
})
console.log(props) // {flag: ''}
</script>
子组件抛出事件
vue3
中可以直接使用defineEmits
进行自定义事件的声明,然后在需要的地方进行往父组件进行事件的抛出。内置就有,不需要导入
子组件:
<template>
<el-button @click="sendFather">向父组件发送值</el-button>
</template>
<script setup lang="ts">
const emits = defineEmits(['sendFather'])
const sendFather = () => {
emits('sendFather', 100) // 向父组件发送了一个100的值
}
</script>
父组件:
<template>
<Son @sendFather="getData"></Son>
</template>
<script setup lang="ts">
import Son from './About.vue'
const getData = (num: number) => {
console.log('接收到了子组件发来的值:', num)
}
</script>
当然也可以直接在模板中进行事件的抛出,需要使用$emit
子组件:
<template>
<el-button @click="$emit('sendFather', 100)">向父组件发送值</el-button>
</template>
<script setup lang="ts"></script>
父组件不变
defineEmits
还支持对象语法
- 主要就是对事件进行下校验,返回
true
则校验通过,返回false
则校验不通过
向父组件发送事件对象event
或者这样
动态组件
- 动态组件属性
is
的值可以是组件名,还可以是标签名
组件名:
<template>
<component :is="Son">插槽内容</component>
</template>
<script setup lang="ts">
import Son from './About.vue'
</script>
标签名:
<template>
<component is="div">div中的内容</component>
</template>
<script setup lang="ts"></script>
- 上面说的被注册的组件名,指的是注册的组件(全局注册的或者局部注册的)
main.ts
import About from './views/About.vue'
const app = createApp(App)
// 注册一个全局组件About
app.component('About', About)
Test.vue
注意:这里is
属性不需要动态绑定,直接指定组件名即可
<template>
<component is="About">插槽内容</component>
</template>
<script setup lang="ts"></script>
多学一招:注册全局组件的另一种方法(利用插件的方法)
找个地方创建一个ts
文件
index.ts
// 1. 导入组件
import About from './About.vue'
// 2. 进行全局组件的注册
const component = {
install: (app, options) => {
// 插件代码
app.component('About', About)
}, //'About'这就是后面可以使用的组件的名字,install是默认的一个方法
}
// 导出该组件
export default component
然后再main.ts
中引入文件并使用app.use()
进行全局注册
main.ts
// 引入
import component from './views/index'
const app = createApp(App)
// 全局注册
app.use(component)
componentIs的使用
1. 第一种方式:直接is属性绑定一个变量,变量就是要展示组件
<template>
<main>
<h2>home</h2>
<el-button @click="toggleCom">切换组件</el-button>
<keep-alive>
<component :is="com"></component>
</keep-alive>
</main>
</template>
<script lang="ts" setup>
import MusicView from './MusicView.vue';
import AboutView from './AboutView.vue';
import { shallowRef } from 'vue';
// 这里一定要用shallowRef变为浅层响应式,不能用ref(会抛出一个警告,应该是ref响应式成本太大了)
const com = shallowRef(MusicView);
const toggleCom = () => {
com.value = com.value === MusicView ? AboutView : MusicView;
}
</script>
2. is属性的值通过三元判断展示的组件
<template>
<main>
<h2>home</h2>
<el-button @click="toggleCom">切换组件</el-button>
<keep-alive>
<component :is="show ? AboutView : MusicView"></component>
</keep-alive>
</main>
</template>
<script lang="ts" setup>
import MusicView from './MusicView.vue';
import AboutView from './AboutView.vue';
import { ref } from 'vue';
const show = ref(true);
const toggleCom = () => {
show.value = !show.value;
}
</script>
穿透
- “透传
attribute
”指的是传递给一个组件,却没有被该组件声明为props
或emits
的attribute
或者v-on
事件监听器。最常见的例子就是class
、style
和id
。
分好两种情况
1. 组件内只有一个根标签
透传的 attribute 会自动被添加到根元素上。并且style和class会进行合并
父组件:
<template>
<About class="box" style="color: red"></About>
</template>
<script setup lang="ts">
import About from './About.vue'
</script>
子组件:
<template>
<div class="son" style="font-size: 20px">
<h2>子组件内容</h2>
</div>
</template>
<script setup lang="ts"></script>
则子组件div
标签上会接收所有穿透过来的属性。并且class
和style
会进行合并
事件也会被子组件的跟标签继承
父组件:
<template>
<About class="box" style="color: red" @click="click"></About>
</template>
<script setup lang="ts">
import About from './About.vue'
const click = () => {
console.log('父组件打印')
}
</script>
子组件:
<template>
<div class="son" style="font-size: 20px" @click="click">
<h2>子组件内容</h2>
</div>
</template>
<script setup lang="ts">
const click = () => {
console.log('子组件打印')
}
</script>
这时点击子组件,会打印两次
- 子组件的跟标签会继承父组件传递过来的事件,并且也可以写自己的事件。会优先触发子组件的,然后触发父组件传递的(类似于冒泡吧,也可能是把事件处理逻辑合并了)
深层组件穿透
2.组件内有多个跟标签
报错信息:
翻译后:
额外的非道具属性(类、样式、liData)已传递给组件,但由于组件呈现片段或文本根节点,因此无法自动继承。
封装组件时,可能会遇到这种问题
禁用穿透
-
有的时候,我们可能不希望父组件传递过来的属性应用在跟标签上,想应用在跟标签内的标签上,则就可以禁用穿透。然后就可以使用
$attrs
在需要的地方进行使用 -
这个
$attrs
对象包含了除组件所声明的props
和emits
之外的所有其他attribute
,例如class
,style
,v-on
监听器等等
禁用穿透
<script setup>
defineOptions({ inheritAttrs: false })
// ...setup 逻辑
</script>
举例说明:
父组件:
<template>
<About class="box" style="color: red" :liData="liData"></About>
</template>
<script setup lang="ts">
import About from './About.vue'
import { ref } from 'vue'
const liData = ref('给li标签的内容')
</script>
子组件:
<template>
<div class="son" style="font-size: 20px">
<h2>子组件内容</h2>
<ul>
<li>{{ $attrs.liData }}</li>
</ul>
</div>
</template>
<script setup lang="ts">
// defineOptions({
// inheritAttrs: false,
// })
</script>
展示效果:
会发现,不加穿透禁用,都会继承到跟标签上
加了以后的效果(把上面子组件内的内容注释解开)
就会发现,父组件穿透过来的属性都没有加到跟标签上了。而是全被$attrs
接收了。就可以通过$attrs
随便在子组件任意标签上使用穿透的属性
对子组件进行下改造
<template>
<div class="son" style="font-size: 20px">
<h2 :class="$attrs.class" :style="$attrs.style">子组件内容</h2>
<ul>
<li>{{ $attrs.liData }}</li>
</ul>
</div>
</template>
<script setup lang="ts">
defineOptions({
inheritAttrs: false,
})
</script>
此时的页面效果:
需要注意的地方:
扩展
- 可以使用穿透的特性,来进行父子组件的双向数据绑定(很鸡肋,看看就行。别用这种,用
v-model
更好)
父组件:
<template>
{{ num }}
<About :num="num" @click="click"></About>
</template>
<script setup lang="ts">
import About from './About.vue'
import { ref } from 'vue'
const num = ref(100)
const click = (Num: number) => {
num.value = Num
}
</script>
子组件:
<template>
<div>
<h2>子组件内容 -- {{ n }}</h2>
<el-button @click="$attrs.onClick(++n)">
点击改变父组件传来的num值
</el-button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 我需要按钮点击的时候才触发父组件传来的点击事件,所以就需要禁用穿透
defineOptions({ inheritAttrs: false })
const props = defineProps({ num: Number })
const n = ref(props.num)
</script>
子组件也可以换成下面的方式,会更灵活一点
<template>
<div>
<h2>子组件内容 -- {{ n }}</h2>
<el-button @click="click"> 点击改变父组件传来的num值 </el-button>
</div>
</template>
<script setup lang="ts">
import { ref, useAttrs } from 'vue'
// 我需要按钮点击的时候才触发父组件传来的点击事件,所以就需要禁用穿透
defineOptions({ inheritAttrs: false })
const props = defineProps({ num: Number })
const attrs = useAttrs()
const n = ref(props.num)
const click = () => {
attrs.onClick(++n.value)
}
</script>
插槽
插槽一共分为3
种:默认插槽,具名插槽和作用域插槽
插槽传递的是模板内容(也就是html
,css
,js
组合起来的一段结构)
写在组件标签内的内容,就是插槽内容
父组件:
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
子组件:
<button class="fancy-btn">
<slot>插槽默认内容,如果没有传入插槽内容,则就会显示默认内容</slot> <!-- 插槽出口 -->
</button>
最终子组件渲染出来的结果
<button class="fancy-btn">Click me!</button>
插槽分为插槽内容(父组件)和插槽出口(子组件),它们是一一对应的(用name做区分)
插槽的作用域
- 插槽内容可以直接使用父组件中的数据(因为它就在父组件中)
- 而插槽出口可以访问子组件中的数据(因为它在子组件中)
- 但是插槽内容有时候需要子组件的数据(也就是父组件需要使用子组件中的数据),所以就需要借助作用域插槽
默认插槽
- 就是
name
为default
的插槽,即不写名字的插槽(没有提供name
的<slot>
出口会隐式地命名为“default”) - 有时候只需要一个插槽的时候,就可以使用默认插槽
父组件:
<template>
<About> 默认插槽的内容 </About> <!-- 插槽内容 -->
</template>
<script setup lang="ts">
import About from './About.vue'
</script>
子组件:
<template>
<div>
<h2>子组件内容</h2>
<slot>插槽默认内容</slot> <!-- 插槽出口 -->
</div>
</template>
<script setup lang="ts"></script>
具名插槽
- 顾名思义,就是有具体名字(不能是
default
)的插槽。也就是name名字你需要指定 - 插槽内容和插槽出口的
name
一样的就是一对。适合需要多个插槽的情况
v-slot
可以简写为#
号<template v-slot:title>
。其意思就是“将这部分模板片段传入子组件的 title 插槽中”。<template #:content>
。其意思就是“将这部分模板片段传入子组件的content
插槽中”。
条件插槽
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div v-if="$slots.default" class="card-content">
<slot />
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
动态插槽名
作用域插槽
- 在某些场景下插槽内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。则就需要作用域插槽了,也就是在<slot>标签上绑定数据向父组件传递
- 总结起来就是:作用域插槽可以使用父组件中的数据和子组件中的数据
默认作用域插槽
- 就是
name
为default
的作用域插槽
父组件:
<template>
<About>
<template v-slot="record"> <!-- 这里也可以使用结构 -->
{{ record }}
</template>
</About>
</template>
<script setup lang="ts">
import About from './About.vue'
</script>
子组件:
<template>
<div>
<h2>子组件内容</h2>
<slot :list="list">插槽默认内容</slot>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const list = ref([
{ id: 0, name: 'zs' },
{ id: 1, name: 'ls' },
])
</script>
具名作用域插槽
- 就是
name
不为default
的作用域插槽。就比上面的多了一个name
父组件:
<template>
<About>
<template v-slot:item="record"> <!-- 这里也可以使用结构 -->
{{ record }}
</template>
</About>
</template>
<script setup lang="ts">
import About from './About.vue'
</script>
子组件:
<template>
<div>
<h2>子组件内容</h2>
<slot name="item" :list="list">插槽默认内容</slot>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const list = ref([
{ id: 0, name: 'zs' },
{ id: 1, name: 'ls' },
])
</script>
注意:
- 父组件插槽内容中,
v-slot
只能用在template
标签和组件标签上 - 插槽可以重复多次使用
依赖注入
- 多用于深层组件传值用
- 分为提供者
provide
(祖先组件,负责提供数据)和消费者inject
(子孙组件,负责消费数据)。提供的值最好是响应式的
提供者
- 要为组件后代提供数据,需要使用到
provide
函数 - 注入名可以是一个字符串或者Symbol值(Symbol值必须是同一个文件里导出来的,这样才能保证provide和inject使用的是同一个Symbol值)
- 一个组件可以多次调用
provide()
,使用不同的注入名,注入不同的依赖值
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
提供应用级别的数据:需要写到main.ts
中,这样整个应用都能够进行使用(用的很少,通常写插件时提供数据用它。或者一些数据,整个应用都频繁的使用到,也可以使用应用层provide
提供整个应用数据)
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
消费者
- 要注入上层组件提供的数据,需使用
inject
函数:
<script setup>
import { inject } from 'vue'
const message = inject('message', "默认值") // 没有找到的话,则会显示默认值
</script>
注意:⭐
- 如果提供的值是一个
ref
,注入进来的会是该ref
对象,而不会自动解包为其内部的值。这使得注入方组件能够通过ref
对象保持了和供给方的响应性链接
注入默认值
案例
祖先组件:
<template>
<About></About>
<el-button @click="num++">改变num值</el-button>
</template>
<script setup lang="ts">
import About from './About.vue'
import { provide, ref } from 'vue'
const num = ref(100)
provide('num', num) // 最好传入一个ref对象,这样能保持响应式
</script>
子孙组件:
<template>
<div>
<h2>子组件内容 -- {{ num }}</h2>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue'
const num = inject('num', 0)
</script>
修改数据应尽量控制在提供方
- 当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护
祖先组件:
<template>
{{ num }}
<hr />
<About></About>
</template>
<script setup lang="ts">
import About from './About.vue'
import { provide, ref } from 'vue'
const num = ref(100)
// 让num加10的方法
const addTen = () => {
num.value += 10
}
provide('num', {
num,
addTen,
})
</script>
子孙组件:
<template>
<div>
<h2>子组件内容 -- {{ num }}</h2>
<el-button @click="addTen">num+10</el-button>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue'
const num = inject('num', {})
const addTen = () => {
num.addTen()
}
</script>
最后,如果你想确保提供的数据不能被注入方的组件更改,你可以使用 readonly() 来包装提供的值。
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>
使用 Symbol 作注入名
异步组件(了解)
-
就是异步加载的组件(
defineAsyncComponent
),它只会在需要访问的时候才会进行组件的加载,接收一个返回 Promise 的加载函数 -
Es
模块动态导入(import(...)
)也会返回一个Promise
,大多数情况下会将它和defineAsyncComponent
搭配使用
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
- 最后得到的
AsyncComp
是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的props
和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载
与普通组件一样,异步组件可以使用 app.component()
全局注册
app.component('MyComponent', defineAsyncComponent(() =>
import('./components/MyComponent.vue')
))
也可以直接在父组件中直接定义它们:
<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
</script>
<template>
<AdminPage />
</template>
异步组件对象形式
- 异步组件不仅支持里面传入一个返回 Promise 的加载函数,而且还支持传入一个对象
const AsyncComp = defineAsyncComponent({
// 异步加载的组件
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms。因为加载时显示的组件和加载后显示的组件之间直接切换的话,会有闪白出现
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
Vue内置组件(了解)
Teleport
可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去
// 组件内
...
<Teleport to="body">
// 模板内容...
</Teleport>
....
其实就是可以把当前组件中的一部分模板内容插入到任意结构中。to属性指定插入到那,值可以是一个css选择器或者一个dom对象。前提是要插入的部分已经渲染完了,不然插入不进去。当然数据用的还是本组件的
父组件:
<template>
<button @click="open = true">Open Modal</button>
<About></About>
<Teleport to=".about" v-if="open">
<div>点击按钮出现的内容</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import About from './About.vue'
const open = ref(false)
</script>
子组件:
<template>
<div class="about">
<h2>子组件内容</h2>
</div>
</template>
<script setup lang="ts"></script>
也可以禁用此组件
多个 Teleport 共享目标
使用此组件进行结构的插入,也可以用h函数和render函数搭配实现插入结构到指定组件或者dom的效果,插入的前提也是要插入的部分必须渲染过了
感兴趣的可以看下详细文档:
h函数:渲染函数 API | Vue.js (vuejs.org)
render函数:渲染选项 | Vue.js (vuejs.org)
简单理解就是h函数创建虚拟dom,而render函数渲染虚拟dom到真实的结构中去(变为真实dom)
h函数可以有3个函数
参数1: 可以是一个字符串(指定html标签),也可以是一个组件名
参数2:对象,如果参数1指定的html标签,里面放属性或事件(id,css,style,innerHTML,onClick...)。如果参数1指定的组件,里面放props属性
参数3:如果参数1指定的html标签,里面放标签的内容。如果参数1指定的组件,里面放插槽
render函数可以有2个函数
参数1: 虚拟dom
参数2:插入到哪,可以是一个dom对象
案例1:创建一个html标签
<template>
<button @click="click">点击进行结构的插入</button>
</template>
<script setup lang="ts">
import { h, render } from 'vue'
const click = () => {
// h函数会创建一个虚拟dom
const d = h(
'div',
{
id: 'div-contioner',
style: { height: '200px', width: '200px', background: 'pink' },
},
'div中的内容'
)
// 然后render函数进行渲染虚拟dom为真实dom
render(d, document.body) // 插入到body中
}
</script>
案例1:创建一个组件
<template>
<button @click="click">点击进行结构的插入</button>
</template>
<script setup lang="ts">
import { h, render, ref } from 'vue'
import About from './About.vue'
const count = ref(99)
const click = () => {
// h函数会创建一个虚拟dom
const d = h(
About, // 就把About当成子组件就行
{
// 就把这部分当成往子组件传值就行
num: 100,
count,
onCustomEvent: () => {
// 也可以传事件
},
},
{
default: () => 'default slot', // 默认插槽
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')],
}
)
// 然后render函数进行渲染虚拟dom为真实dom
render(d, document.body)
}
</script>
子组件
<template>
<div class="about">
<h2>子组件内容</h2>
{{ num }}
{{ count }}
{{ onCustomEvent }}
<!-- 默认插槽 -->
<slot></slot>
<!-- foo插槽 -->
<slot name="foo"></slot>
<!-- bar插槽 -->
<slot name="bar"></slot>
</div>
</template>
<script setup lang="ts">
defineProps({
num: Number,
count: Number,
onCustomEvent: Function,
})
</script>
Suspense
可以管理多个异步依赖的组件,统一进行loading,显示
什么是异步依赖的组件
- 异步组件:使用defineAsyncComponent包裹的组件
- 带有异步 setup() 钩子的组件。这也包含了使用 <script setup> 时有顶层 await 表达式的组件。
带有异步setup()钩子的组件
export default {
async setup() {
const res = await fetch(...)
const posts = await res.json()
return {
posts
}
}
}
<script setup> 时有顶层 await 表达式的组件
<script setup>
const res = await fetch(...)
const posts = await res.json()
</script>
<template>
{{ posts }}
</template>
加载中状态
- <Suspense> 组件有两个插槽:#default 和 #fallback。两个插槽都只允许一个直接子节点
<Suspense>
<!-- 具有深层异步依赖的组件 -->
<Dashboard />
<!-- 在 #fallback 插槽中显示 “正在加载中” -->
<template #fallback>
Loading...
</template>
</Suspense>
案例
AboutView.vue
<template>
<div>
<h3>about</h3>
</div>
</template>
MusicView.vue
<template>
<div>
<h3>Music</h3>
</div>
</template>
<script setup lang="ts">
await fetch('xxx')
</script>
HomeView.vue
<template>
<main>
<h2>home</h2>
<Suspense @fallback="fallback" @pending="pending" @resolve="resolve">
<!-- 具有深层异步依赖的组件 -->
<div>
<AboutView></AboutView>
<MusicView></MusicView>
</div>
<!-- 在 #fallback 插槽中显示 “正在加载中” -->
<template #fallback>
Loading...
</template>
</Suspense>
</main>
</template>
<script lang="ts" setup>
import MusicView from './MusicView.vue';
import { onErrorCaptured, defineAsyncComponent } from 'vue';
// 注册一个异步组件
const AboutView = defineAsyncComponent(() => {
return import('./AboutView.vue');
})
// 像使用其他一般组件一样使用 `AboutView`
// 会首先触发
const pending = () => {
console.log("进入挂起状态时触发")
}
// 其次触发
const fallback = () => {
console.log("fallback 插槽的内容显示时触发")
}
// 最后加载完成后触发
const resolve = () => {
console.log("在 default 插槽完成获取新内容时触发")
}
// 捕获后代错误
onErrorCaptured(err => {
console.log(err);
return false // 返回 false 来阻止错误继续向上传递
})
</script>
KeepAlive
在多个组件间动态切换时缓存被移除的组件实例。一般都是结合动态组件(上面有讲)使用
被KeepAlive标签包裹的组件,卸载的时候不会直接卸载,而是进行了缓存。有对应的生命周期
直接上案例
AboutView.vue
<template>
<div>
<h3>about</h3>
</div>
</template>
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue';
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
console.log('About组件激活');
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
console.log('About组件失活');
})
</script>
MusicView.vue
<template>
<div>
<h3>Music</h3>
</div>
</template>
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue';
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
console.log('Music组件激活');
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
console.log('Music组件失活');
})
</script>
HomeView.vue
<template>
<main>
<h2>home</h2>
<el-button @click="toggleCom">切换组件</el-button>
<keep-alive>
<component :is="show ? AboutView : MusicView"></component>
</keep-alive>
</main>
</template>
<script lang="ts" setup>
import MusicView from './MusicView.vue';
import AboutView from './AboutView.vue';
import { ref } from 'vue';
const show = ref(true);
const toggleCom = () => {
show.value = !show.value;
}
</script>
可以通过 include
和 exclude
属性 来限制里面缓存的组件
<!-- 以英文逗号分隔的字符串 -->
<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>
还可以通过max属性来限制最大缓存组件数,超出则最久没有被访问的缓存实例将被销毁
<KeepAlive :max="10">
<component :is="activeComponent" />
</KeepAlive>
自定义指令(了解)
两种在 Vue 中重用代码的方式:组件和组合式函数(下面补充部分)。组件是主要的构建模块,而组合式函数则侧重于有状态的逻辑。另一方面,自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。也就是需要频繁操作底层dom的时候才考虑用自定义指令。很少用,了解即可
自定义指令分为局部自定义指令(在组件内声明,只能组件内使用)和全局自定义指令(挂载到app实例上,全局都能使用)
局部自定义指令:input元素自动获焦
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
全局自定义指令:(将一个自定义指令全局注册到应用层级)
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
注意:
-
在
<script setup>
中,任何以v
开头的驼峰式命名的变量都可以被用作一个自定义指令。在上面的例子中,vFocus
即可以在模板中以v-focus
的形式使用。
生命周期钩子
指令内都是类似于生命周期的钩子,也很好理解,因为需要操作dom,所以要在合适的时机对dom做出对应的操作才行
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
钩子中可用。
举例来说
<div v-example:foo.bar="baz">
这样的一个自定义指令,binding中的参数对应的也就是
{
arg: 'foo',
modifiers: { bar: true }, // 修饰符,有的就为true
value: /* `baz` 的值 */,
oldValue: /* 上一次更新时 `baz` 的值 */
}
和内置指令类似,自定义指令的参数也可以是动态的
<div v-example:[arg]="value"></div>
注意:除了 el
外,其他参数都是只读的,不要更改它们。若你需要在不同的钩子间共享信息,推荐通过元素的 dataset attribute (自定义属性)实现。(通过el参数,就能获取到自定义属性)
自定义指令用在组件上
会始终应用于组件的根节点
父组件
<MyComponent v-demo="test" />
子组件
<!-- MyComponent 的模板 -->
<div> <!-- v-demo 指令会被应用在此处 -->
<span>My component content</span>
</div>
当子组件含有多个根节点时,自定义指令会抛出警告
案例
写一个全局的自定义指令,使input自动获焦
main.ts
// 注册一个全局的自定义指令
app.directive('focus', {
mounted(el) {
el.focus()
}
})
使用
<input v-focus />
再写一个自定义指令,根据自定义指令的参数,实现元素的显示与隐藏(类似于v-if,v-show)
main.ts
// 注册一个全局的自定义指令
app.directive('isShow', {
mounted(el, binding) {
console.log(el); // 使用自定义指令的元素本身
console.log(binding.value); // 就是自定义指令传来的值
// 根据传来的值,来决定元素显示或者隐藏
el.style.display = binding.value ? 'block' : 'none'
}
})
使用
<el-button v-isShow="true">显示</el-button>
<el-button v-isShow="false">隐藏</el-button>
简化形式
对象字面量
插件(了解)
插件是什么
是一种能为 Vue 添加全局功能的工具代码
插件的形式
- 插件可以是一个拥有 install() 方法的对象
const myPlugin = {
install(app, options) {
// 配置此应用
// app是vue实例
// options是接收的参数:也就是 app.use() 使用插件时传入的参数
}
}
- 插件还可以是一个函数
const myPlugin = (app, options) => {
// 配置此应用
// app是vue实例
// options是接收的参数:也就是 app.use() 使用插件时传入的参数
}
插件常见使用场景
- 通过 app.component() 和 app.directive() 注册一到多个全局组件或全局自定义指令。
- 通过 app.provide() 使一个资源可被注入进整个应用。
- 向 app.config.globalProperties 中添加一些全局实例属性或方法
- 一个可能上述三种都包含了的功能库 (例如 vue-router)。
编写一个插件
- 定义插件
单独新建一个Plugin的文件夹,新建i18n.ts文件
export default {
install: (app: any, options: any) => {
console.log('配置:', options);
// 在这里编写插件代码
// 注入一个全局可用的 $translate() 方法
app.config.globalProperties.$translate = (key: string) => {
console.log('接收到的参数:', key)
// 获取 `options` 对象的深层属性
// 使用 `key` 作为索引
return '翻译后的结果'
}
}
}
- 注册插件
// 引入插件
import i18nPlugin from './Plugin/i18n'
const app = createApp(App)
// 注册插件
app.use(i18nPlugin, {
str: 'hello.world'
})
- 使用插件
<template>
<main>
<h1>{{ $translate('greetings.hello') }}</h1>
</main>
</template>
插件中的 Provide / Inject
在插件中,我们可以通过 provide
来为插件用户供给一些内容。举例来说,我们可以将插件接收到的 options
参数提供给整个应用,让任何组件都能使用这个翻译字典对象。
定义插件:
// plugins/i18n.js
export default {
install: (app, options) => {
app.provide('i18n', options)
}
}
注册插件:
import i18nPlugin from './plugins/i18n'
app.use(i18nPlugin, {
greetings: {
hello: 'Bonjour!'
}
})
使用插件:插件用户就可以在他们的组件中以 i18n
为 key 注入并访问插件的选项对象了。
<script setup>
import { inject } from 'vue'
const i18n = inject('i18n')
console.log(i18n.greetings.hello)
</script>
---------- 后续补充部分 ----------
shallowRef
-
ref() 的浅层作用形式。也就是只会对
.value
这一层保持响应式,深层的是没有响应式的
<template>
<div>
<p>{{ num }}</p>
<el-button @click="changeNum">改变Num</el-button>
<p>{{ person }}</p>
<el-button @click="changePerson">改变Person</el-button>
<el-button @click="changePerson2">改变Person2</el-button>
</div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
const num = shallowRef(100)
const person = shallowRef({
name: 'zs',
age: 26,
})
const changeNum = () => {
// shallowRef声明的变量,只会对.value这一层保持响应式!所以这里的num.value++,会触发视图更新
num.value = num.value++
}
const changePerson = () => {
// shallowRef声明的变量,只会对.value这一层保持响应式!
// 这里操作的是person.value.age,不是直接对person.value进行操作。所以不会触发视图更新
person.value.age = person.value.age++
}
const changePerson2 = () => {
// 这里直接对person.value进行操作,所以会触发视图更新
person.value = { name: 'ls', age: 26 }
}
</script>
reactive
reactive()
将使对象本身具有响应性:所以参数最好写对象类型,不然没有响应式!
import { reactive } from 'vue'
const state = reactive({ count: 0 })
值得注意的是,reactive()
返回的是一个原始对象的 Proxy,它和原始对象是不相等的:
const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
为保证访问代理的一致性,对同一个原始对象调用 reactive()
会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive()
会返回其本身:
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
局限性:不能替换整个对象。如果替换整个对象,响应性连接将丢失
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
注意点:对解构操作不友好。结构后会丢失响应式
const state = reactive({ count: 0 })
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)
shallowReactive
-
reactive() 的浅层作用形式。只有根级别的属性是响应式的,属性的值会被原样存储和暴露,也意味着值为
ref
的属性不会被自动解包了(这个下面ref
解包有讲)。
<template>
<div>
<p>{{ person }}</p>
<el-button @click="changePersonName">改变Person的name</el-button>
<el-button @click="changePersonAge">改变Person的age</el-button>
<el-button @click="changePersonCity">改变Person的city</el-button>
</div>
</template>
<script setup lang="ts">
import { shallowReactive } from 'vue'
const person = shallowReactive({
name: 'zs',
age: 26,
address: {
city: '河南',
},
})
const changePersonName = () => {
// 更改状态自身的属性是响应式的,也就是更改person的自身属性是响应式的,但是更改person的嵌套属性不是响应式的
person.name = 'ls'
}
const changePersonAge = () => {
// 更改状态自身的属性是响应式的,也就是更改person的自身属性是响应式的,但是更改person的嵌套属性不是响应式的
person.age = 27
}
const changePersonCity = () => {
// 更改person的嵌套属性不是响应式的。下层嵌套对象不会被转为响应式
person.address.city = '北京'
}
</script>
⭐ref解包细节
-
ref
声明的变量作为reactive
的参数:ref
声明的变量被访问或修改时自动解包
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。
-
数组和集合的注意事项
与 reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型 (如 Map
) 中的元素被访问时,它不会被解包:
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)
⭐组件上的v-model
v-model
可以在组件上使用以实现双向绑定。
从 Vue 3.4 开始,推荐的实现方式是使用 defineModel() 宏:
父组件:
<template>
<div>
<h2>父组件</h2>
<Son v-model="num"></Son>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Son from './components/Son.vue'
const num = ref(100)
</script>
子组件:
<template>
<div>Son{{ model }}</div>
<el-button @click="changeNum">改变父组件传来的值</el-button>
</template>
<script setup lang="ts">
const model = defineModel({ type: Number, default: 1 })
const changeNum = () => {
console.log(model.value++)
}
</script>
defineModel()
返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:
-
它的
.value
和父组件的v-model
的值同步; -
当它被子组件变更了,会触发父组件绑定的值一起更新。
这意味着你也可以用 v-model
把这个 ref 绑定到一个原生 input 元素上,在提供相同的 v-model
用法的同时轻松包装原生 input 元素:
<template>
<div>
<h2>父组件{{ model }}</h2>
<input type="text" v-model="model" />
</div>
</template>
<script setup lang="ts">
const model = defineModel({ type: String, default: '123' })
</script>
-
defineModel
中可以接收0
个,1
个,2
个参数
// 声明 "modelValue" prop,由父组件通过 v-model 使用
const model = defineModel()
// 或者:声明带选项的 "modelValue" prop
const model = defineModel({ type: String })
// 在被修改时,触发 "update:modelValue" 事件
model.value = "hello"
// 声明 "count" prop,由父组件通过 v-model:count 使用
const count = defineModel("count")
// 或者:声明带选项的 "count" prop
const count = defineModel("count", { type: Number, default: 0 })
function inc() {
// 在被修改时,触发 "update:count" 事件
count.value++
}
底层机制
defineModel
是一个便利宏。编译器将其展开为以下内容:
-
一个名为
modelValue
的 prop,本地 ref 的值与其同步; -
一个名为
update:modelValue
的事件,当本地 ref 的值发生变更时触发。
在 3.4 版本之前,需要这样写
父组件:
<template>
<div>
<h2>父组件</h2>
<Son v-model="num" v-model:count="count" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Son from './components/Son.vue'
const num = ref(100)
const count = ref(99)
</script>
子组件:
<template>
<div>Son{{ num }}</div>
<el-input v-model="num"></el-input>
<p>{{ count }}</p>
<el-input v-model="count"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue'
// 老版本写法
const props = defineProps({
modelValue: Number,
})
const emits = defineEmits(['update:modelValue'])
const num = computed({
get: () => {
return props.modelValue
},
set: (newVal) => {
emits('update:modelValue', newVal)
},
})
// 新版本写法
const count = defineModel('count', { type: Number })
</script>
或者子组件是这样的(本质就是往父组件抛出自定义事件,来改变传给父组件的值,因为props
接收的值总是最新的)
<template>
<div>Son{{ num }}</div>
<el-button @click="changeNum">改变num的值</el-button>
<p>{{ count }}</p>
<el-input v-model="count"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue'
// 老版本写法
const props = defineProps({
modelValue: Number,
})
const emits = defineEmits(['update:modelValue'])
const num = computed(() => props.modelValue)
const changeNum = () => {
emits('update:modelValue', num.value! + 1)
}
// 新版本写法
const count = defineModel('count', { type: Number })
</script>
修饰符和转换器
为了获取 v-model
指令使用的修饰符,我们可以像这样解构 defineModel()
的返回值:
-
第二个结构传来的值,就是
v-model
的修饰符,如果没有则就为空
const [modelValue, modelModifiers] = defineModel()
// 对应 v-model.trim
if (modelModifiers.trim) {
// ...
}
当存在修饰符时,我们可能需要在读取或将其同步回父组件时对其值进行转换。我们可以通过使用 get
和 set
转换器选项来实现这一点:
const [modelValue, modelModifiers] = defineModel({
// get() 省略了,因为这里不需要它
set(value) {
// 如果使用了 .trim 修饰符,则返回裁剪过后的值
if (modelModifiers.trim) {
return value.trim()
}
// 否则,原样返回
return value
}
})
带参数的 v-model
修饰符
展示如何在使用多个不同参数的 v-model
时使用修饰符:
父组件:
<UserName
v-model:first-name.capitalize="first"
v-model:last-name.uppercase="last"
/>
子组件:
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')
console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>
生命周期
钩子应当在组件初始化时被同步注册。例如,请不要这样做:
setTimeout(() => {
onMounted(() => {
// 异步注册时当前组件实例已丢失
// 这将不会正常工作
})
}, 100)
组合式函数
在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
-
说通俗点就是,可以根绝不同的状态变化而动态的输出结果
追踪鼠标的案例
这是直接在结构里面写的:
<template>Mouse position is at: {{ x }}, {{ y }}</template>
<script setup lang="ts">
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
文件夹,然后里面存放自定义hook
定义自定义函数:
import { ref, onMounted, onUnmounted } from 'vue'
export const useMouse = () => {
const x = ref(0)
const y = ref(0)
function update(event: MouseEvent) {
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 lang="ts">
import { useMouse } from './hook'
const { x, y } = useMouse()
</script>
异步状态示例
-
上面的追踪鼠标例子,演示的是没有传入参数的方式。这里演示下传入参数的,包括传入静态参数和动态响应式参数
-
在做异步数据请求时,常常需要处理不同的状态:加载中、加载成功和加载失败。
<template>
<div v-if="error">Oops! Error encountered: {{ error.message }}</div>
<div v-else-if="data">
Data loaded:
<pre>{{ data }}</pre>
</div>
<div v-else>Loading...</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const data = ref(null)
const error = ref(null)
fetch('...')
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
</script>
传入静态参数
-
请求还是比较常用的,就可以把上面的抽离成一个自定义函数
import { ref } from 'vue'
export function useFetch(url: string) {
const data = ref(null)
const error = ref(null)
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
return { data, error }
}
使用:
<script setup>
import { useFetch } from './hook'
const { data, error } = useFetch('...')
</script>
传入响应式参数
useFetch()
接收一个响应式 URL 字符串作为输入,只要参数的值变化了,就自动进行请求
-
参数可以接收一个
ref
,也可以接收一个getter
函数
// 1. ref
const url = ref('/initial-url')
const { data, error } = useFetch(url)
// 这将会重新触发 fetch
url.value = '/new-url'
// 2. 或者getter函数
// 当 props.id 改变时重新 fetch
const { data, error } = useFetch(() => `/posts/${props.id}`)
就可以用 watchEffect() 和 toValue() API 来重构我们现有的实现:
toValue()
是一个在 3.3 版本中新增的 API。它的设计目的是将 ref 或 getter 规范化为值。如果参数是 ref,它会返回 ref 的值;如果参数是函数,它会调用函数并返回其返回值。否则,它会原样返回参数。
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = () => {
// reset state before fetching..
data.value = null
error.value = null
// toValue方法不管接收的是ref,还是一个getter函数,都会最终转化为对应的返回值
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
watchEffect(() => {
fetchData()
})
return { data, error }
}
规范要求
- 组合式函数约定用驼峰命名法命名,并以“use”作为开头
- 最好处理一下输入参数是 ref 或 getter 而非原始值的情况。可以利用 toValue() 工具函数来实现:
import { toValue } from 'vue'
function useFeature(maybeRefOrGetter) {
// 如果 maybeRefOrGetter 是一个 ref 或 getter,
// 将返回它的规范化值。
// 否则原样返回。
const value = toValue(maybeRefOrGetter)
}
- 如果需要对传入的响应式参数进行监听。要么使用
watch()
显式地监视 ref 或 getter,要么在watchEffect()
中调用toValue()
。 - 组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:
// x 和 y 是两个 ref
const { x, y } = useMouse()
- 在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但在
onUnmounted()
时要清理副作用
举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在
onUnmounted()
中被移除 (就像我们在useMouse()
示例中看到的一样)。