什么是渐近式框架?
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">{{ message }}</div>
<script>
const { createApp } = Vue
createApp({
data() {
return {
message: 'Hello Vue!'
}
}
}).mount('#app')
</script>
npm init vue@latest
声明式和命令式是两种编程规范。Vue是声明式的,Jquery那样直接操作DOM是命令式
换言之:
Model , View ,ViewModel ,
M:里面保存的是每个页面中单独的数据,比如页面中有一个table表格,而这个table表格需要发送axios请求,服务器会返回一个数据,这个数据可能是对象,也可能是数组,这个返回的数据就是M
**V:每个页面中的html页面结构 **
VM:**它是一个调度者(中间层),分割了M和V ,**我们的数据需要渲染到页面中,不能直接渲染,需要调用VM,也就是M层的数据需要渲染到V的页面中去,必须经过VM层做处理
前端页面中使用MVVM的思想,主要是为了让我们开发更加方便,因为MVVM提供了数据的双向绑定,注意:数据的双向绑定时VM提供的
选项式 API ,我们可以用包含多个选项的对象来描述组件的逻辑,例如 data 、methods 和 mounted。选项所定义的属性都会暴露在函数内部的this上,它会指向当前的组件实例。
用于接收父组件传递过来的属性
详细说明:
props 会暴露到 this上
示例:
export default {
props: ['foo'],
created() {
// props 会暴露到 `this` 上
console.log(this.foo)
}
}
除了使用字符串数组来声明 prop 外,还可以使用对象的形式:
export default {
props: {
title: String,
likes: Number
}
}
用于声明组件初始化响应式状态的函数。
详细说明:
data函数应当返回一个普通JavaScript对象,Vue会将它转换为响应式对象。实例创建后。可以通过this. d a t a 访问该响应式对象。组件实例也代理了该数据对象上的所有属性,因此 t h i s . a 等价于 t h i s . data 访问该响应式对象。组件实例也代理了该数据对象上的所有属性,因此this.a 等价于 this. data访问该响应式对象。组件实例也代理了该数据对象上的所有属性,因此this.a等价于this.data.a
示例:
Vue.createApp({
data() {
return {
count: 100
}
},
// 生命周期 函数
created() {
console.log(this.count) // 100
console.log(this.$data.count) // 100
},
}).mount("#app")
注意:
在Vue2.x的时候,data是一个对象
在Vue3.x的时候,必须传入一个函数
用于声明要混入到组件实例中的方法
详细说明:
声明的方法可以直接通过组件实例访问,或者在模板语法表达式中使用。所有的方法都会将它们的this 上下文自动绑定为组件实例。
示例:
export default {
data() {
return { a: 1 }
},
methods: {
plus() {
this.a++
}
},
created() {
this.plus()
console.log(this.a) // => 2
}
}
注意:在声明方法时避免使用箭头函数,因为它们不能通过this访问组件实例
用于声明要在组件实例上暴露的计算属性。
详细说明:
该选项接收一个对象,其中键是计算属性的名称,值是一个计算属性getter,或一个具有get和set方法的对象(用于声明可写的计算属性)。
注意:如果你为计算属性使用了箭头函数,则 this 不会指向该组件实例,可以通过该函数的第一个参数来访问。
export default {
computed: {
aDouble: (vm) => vm.a * 2
}
}
export default {
data() {
return { a: 1 }
},
computed: {
// 只读
aDouble() {
return this.a * 2
},
// 可写
aPlus: {
get() {
return this.a + 1
},
set(v) {
this.a = v - 1
}
}
},
created() {
console.log(this.aDouble) // => 2
console.log(this.aPlus) // => 2
this.aPlus = 3
console.log(this.a) // => 2
console.log(this.aDouble) // => 4
}
}
computed:
methods:
用于声明在数据更改时调用的侦听回调。
<script>
export default {
name: "Home",
data() {
return {
person: {
name: ""
}
};
},
watch: {
"person.name": {
handler(newVal, oldVal) {
console.log(`新的值: ${newVal}`);
console.log(`旧的值: ${oldVal}`);
},
}
},
};
</script>
<template>
<div>
输入: <input type="text" v-model="text">
</div>
</template>
<script>
export default {
name: "Home",
data() {
return {
text: "",
};
},
watch: {
text: {
handler(newVal, oldVal) {
},
immediate: true
}
},
};
</script>
<script>
export default {
name: "Home",
data() {
return {
person: {
name: ""
}
};
},
watch: {
person: {
handler(newVal, oldVal) {
},
deep: true
}
},
};
</script>
最基本的数据绑定形式是文本插值,Mustache里面可以写入表达式
示例:
<!-- 基本使用 -->
<h2>{{count}}</h2>
<!-- 表达式 -->
<h2>{{ count * 2 }}</h2>
data() {
return {
count: 100
}
}
仅渲染元素和组件一次,之后跳过渲染,可以用来优化更新时的性能。
示例:
<h2 v-once>{{message}}</h2>
更新元素的文本内容
绑定值类型:string
详细说明:
v-text通过设置元素的textContent属性来工作,因此它将覆盖元素中所有现有的内容。如果你需要更新 textContent 的部分,可以使用 Mustache 语法 两者是一样的
示例:
<h2 v-text="message"></h2>
<!-- 等同于 -->
<h2>{{message}}</h2>
用法和v-text是一样的,区别在于v-html可以解析html元素
跳过该元素及其所有子元素的编译。也就是一次都不编译。
详细说明:
元素内具有 v-pre ,所有 Vue 模板语法都会被保留并按原样渲染。
示例:
<span v-pre>{{ this will not be compiled }}</span>
用于隐藏尚未完成编译的DOM模板
详细说明:
该指令只在没有构建步骤的环境下需要使用。
当使用直接在DOM中书写的模板时,可以会出现一种叫做 “未编译模板闪现” 的情况:用户可能先看到的是还没有编译完成的双大括号标签,直到挂载的组件将它们替换为实际渲染的内容。v-cloak 会保留在所绑定的元素上,直到相关组件实例被挂载后才移出。配合像 [v-cloak] { display: none } 这样的 CSS 规则。
示例:
[v-cloak] {
display: none;
}
{{ message }}
动态的绑定一个或多个 attribute,也可以是组件的prop。
缩写: : 或者 . (当使用.prop修饰符)
绑定值类型:any (带参数) | object (不带参数)
修饰符:
- .camel - 将短横线命名的attribute转变为驼峰式命名
- .prop - 强制绑定为 DOM property
- .attr - 强制绑定为 DOM attribute
_用途: _
当用于绑定 class 或 style attribute, v-bind 支持额外的值类型如数组或对象。
<!-- 绑定 img 的 src -->
<img v-bind:src="imgurl">
<img :src="imgurl">
<!-- 绑定 a 的 href -->
<a v-bind:href="baidu">百度一下</a>
<!-- 绑定 attribute -->
<img v-bind:src="imageSrc" />
<!-- 动态 attribute 名 -->
<button v-bind:[key]="value"></button>
<!-- 缩写 -->
<img :src="imageSrc" />
<!-- 缩写形式的动态 attribute 名 -->
<button :[key]="value"></button>
<!-- 内联字符串拼接 -->
<img :src="'/path/to/images/' + fileName" />
<!-- class 绑定 isRed是一个布尔值 -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]"></div>
<!-- style 绑定 -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>
<!-- 绑定对象形式的 attribute -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>
<!-- prop 绑定。“prop” 必须在子组件中已声明。 -->
<MyComponent :prop="someThing" />
<!-- 传递子父组件共有的 prop -->
<MyComponent v-bind="$props" />
<!-- XLink -->
<svg><a :xlink:special="foo"></a></svg>
给元素绑定事件监听器
缩写: @
绑定值类型:event
修饰符:
- .stop - 调用 event.stopPropagation()
- .prevent - 调用 event.preventDefault()
- .capture - 在捕获模式添加事件监听器
- .self - 只有事件从元素本身发出才触发处理函数
- .{keyAlias} - 只有某些按键下触发处理函数
- once - 最多触发一次处理函数
- .left - 只在鼠标左键事件触发处理函数
- .right - 只在鼠标右键事件触发处理函数
- middle - 只在鼠标中键事件触发处理函数。
- passive - 通过 { passive: true } 附加一个DOM事件。
_用途: _
绑定事件
示例:
<!-- 方法处理函数 -->
<button v-on:click="doThis"></button>
<!-- 动态事件 -->
<button v-on:[event]="doThis"></button>
<!-- 内联声明 -->
<button v-on:click="doThat('hello', $event)"></button>
<!-- 缩写 -->
<button @click="doThis"></button>
<!-- 使用缩写的动态事件 -->
<button @[event]="doThis"></button>
<!-- 停止传播 -->
<button @click.stop="doThis"></button>
<!-- 阻止默认事件 -->
<button @click.prevent="doThis"></button>
<!-- 不带表达式地阻止默认事件 -->
<form @submit.prevent></form>
<!-- 链式调用修饰符 -->
<button @click.stop.prevent="doThis"></button>
<!-- 按键用于 keyAlias 修饰符-->
<input @keyup.enter="onEnter" />
<!-- 点击事件将最多触发一次 -->
<button v-on:click.once="doThis"></button>
<!-- 对象语法 -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
“真正的”条件渲染,动态的向DOM树内添加或删除 DOM元素。v-if适合条件很少改变的情况。初始渲染:v-if初始值为false时,组件不会渲染,生命周期钩子不会执行;
v-show无论初始状态是什么,组件都会渲染,v-show 不支持 元素,也不支持 v-else。
基于原始数据多次渲染元素或模板
<div v-for="item in items">
{{ item.text }}
</div>
v-for中key属性的值应唯一,起到身份认证的作用,避免v-for"就地更新"策略导致的问题。官方说法:v-for中key 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。 如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。 而使用 key 时,它会基于 key的变化重新排列元素顺序,并且会移除 key 不存在的元素。
key属性可以用来提升v-for渲染的效率!,vue不会去改变原有的元素和数据,而是创建新的元素然后把新的数据渲染进去
VNode的全称是Virtual Node,也就是 虚拟节点
VNode的本质是一个JavaScript的对象
一个个VNode组成的就是虚拟DOM
在表单输入元素或组件上创建双向绑定。
仅限:
-
-
修饰符:
- .lazy -监听 change 事件而不是input
- .number - 将输入的合法符串转为数字
- .trim - 移出输入内容两端空格
<div id="app">
<!-- 手动实现 也是v-model的原理 -->
<input :value="message" type="text" @input="inpChange">
<!-- v-model 实现 -->
<input type="text" v-model="message">
<h2>{{message}}</h2>
</div>
<script>
const app = Vue.createApp({
data() {
return {
message: "100"
}
},
methods: {
inpChange(event) {
this.message = event.target.value
}
},
})
// 挂载
app.mount("#app")
</script>
<input
:value="text"
@input="event => text = event.target.value">
等价于:
<input v-model="text">
<span>Multiline message is:span>
<p style="white-space: pre-line;">{{ message }}p>
<textarea v-model="message" placeholder="add multiple lines">textarea>
单一的复选框
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
多个复选框
export default {
data() {
return {
checkedNames: []
}
}
}
<div>Checked names: {{ checkedNames }}</div>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<div>Picked: {{ picked }}</div>
<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>
<div>Selected: {{ selected }}</div>
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
在任何一个vue实例里面都可以使用
app.component("item-vue", itemVue)
第一个参数为组件的名称,
第二个参数是你要注册的对象(组件)
只能在声明的vue实例里面起作用
const app = Vue.createApp({
data() {
return {
}
},
// 注册局部组件
components:{
"home-vue":{
template:"你好",
data() {
return {
}
},
}
}
})
什么是Vue脚手架?
- 脚手架其实是建筑工程中的一个概念,帮助我们搭建项目的工具称之为脚手架;
Vue脚手架 Vue CLI
- CLI是 Command-Line Interface ,翻译为命令行界面
- 我们可以通过CLI选择项目的配置和创建出我们的项目
- Vue CLI内置了 webpack相关的配置,我们不需要从零来配置
安装cli
npm install @vue/cli -g
如果是比较旧的版本,可以通过下面命令升级
npm update @vue/cli -g
创建项目
vue create 项目名称
官方文档:https://vitejs.dev/
安装项目
npm create vite@latest
父组件传递给子组件: 通过props属性接收
接收方式有两种:数组和对象
export default {
props: ['foo'],
created() {
// props 会暴露到 `this` 上
console.log(this.foo)
}
}
export default {
props: {
title: String,
likes: Number
}
}
官网文档:https://cn.vuejs.org/guide/components/events.html#component-events
子组件传递给父组件:通过$emit触发事件 $emit 方法触发自定义事件
export default {
methods: {
submit() {
this.$emit('someEvent')
}
}
}
父组件可以通过 v-on (缩写为 @) 来监听事件:
<MyComponent @some-event="callback" />
$emit(‘事件名称’, 你要传递的参数)
<button @click="$emit('increaseBy', 1)">
Increase by 1
</button>
父组件接收
<MyButton @increase-by="increaseCount" />
methods: {
increaseCount(n) {
this.count += n
}
}
组件可以显式地通过 emits 选项来声明它要触发的事件
export default {
// 可以清楚的看到有哪些事件发出去
emits:["changeCounter"],
methods:{
addCount(i) {
this.$emit("changeCounter",i)
}
}
}
父组件当传递属性给子组件时,子组件没有通过props接收,就会默认传递给子组件的根标签上
父组件传递的属性不想继承在子组件的根节点上可以使用 inheritAttrs 设置为flase
当有多个根节点时,请使用v-bind绑定$attrs 不然会报警告
插槽就是 子组件 中的提供给 父组件 使用的一个占位符,用 表示,父组件可以在这个占位符中填充任何模板代码
插槽显不显示是由父盒子决定的,插槽在哪个位置上显示是由子组件决定的。
<FancyButton>
Click me!
FancyButton>
<button class="fancy-btn">
<slot>slot>
button>
带 name 的插槽被称为具名插槽 (named slots)。没有提供 name 的 出口会隐式地命名为“default”
子组件,就是给slot标签添加上 name:名字(什么都可以)
父组件,在父组件调用子组件的地方里面添加并加上对应的slot名字,就可以使对应的内容显示在对应的子组件具名插槽中
作用域插槽就是通过子组件中 传递值给父组件,父组件接收使用
<slot name="button" :title="'你好'">leftslot>
<template #button="slotProps">
<button>{{ slotProps.title }}button>
template>
官方地址:https://cn.vuejs.org/guide/components/provide-inject.html
provide写成一个对象
export default {
provide: {
message:100
}
}
provide写成一个函数
export default {
data() {
return {
message: 'hello!'
}
},
provide() {
// 使用函数的形式,可以访问到 `this`
return {
message: this.message
}
}
}
什么是生命周期?
每一个vue实例从创建到销毁的过程,就是这个vue实例的生命周期。
ref用于获取元素和组件实例
用法:给元素或组件绑定一个ref的attribute属性
获取:
<script>
export default {
mounted() {
console.log(this.$refs.input)
}
}
</script>
<template>
<input ref="input" />
</template>
<template>
<div class="app">
<Home ref="homeCom"></Home>
</div>
</template>
<script>
import Home from './components/Home.vue';
export default {
components:{
Home
},
mounted() {
console.log(this.$refs.homeCom)
}
}
</script>
<template>
<div class="app">
<Home ref="homeCom"></Home>
</div>
</template>
<script>
import Home from './components/Home.vue';
export default {
components:{
Home
},
mounted() {
this.$refs.homeCom.homeClick() // 我是home
}
}
</script>
<template>
<div class="home">
<p>home</p>
</div>
</template>
<script>
export default {
methods:{
homeClick() {
console.log("我是home")
}
}
}
</script>
<template>
<div class="app">
<Home ref="homeCom"></Home>
</div>
</template>
<script>
import Home from './components/Home.vue';
export default {
components:{
Home
},
mounted() {
console.log(this.$refs.homeCom.$el)
}
}
</script>
$parent用来获取当前组件的父组件
$root 用来获取当前组件
一个用于渲染动态组件或元素的“元组件”。
**、 **和 具有类似组件的特性,也是模板语法的一部分。但它们并非真正的组件,同时在模板编译期间会被编译掉。因此,它们通常在模板中用小写字母书写。
详细信息:
要渲染的实际组件由 is 决定。
案例:
<script>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export default {
components: { Foo, Bar },
data() {
return {
view: 'Foo'
}
}
}
</script>
<template>
<component :is="view" />
</template>
地址:https://cn.vuejs.org/guide/built-ins/keep-alive.html#keepalive
默认会缓存内部的所有组件实例,但我们可以通过 include 和 exclude prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组.
它会根据组件的 name 选项进行匹配,所以组件如果想要条件性地被 KeepAlive 缓存,就必须显式声明一个 name 选项。
<!-- 以英文逗号分隔的字符串 -->
<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>
activated 和 deactivated 生命周期
因为keep-alive包裹的组件不会被销毁,所以无法监听 created 和 unmounted 生命周期
异步组件是对网站的一种优化,其意思就是懒加载,当我们需要用到那个组件的时候才去打包挂载到DOM树上。
import { defineAsyncComponent } from 'vue'
// 会为 Foo.vue 及其依赖创建单独的一个块
// 它只会按需加载
//(即该异步组件在页面中被渲染时)
const Foo = defineAsyncComponent(() => import('./Foo.vue'))
v-model 可以在组件上使用以实现双向绑定。是一种组件通信。
v-bind:modelValue 这个等于 父组件传递子组件的属性
@update:modelValue 接收子组件$emit的事件
<CustomInput
v-bind:modelValue="searchText" //这个等于 父组件传递子组件
@update:modelValue="searchText = newValue" //接收子组件$emit的事件
/>
<script>
import CustomInput from './CustomInput.vue';
export default {
components:{
CustomInput
},
data() {
return {
searchText:100
}
}
}
</script>
export default {
// 接收父组件传递过来的值 modelValue是固定的
props:{
modelValue:{
type:Number,
default:0
}
},
methods:{
change() {
// 向父组件发出一个事件 update:modelValu 是固定的
this.$emit("update:modelValue",9999)
}
}
}
不再推荐
在 Vue 2 中,mixins 是创建可重用组件逻辑的主要方式。尽管在 Vue 3 中保留了 mixins 支持,但对于组件间的逻辑复用,Composition API** 是现在更推荐的方式。**
Composition API是Vue3 提供了一种新的方式(组合函数式)来组织和编写组件的逻辑代码。
注意
setup() 组件选项的用法。对于结合单文件组件使用的组合式 API,推荐通过
setup()钩子是组合式API的入口 ,使用场景:
总结:尽量使用
在 setup() 函数中返回的对象会暴露给模板和组件实例。其他的选项也可以通过组件实例来获取 setup() 暴露的属性
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
// 返回值会暴露给模板和其他的选项式 API 钩子
return {
count
}
},
mounted() {
console.log(this.count) // 0
}
}
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。
setup 函数的第一个参数是组件的 props。和标准的组件一致,一个 setup 函数的 props 是响应式的,并且会在传入新的 props 时同步更新。
export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
}
}
请注意如果你解构了 props 对象,解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx 的形式来使用其中的 props。
如果你确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs() 和 toRef() 这两个工具函数:
import { toRefs, toRef } from 'vue'
export default {
setup(props) {
// 将 `props` 转为一个其中全是 ref 的对象,然后解构
const { title } = toRefs(props)
// `title` 是一个追踪着 `props.title` 的 ref
console.log(title.value)
// 或者,将 `props` 的单个属性转为一个 ref
const title = toRef(props, 'title')
}
}
传入 setup 函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup 中可能会用到的值:
export default {
setup(props, context) {
// 透传 Attributes(非响应式的对象,等价于 $attrs)
console.log(context.attrs)
// 插槽(非响应式的对象,等价于 $slots)
console.log(context.slots)
// 触发事件(函数,等价于 $emit)
console.log(context.emit)
// 暴露公共属性(函数)
console.log(context.expose)
}
}
接收一个复杂数据类型,返回一个对象的响应式代理。
创建一个响应式对象:
const obj = reactive({ count: 0 })
obj.count++
ref的解包:
const count = ref(1)
const obj = reactive({ count })
// ref 会被解包
console.log(obj.count === count.value) // true
// 会更新 `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2
// 也会更新 `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3
接受一个值,返回一个响应式的,可以修改的ref对象,这个对象只有一个.vaule属性。
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
ref在模板中自动解包:
{{count}}
{{count + 1}}
接收一个对象(无论是响应式还是普通的) 或是一个 ref ,返回一个原值的只读代理
单项数据流:
子组件拿到数据后只能使用,不能修改,如果要修改,通过事件传递给父组件来修改。
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 用来做响应性追踪
console.log(copy.count)
})
// 更改源属性会触发其依赖的侦听器
original.count++
// 更改该只读副本将会失败,并会得到一个警告
copy.count++ // warning!
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
<template>
<div class="app">
<!-- ref里面是字符串 -->
<h3 ref="h3El">哈哈哈哈</h3>
</div>
</template>
<script>
import { onMounted, ref } from 'vue';
export default {
setup() {
const h3El = ref()
onMounted(() => {
console.log(h3El.value)
})
return {
h3El
}
},
}
</script>
在 少了两个生命周期:beforeCreate 和 created
如果需要这两个生命周期,直接在setup中写你的业务逻辑
官方文档:https://cn.vuejs.org/api/composition-api-dependency-injection.html#provide
provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。
- defineProps 和 defineEmits 都是只能在