<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖
。
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
基本语法
<script setup>
是组合式 API 的编译时语法糖,可简化代码结构。
<script setup>
// start-end 里面的代码会被编译成组件 setup() 函数的内容
import { ref } from 'vue'
const count = ref(0)
// end
</script>
<script setup>
中的代码会在每次组件实例被创建的时候执行。
响应式状态
响应式状态需要明确使用响应式 API 来创建,在<script setup>
中使用ref、reactive、computed,watch
等需要手动导入,但是ref, reactive变量会自动暴露return
<script setup>
import { ref, reactive, computed, watch } from 'vue'
const count = ref(0)
watch(count, () => {
console.log("count改变了");
});
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
组件的使用
导入的组件可以直接在模板中作为自定义组件的标签名使用,而不需要在 components 选项内注册。 建议组件名使用PascalCase格式
,大驼峰。
<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
<MyComponent />
</template>
动态组件
在 <script setup>
中要使用动态组件的时候,应该使用动态的:is
来绑定:
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>
递归组件
一个单文件组件可以通过它的文件名被其自己所引用。例如:名为 FooBar.vue 的组件可以在其模板中用 <FooBar/>
引用它自己。也可以为自己添加别名
import { FooBar as FooBarChild } from './components'
命名空间组件
对于从一个单文件中导出多个组件的情况,可以使用<Foo.Bar>
形式来引用嵌套组件,即可以使用带 . 的组件标签
<script setup>
import * as Form from './form-components'
</script>
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
自定义指令
全局注册的自定义指令在 <script setup>
中可正常使用,本地自定义指令不需显式注册,但需要遵循vNameOfDirective
命名规范
<script setup>
const vMyDirective = {
beforeMount: (el) => {
// 在元素上做些操作
}
}
</script>
<template>
<h1 v-my-directive>This is a Heading</h1>
</template>
如果指令是从别处导入的,可以通过重命名来使其符合命名规范:
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
defineProps() 和 defineEmits()
用于在 <script setup>
中声明 props 和自定义事件
,以获得类型推导和IDE代码提示。它们可以自动的被导入
。
注意,define宏是只能在
<script setup>
中使用的编译器宏
<script setup>
// 在s-setup中,不需要import了,且
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
// setup 代码
</script>
类型标注
泛型参数
- defineProps 或 defineEmits 要么使用运行时声明,要么使用类型声明,两种声明方式不能同时使用
// 基于类型声明
const props = defineProps<{
foo: string
bar?: number
}>()
// emit
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
// 3.3+:另一种更简洁的语法
const emit = defineEmits<{
change: [id: number] // 具名元组语法
update: [value: string]
}>()
改进:
const props = defineProps<{
foo: string
bar?: number
}>()
// 改==> 可以将 props 的类型移入一个单独的接口中
interface Props {
foo: string
bar?: number
}
const props = defineProps<Props>()
// 改进后,也可以用于Props从另一个文件导入
<script setup lang="ts">
import type { Props } from './foo'
const props = defineProps<Props>()
</script>
声明方式
运行时声明使用对象语法定义
,而类型声明则使用类型语法
。类型声明通常在 TypeScript 环境中使用。
运行声明
可以看到运行时声明使用 JavaScript 对象定义,
<script setup>
// 运行时声明
const props = defineProps({
title: String,
likes: Number,
isPublished: Boolean
})
</script>
<template>
<h1>{{ props.title }}</h1>
...
</template>
<script setup>
const emit = defineEmits({
'update:title': (newTitle) => typeof newTitle === 'string',
'increment': (payload) => true // 任何payload都视作有效
})
</script>
类型声明
类型声明则使用 TypeScript 的类型或接口定义。推荐!!!
对于使用 TypeScript 的项目,defineProps 和 defineEmits 可以利用类型声明来提供 prop 和 emit 的类型定义。这在编译时提供了更好的类型检查和编辑器的代码智能提示功能
<script setup lang="ts">
// TypeScript interface 或 type
interface Props {
title: string;
likes: number;
isPublished: boolean;
}
// 使用泛型定义 props 类型
const props = defineProps<Props>()
</script>
<template>
<h1>{{ props.title }}</h1>
...
</template>
<script setup lang="ts">
// TypeScript 泛型定义 emits
const emit = defineEmits<{
(e: 'update:title', newTitle: string): void;
(e: 'increment', payload: number): void;
}>()
</script>
默认值
defineModel
从 Vue 3.4 开始,推荐使用 [defineModel()](https://cn.vuejs.org/api/sfc-script-setup.html#definemodel)
宏在组件上使用以实现双向绑定:
这个宏可以用来声明一个双向绑定 prop,通过父组件的 v-model 来使用
<!-- Child.vue -->
<script setup>
const model = defineModel()
function update() {
model.value++
}
</script>
<template>
<div>parent bound v-model is: {{ model }}</div>
</template>
- Parent.vue
<!-- Parent.vue -->
<Child v-model="count" />
defineModel() 返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:
- 它的 .value 和父组件的 v-model 的值同步;
- 当它被子组件变更了,会触发父组件绑定的值一起更新。
示例
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const msg = ref('Hello World!')
</script>
<template>
<h1>{{ msg }}</h1>
<Child v-model="msg" />
</template>
<script setup>
const test = defineModel()
</script>
<template>
<!-- 当我们修改了子组件的defineModel()返回值test-->
<!-- 会触发父组件v-model中的值msg同步 -->
<span>My input</span> <input v-model="test">
</template>
底层原理
defineModel 是一个便利宏。编译器将其展开为以下内容:
- 一个名为
modelValue
的 prop,本地 ref 的值与其同步; - 一个名为
update:modelValue
的事件,当本地 ref 的值发生变更时触发。
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
v-model参数
<!-- 父组件 -->
<MyComponent v-model:title="bookTitle" />
<!-- MyComponent.vue -->
<script setup>
const title = defineModel('title')
// 如果需要额外参数
const title = defineModel('title', { required: true })
</script>
<template>
<input type="text" v-model="title" />
</template>
3.4之前写法
<!-- MyComponent.vue -->
<script setup>
defineProps({
title: {
required: true
}
})
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
顶层await
<script setup>
中可以使用顶层 await。结果代码会被编译成 async setup()
<script setup>
// 不需要写async
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
async setup()
必须与 Suspense 内置组件组合使用