Skip to content

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 TypeScript 声明 props 和自定义事件。
  • 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
  • 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。

基本语法

<script setup> 是组合式 API 的编译时语法糖,可简化代码结构。

typescript
<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

vue
<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格式,大驼峰。

vue
<script setup>
  import MyComponent from './MyComponent.vue'
</script>

<template>
  <MyComponent />
</template>

动态组件

<script setup> 中要使用动态组件的时候,应该使用动态的:is来绑定:

typescript
<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/> 引用它自己。也可以为自己添加别名

typescript
import { FooBar as FooBarChild } from './components'

命名空间组件

对于从一个单文件中导出多个组件的情况,可以使用<Foo.Bar>形式来引用嵌套组件,即可以使用带 . 的组件标签

vue
<script setup>
  import * as Form from './form-components'
</script>

<template>
  <Form.Input>
    <Form.Label>label</Form.Label>
  </Form.Input>
</template>

自定义指令

全局注册的自定义指令在 <script setup> 中可正常使用,本地自定义指令不需显式注册,但需要遵循vNameOfDirective命名规范

vue
<script setup>
  const vMyDirective = {
    beforeMount: (el) => {
      // 在元素上做些操作
    }
  }
</script>
<template>
  <h1 v-my-directive>This is a Heading</h1>
</template>

如果指令是从别处导入的,可以通过重命名来使其符合命名规范:

typescript
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>

defineProps() 和 defineEmits()

用于在 <script setup> 中声明 props 和自定义事件,以获得类型推导和IDE代码提示。它们可以自动的被导入

注意,define宏是只能在<script setup> 中使用的编译器宏

typescript
<script setup>
  // 在s-setup中,不需要import了,且
const props = defineProps({
  foo: String
})

const emit = defineEmits(['change', 'delete'])
// setup 代码
</script>

类型标注

泛型参数

  • defineProps 或 defineEmits 要么使用运行时声明,要么使用类型声明,两种声明方式不能同时使用
typescript
// 基于类型声明
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]
}>()

改进:

typescript
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 对象定义,

vue
<script setup>
  // 运行时声明
  const props = defineProps({
    title: String,
    likes: Number,
    isPublished: Boolean
  })
</script>

<template>
  <h1>{{ props.title }}</h1>
  ...
</template>
typescript
<script setup>
const emit = defineEmits({
  'update:title': (newTitle) => typeof newTitle === 'string',
  'increment': (payload) => true // 任何payload都视作有效
})
</script>

类型声明

类型声明则使用 TypeScript 的类型或接口定义。推荐!!!


对于使用 TypeScript 的项目,defineProps 和 defineEmits 可以利用类型声明来提供 prop 和 emit 的类型定义。这在编译时提供了更好的类型检查和编辑器的代码智能提示功能

vue
<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>
typescript
<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 来使用

vue
<!-- Child.vue -->
<script setup>
  const model = defineModel()

  function update() {
    model.value++
  }
</script>

<template>
  <div>parent bound v-model is: {{ model }}</div>
</template>
  • Parent.vue
vue
<!-- Parent.vue -->
<Child v-model="count" />

defineModel() 返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:

  • 它的 .value 和父组件的 v-model 的值同步;
  • 当它被子组件变更了,会触发父组件绑定的值一起更新。

示例

vue
<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>
vue
<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 的值发生变更时触发。
typescript
<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参数

vue
<!-- 父组件 -->
<MyComponent v-model:title="bookTitle" />
vue
<!-- 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之前写法

vue
<!-- 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()

typescript
<script setup>
  // 不需要写async
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>

async setup() 必须与 Suspense 内置组件组合使用