script setup 语法
问题
Vue 3 的 <script setup> 是什么?它有什么优势?
答案
<script setup> 是 Vue 3.2 引入的编译时语法糖,是在单文件组件(SFC)中使用 Composition API 的推荐方式。它让代码更简洁,并提供更好的性能和 TypeScript 支持。
基本对比
- 普通 setup()
- script setup
<script>
import { ref, computed } from 'vue';
import ChildComponent from './Child.vue';
export default {
components: {
ChildComponent
},
props: {
title: String
},
emits: ['update'],
setup(props, { emit }) {
const count = ref(0);
const double = computed(() => count.value * 2);
function increment() {
count.value++;
emit('update', count.value);
}
return {
count,
double,
increment
};
}
};
</script>
<script setup>
import { ref, computed } from 'vue';
import ChildComponent from './Child.vue';
// 1. 组件自动注册
// 2. 无需 return
const props = defineProps({
title: String
});
const emit = defineEmits(['update']);
const count = ref(0);
const double = computed(() => count.value * 2);
function increment() {
count.value++;
emit('update', count.value);
}
</script>
核心优势
| 优势 | 说明 |
|---|---|
| 更简洁 | 无需 return,顶层变量自动暴露给模板 |
| 组件自动注册 | import 的组件可直接在模板中使用 |
| 更好的性能 | 编译优化,减少运行时开销 |
| 更好的 IDE 支持 | 更准确的类型推断 |
| 更好的代码压缩 | 变量名可以被压缩 |
defineProps - 声明 Props
<script setup lang="ts">
// 方式 1:运行时声明
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
});
// 方式 2:类型声明(推荐)
interface Props {
title: string;
count?: number;
items?: string[];
}
const props = defineProps<Props>();
// 方式 3:带默认值的类型声明
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
});
// 使用 props
console.log(props.title);
</script>
注意
defineProps 和 defineEmits 是编译器宏,不需要导入,在编译时会被处理。
defineEmits - 声明事件
<script setup lang="ts">
// 方式 1:运行时声明
const emit = defineEmits(['update', 'delete']);
// 方式 2:类型声明(推荐)
const emit = defineEmits<{
update: [id: number, value: string];
delete: [id: number];
}>();
// 方式 3:对象语法(带验证)
const emit = defineEmits({
// 返回 true 表示验证通过
update: (id: number, value: string) => {
return id > 0;
}
});
// 使用
emit('update', 1, 'new value');
</script>
defineExpose - 暴露组件接口
<script setup> 组件默认是封闭的,需要显式暴露给父组件:
<!-- Child.vue -->
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
function reset() {
count.value = 0;
}
// 显式暴露
defineExpose({
count,
reset
});
</script>
<!-- Parent.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import Child from './Child.vue';
const childRef = ref<InstanceType<typeof Child>>();
function handleClick() {
console.log(childRef.value?.count);
childRef.value?.reset();
}
</script>
<template>
<Child ref="childRef" />
</template>
defineOptions - 组件选项
<script setup lang="ts">
// 定义组件名、继承属性等
defineOptions({
name: 'MyComponent',
inheritAttrs: false
});
</script>
defineModel - 双向绑定(Vue 3.4+)
<!-- 子组件 -->
<script setup lang="ts">
// Vue 3.4+ 新增
const modelValue = defineModel<string>();
// 带默认值
const count = defineModel('count', { default: 0 });
// 使用
modelValue.value = 'new value'; // 自动触发 update:modelValue
</script>
<template>
<input v-model="modelValue" />
</template>
<!-- 父组件 -->
<template>
<Child v-model="value" v-model:count="count" />
</template>
defineSlots - 类型化插槽(Vue 3.3+)
<script setup lang="ts">
const slots = defineSlots<{
default(props: { item: string }): any;
header(props: { title: string }): any;
}>();
</script>
<template>
<div>
<header>
<slot name="header" title="Hello" />
</header>
<main>
<slot :item="currentItem" />
</main>
</div>
</template>
使用顶层 await
<script setup> 支持顶层 await,组件会自动变成异步组件:
<script setup>
// 顶层 await
const data = await fetch('/api/data').then(r => r.json());
</script>
<template>
<div>{{ data }}</div>
</template>
<!-- 父组件需要配合 Suspense -->
<template>
<Suspense>
<AsyncComponent />
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
与普通 script 配合使用
<!-- 可以同时使用两个 script -->
<script>
// 普通 script:用于声明只需执行一次的逻辑
export const staticValue = 'constant';
// 或者导出 Options API 兼容的选项
export default {
name: 'MyComponent',
inheritAttrs: false
};
</script>
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
编译优化
<script setup> 在编译时会进行优化:
<!-- 源代码 -->
<script setup>
import { ref } from 'vue';
const msg = ref('Hello');
</script>
<template>
<div>{{ msg }}</div>
</template>
// 编译后(简化)
import { ref, toDisplayString } from 'vue';
const __sfc__ = {
setup() {
const msg = ref('Hello');
return { msg };
}
};
function render(_ctx) {
// 直接访问 setup 返回值,无需通过 _ctx
return h('div', toDisplayString(_ctx.msg));
}
常见面试问题
Q1: script setup 和普通 setup 有什么区别?
答案:
| 特性 | script setup | 普通 setup() |
|---|---|---|
| 语法 | 顶层代码即 setup | 需要在 setup() 函数内 |
| return | 自动暴露 | 需要手动 return |
| 组件注册 | 自动注册 | 需要 components 选项 |
| props/emits | 使用编译器宏 | 通过参数获取 |
| 性能 | 更好(编译优化) | 一般 |
| TypeScript | 更好的类型推断 | 需要 defineComponent |
Q2: defineProps 是什么?为什么不需要 import?
答案:
defineProps 是编译器宏(Compiler Macro),不是运行时函数:
// 编译前
const props = defineProps<{ msg: string }>();
// 编译后
const props = __props; // 直接引用组件的 props
特点:
- 无需导入:编译时被处理,不存在于运行时
- 类型安全:支持 TypeScript 泛型语法
- 编译优化:生成更高效的代码
类似的编译器宏还有:defineEmits、defineExpose、defineOptions、defineSlots、defineModel、withDefaults
Q3: 如何在 script setup 中定义组件名?
答案:
<!-- 方式 1:使用 defineOptions(推荐) -->
<script setup>
defineOptions({
name: 'MyComponent'
});
</script>
<!-- 方式 2:使用额外的 script 块 -->
<script>
export default {
name: 'MyComponent'
};
</script>
<script setup>
// ...
</script>
<!-- 方式 3:通过文件名推断 -->
<!-- 文件名 MyComponent.vue 会自动推断为 MyComponent -->
Q4: script setup 组件如何被父组件访问?
答案:
<script setup> 组件默认不暴露任何内容,需要使用 defineExpose:
<!-- Child.vue -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
const privateData = ref('private'); // 不暴露
function publicMethod() {
console.log('public');
}
// 只暴露需要的内容
defineExpose({
count,
publicMethod
});
</script>
<!-- Parent.vue -->
<script setup>
import { ref, onMounted } from 'vue';
import Child from './Child.vue';
const childRef = ref();
onMounted(() => {
console.log(childRef.value.count); // ✅ 0
console.log(childRef.value.privateData); // ❌ undefined
childRef.value.publicMethod(); // ✅
});
</script>
<template>
<Child ref="childRef" />
</template>
Q5: 如何在 script setup 中使用 TypeScript?
答案:
<script setup lang="ts">
import { ref, computed } from 'vue';
// 1. ref 类型推断
const count = ref(0); // Ref<number>
const message = ref<string | null>(null); // 显式指定
// 2. Props 类型
interface Props {
title: string;
list?: number[];
}
const props = withDefaults(defineProps<Props>(), {
list: () => []
});
// 3. Emits 类型
const emit = defineEmits<{
change: [id: number];
update: [value: string];
}>();
// 4. ref 模板引用类型
import type { ComponentPublicInstance } from 'vue';
const inputRef = ref<HTMLInputElement | null>(null);
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null);
// 5. computed 类型
const double = computed<number>(() => count.value * 2);
</script>