Vue | Vue3中<script setup>用法详解

你是不是还在为 Vue 组件的那些繁琐语法头疼?每次写个组件都要 export default、methods、data 来回折腾,感觉代码总是啰里啰嗦的?

所幸的是,Vue3的**<script setup>语法糖彻底简化了组件开发,让代码更简洁高效。它通过自动暴露顶层绑定、省略setup()函数定义、增强TypeScript支持等特性,使开发者能够用更少的代码实现更多功能。本文详细介绍了其核心优势、基础用法、组件通信方式,并提供了高级技巧和实战案例。从响应式数据定义到组合式函数封装,再到组件暴露方法,<script setup>**都能显著提升开发体验。另外,本篇文章还解答了常见问题并给出最佳实践建议,是Vue3开发者必学的现代语法规范。


一、什么是 <script setup>

简单来说,**<script setup>**是 Vue3 引入的一种编译时语法糖,它能让单文件组件的脚本部分变得更加简洁明了。

以前我们写个组件得这样:

javascript 复制代码
<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

现在用**<script setup>** 就简单多了:

javascript 复制代码
<script setup>
    import { ref } from 'vue'
    const count = ref(0)
    const increment = () => {
    count.value++
    }
</script>

看出来了吧?代码一下子清爽了很多!不再需要那些模板化的结构,直接写逻辑就行。


二、为什么要用 <script setup>

你可能要问,我已经习惯原来的写法了,为什么要换呢?这里给你几个无法拒绝的理由:

代码量大幅减少,不用再写那些重复的样板代码。组件间的数据传递和事件处理变得更加直观。更好的 TypeScript 支持,类型推断更加准确。编译时优化,性能更优秀。

最重要的是,写起来真的很快乐!你再也不用在 methods、data、computed 之间来回切换了。


三、基础用法速成

让我们从最简单的开始,一步步掌握 **<script setup>**的核心用法。

定义响应式数据,在 <script setup> 里,我们直接用 ref 和 reactive:

javascript 复制代码
<script setup>
import { ref, reactive } from 'vue'

// 基本类型用 ref
const name = ref('张三')
const age = ref(25)

// 对象类型可以用 reactive
const userInfo = reactive({
  job: '前端开发',
  salary: 20000
})

// 修改数据也很简单
const updateInfo = () => {
  name.value = '李四'  // ref 需要通过 .value 访问
  userInfo.salary = 25000 // reactive 直接修改属性
}
</script>

定义方法就更简单了,直接写函数就行:

javascript 复制代码
<script setup>
  const sayHello = () => {
    console.log('你好,Vue3!')
  }

  const calculate = (a, b) => {
    return a + b
  }
</script>

计算属性也用起来:

javascript 复制代码
<script setup>
import { ref, computed } from 'vue'

const price = ref(100)
const quantity = ref(2)

// 计算总价
const total = computed(() => {
  return price.value * quantity.value
})

// 复杂的计算属性
const discountTotal = computed(() => {
  const totalVal = price.value * quantity.value
  return totalVal > 200 ? totalVal * 0.9 : totalVal
})
</script>

四、组件通信变得超简单

在**<script setup>**里,组件间的通信也变得特别直观。

定义 props 可以用 defineProps:

javascript 复制代码
<script setup>
// 基础用法
defineProps(['title', 'content'])

// 带类型检查的用法
defineProps({
  title: String,
  content: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})

// 用 TypeScript 的话更简单
defineProps<{
  title?: string
  content: string
  count?: number
}>()
</script>

定义 emits 用 defineEmits:

javascript 复制代码
<script setup>
// 基础用法
const emit = defineEmits(['update', 'delete'])

// 带验证的用法
const emit = defineEmits({
  update: (id) => {
    if (id) return true
    console.warn('需要提供 id')
    return false
  }
})

// 实际使用
const handleUpdate = () => {
  emit('update', 123)
}

const handleDelete = () => {
  emit('delete', 456)
}
</script>

五、高级技巧让你更专业

掌握了基础用法,再来看看一些提升效率的高级技巧。

✔ 使用组合式函数,这是 Vue3 的精髓之一:

javascript 复制代码
<script setup>
// 导入 Vue 的响应式 API 和生命周期钩子
import { ref, onMounted } from 'vue'

// 封装一个获取数据的组合式函数,即定义一个组合式函数 useFetch,参数为请求的 URL
const useFetch = (url) => {
  // 创建响应式变量 data,初始为 null,用于存储请求成功返回的数据
  const data = ref(null)
  // 创建响应式变量 loading,初始为 false,表示是否正在请求中
  const loading = ref(false)
  // 创建响应式变量 error,初始为 null,用于存储请求失败时的错误对象
  const error = ref(null)

  // 定义异步请求函数 fetchData
  const fetchData = async () => {
    // 开始请求,将 loading 设为 true
    loading.value = true
    try {
      // 发起 fetch 请求,等待响应
      const response = await fetch(url)
      // 将响应体解析为 JSON,并存入 data.value
      data.value = await response.json()
    } catch (err) {
      // 如果请求或解析过程中抛出异常,将错误对象存入 error.value
      error.value = err
    } finally {
      // 无论成功或失败,最终将 loading 置为 false
      loading.value = false
    }
  }

  // 组件挂载完成后自动执行第一次数据获取
  onMounted(fetchData)

  // 返回响应式数据和控制函数,供组件使用
  return {
    data,
    loading,
    error,
    refetch: fetchData   // 允许手动重新请求
  }
}

// 在组件中调用 useFetch,解构出 userData、loading、error
// 注意:这里将 data 重命名为 userData
const { data: userData, loading, error } = useFetch('/api/user')
</script>

● 注:fetchData函数为什么不用加括号()

onMounted(fetchData) 中的 fetchData不需要手动调用。

onMounted 是 Vue 的生命周期钩子,它接收一个函数作为参数,并在组件挂载完成后自动执行这个函数。

因此,这里的 fetchData 只是把函数引用传给了 onMounted,由 Vue 在合适的时机调用它,而不是在定义时立即执行。

在 `onMounted(fetchData)` 中,`fetchData` 不加括号是因为我们需要传递**函数本身**作为回调,而不是立即执行它。

☑ 加括号 ------onMounted(fetchData()):会立即调用 `fetchData`,然后将它的返回值(一个 `Promise`)传给 `onMounted`。这会导致请求在组件挂载之前就发起,而且 `onMounted` 收到的是一个 `Promise` 而不是函数,无法在挂载后再次执行。

☑ 不加括号 ------onMounted(fetchData):只是将 fetchData`这个函数的引用传给 onMounted。Vue 内部会在组件挂载完成后,自动调用这个函数,从而实现期望的"挂载后请求数据"的逻辑。

简单类比:就像 `setTimeout(yourFunction, 1000)` 要传函数名,而不是 `yourFunction()`。

✔ 使用 defineExpose 暴露组件方法:

javascript 复制代码
<script setup>
// 从 Vue 中导入 ref 函数,用于创建响应式数据
import { ref } from 'vue'

// 创建一个响应式引用 count,初始值为 0
// count.value 可以读取或修改,变化时会触发视图更新
const count = ref(0)

// 定义 increment 函数:将 count 的值增加 1
const increment = () => {
  count.value++   // 直接修改响应式数据的 value 属性
}

// 定义 reset 函数:将 count 的值重置为 0
const reset = () => {
  count.value = 0
}

// 使用 defineExpose 显式暴露 increment 和 reset 方法给父组件
// 父组件可以通过模板 ref 调用这些方法(例如:childRef.increment())
defineExpose({
  increment,
  reset
})
</script>

✔ 使用 useSlots 和 useAttrs:

javascript 复制代码
<script setup>
// 从 Vue 中导入 useSlots 和 useAttrs 组合式 API
import { useSlots, useAttrs } from 'vue'

// 调用 useSlots() 获取当前组件的插槽对象
// 该对象包含了父组件传递的所有具名插槽和默认插槽
const slots = useSlots()

// 调用 useAttrs() 获取当前组件接收到的所有属性(非 props 声明的属性)
// 例如 class、style、id 以及自定义属性等
const attrs = useAttrs()

// 判断 slots 对象中是否存在名为 'header' 的插槽
// !! 将 slots.header 转换为布尔值:存在则为 true,不存在则为 false
// 可以用于条件渲染插槽内容或控制样式
const hasHeaderSlot = !!slots.header

// 从 attrs 对象中解构取出 class 属性值(如果不存在则默认为空字符串)
// 可以用于动态合并或处理外部传入的 CSS 类名
const extraClass = attrs.class || ''
</script>

●注:const hasHeaderSlot = !!slots.header

这是JavaScript中常见的将任意值转换为布尔值的技巧。!是逻辑非运算符,两次!!就是将值强制转换为布尔类型。slots.header可能为undefined或一个函数(插槽存在时)。使用!!可以确保hasHeaderSlot是一个布尔值(true/false),而不是其他truthy/falsy值。所以:将slots.header转换为布尔值,方便后续条件判断。

在 JavaScript 中,!! 是一种将任意值强制转换为布尔类型(true 或 false)的常用写法。

slots.header 的值可能是 undefined(没有该插槽)或一个函数(存在该插槽)。

  • 单个 ! 会先取反:!slots.header 将 undefined 转为 true,将函数对象转为 false。
  • 再取反一次 !!,就得到原始值的布尔等价形式:有插槽时 true,无插槽时 false。

这样 hasHeaderSlot 就是一个明确的布尔值,可以直接用于 v-if 或条件渲染,代码更清晰、严谨。

与 Boolean() 的关系

!!value 与 Boolean(value) ‌效果完全相同‌,区别仅在于写法:

  • !!value:更简洁,常见于代码 golf 或追求简洁的场景;
  • Boolean(value):更具可读性,适合团队协作项目。
javascript 复制代码
!!42;        // true
Boolean(42); // true

**注意事项:**在条件语句(如 if、while)中,JavaScript 会自动进行隐式布尔转换,通常不需要使用 !!:

javascript 复制代码
const value = "hello";
if (value) { // 自动转换,无需 !!value
  console.log("有值");
}

只有在‌需要显式获得布尔值 ‌(如赋值、返回、传参)时,才推荐使用 !!


六、实战案例:打造一个任务管理器

我们来写个完整的任务管理组件:

html 复制代码
<template>
  <!-- 任务管理器主容器 -->
  <div class="task-manager">
    <!-- 添加任务区域 -->
    <div class="add-task">
      <!-- 输入框:双向绑定 newTask,按回车触发 addTask -->
      <input 
        v-model="newTask" 
        @keyup.enter="addTask"
        placeholder="输入新任务..."
        class="task-input"
      >
      <!-- 添加按钮:点击触发 addTask -->
      <button @click="addTask" class="add-btn">添加</button>
    </div>
    
    <!-- 任务列表区域 -->
    <div class="task-list">
      <!-- 遍历 filteredTasks(根据筛选条件计算出的任务列表) -->
      <div 
        v-for="task in filteredTasks" 
        :key="task.id"
        :class="['task-item', { completed: task.completed }]"
      >
        <!-- 复选框:双向绑定任务的 completed 状态 -->
        <input 
          type="checkbox" 
          v-model="task.completed"
          class="task-checkbox"
        >
        <!-- 任务文本,当 completed 为 true 时应用删除线样式 -->
        <span class="task-text">{{ task.text }}</span>
        <!-- 删除按钮:点击时传入任务 id 调用 removeTask -->
        <button @click="removeTask(task.id)" class="remove-btn">删除</button>
      </div>
    </div>
    
    <!-- 任务统计与筛选区域 -->
    <div class="task-stats">
      <span>总计: {{ totalTasks }} 个任务</span>
      <span>已完成: {{ completedTasks }} 个</span>
      <!-- 点击按钮修改 filter 的值,触发 filteredTasks 重新计算 -->
      <button @click="filter = 'all'">全部</button>
      <button @click="filter = 'active'">未完成</button>
      <button @click="filter = 'completed'">已完成</button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'

// 响应式数据
const newTask = ref('')          // 绑定输入框的新任务文本
const tasks = ref([])            // 存储所有任务的数组,每个任务包含 id、text、completed
const filter = ref('all')        // 当前筛选条件:'all' | 'active' | 'completed'

// 添加新任务
const addTask = () => {
  // 去除首尾空格后判断是否为空
  if (newTask.value.trim()) {
    // 向 tasks 数组推入新任务对象(id 使用时间戳,text 为去除空格的输入值,completed 默认为 false)
    tasks.value.push({
      id: Date.now(),
      text: newTask.value.trim(),
      completed: false
    })
    newTask.value = ''           // 清空输入框
    saveToLocalStorage()         // 保存到 localStorage
  }
}

// 删除任务
const removeTask = (id) => {
  // 通过 filter 过滤掉匹配 id 的任务,重新赋值给 tasks
  tasks.value = tasks.value.filter(task => task.id !== id)
  saveToLocalStorage()           // 更新本地存储
}

// 计算属性:任务总数
const totalTasks = computed(() => tasks.value.length)

// 计算属性:已完成任务数量
const completedTasks = computed(() => 
  tasks.value.filter(task => task.completed).length
)

// 计算属性:根据 filter 筛选后的任务列表
const filteredTasks = computed(() => {
  switch (filter.value) {
    case 'active':               // 未完成:completed === false
      return tasks.value.filter(task => !task.completed)
    case 'completed':            // 已完成:completed === true
      return tasks.value.filter(task => task.completed)
    default:                     // 全部
      return tasks.value
  }
})

// 本地存储:将 tasks 数组保存到浏览器的 localStorage 中
const saveToLocalStorage = () => {
  localStorage.setItem('vue-tasks', JSON.stringify(tasks.value))
}

// 从 localStorage 加载保存的任务数据
const loadFromLocalStorage = () => {
  const saved = localStorage.getItem('vue-tasks')
  if (saved) {
    tasks.value = JSON.parse(saved)   // 解析后恢复 tasks 数组
  }
}

// 生命周期:组件挂载后自动加载本地存储中的任务
onMounted(() => {
  loadFromLocalStorage()
})
</script>

<style scoped>
/* 样式部分略,仅作用于当前组件,实现任务管理器的视觉布局 */
.task-manager {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
}

.task-input {
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-right: 8px;
}

.add-btn {
  padding: 8px 16px;
  background: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.task-item {
  display: flex;
  align-items: center;
  padding: 8px;
  border-bottom: 1px solid #eee;
}

.task-item.completed .task-text {
  text-decoration: line-through;
  color: #888;
}

.task-checkbox {
  margin-right: 8px;
}

.task-text {
  flex: 1;
}

.remove-btn {
  padding: 4px 8px;
  background: #ff4757;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.task-stats {
  margin-top: 16px;
  display: flex;
  gap: 12px;
  align-items: center;
}
</style>

这个例子展示了 **<script setup>**在实际项目中的强大能力,代码结构清晰,逻辑组织得当。


七、常见问题解答

Q: 从 Options API 迁移到 <script setup> 难吗? A: 其实不难!大部分概念都是相通的,只是写法更简洁了。建议先从简单的组件开始尝试。

Q: <script setup> 对 TypeScript 支持怎么样? A: 支持非常好!类型推断更加准确,写起来特别舒服。

Q: 还能和普通的 <script> 混用吗? A: 可以的,但通常不建议。除非你有特殊的模块导出需求。

Q: 现有的 Vue2 项目能直接用吗? A: 需要升级到 Vue3,但升级过程比想象中简单,官方提供了详细的迁移指南。


八、最佳实践推荐

按逻辑组织代码,而不是按功能类型。把相关的数据、方法、计算属性放在一起。合理使用组合式函数抽离可复用逻辑。使用 TypeScript 获得更好的开发体验。保持组件的单一职责,不要写太复杂的组件。

九、<script setup> 总结

<script setup> 是 Vue 3 中引入的一种‌编译时语法糖‌,用于在单文件组件(SFC)中更简洁地使用组合式 API(Composition API)。它显著简化了组件的书写方式,提升了开发体验和运行性能。

☑ 核心作用

  • 自动暴露顶层绑定 ‌:在 <script setup> 中声明的变量、函数、导入的内容,‌无需手动 return‌,即可直接在模板中使用 ‌。
  • 省略 setup() 函数定义 ‌:无需再写 export default { setup() { ... } } 的样板代码,逻辑更紧凑。
  • 自动注册组件与指令 ‌:
    • 导入的子组件可直接在模板中作为标签使用,‌无需在 components 中声明‌。
    • 本地自定义指令只需符合命名规范(如 vMyDirective),即可直接在模板中使用。
  • 增强 TypeScript 支持 ‌:
    • 使用 definePropsdefineEmits 可获得‌完整的类型推断‌,支持纯类型声明。
    • 在 Vue 3.5+ 中,从 defineProps 解构出的变量默认是响应式的。
  • 优化运行时性能 ‌:
    • 模板被编译为与 <script setup> 同一作用域的渲染函数,‌避免中间代理对象‌,减少运行时代价。
  • 支持编译器宏 ‌:
    • definePropsdefineEmitsdefineExposedefineModel(Vue 3.4+)等宏‌无需导入‌,仅参与编译,不产生运行时开销。
  • 支持顶层 await ‌:可直接在 <script setup> 中使用 await,代码会被编译为异步 setup 函数。

☑ 典型使用示例

javascript 复制代码
<template>
  <!-- 按钮:点击时触发 increment 方法,显示响应式数据 count 的值 -->
  <button @click="increment">{{ count }}</button>
  <!-- 自动注册,无需声明 -->
  <!-- 使用子组件 ChildComponent,由于 <script setup> 会自动注册导入的组件,无需在 components 选项中声明 -->
  <ChildComponent />
</template>

<script setup>
// 从 Vue 中导入 ref 函数,用于创建响应式数据
import { ref } from 'vue'
// 导入子组件(文件路径假设为 ./ChildComponent.vue)
import ChildComponent from './ChildComponent.vue'

// 创建一个响应式引用 count,初始值为 0
const count = ref(0)
// 定义 increment 函数:每次调用使 count 的值增加 1
const increment = () => { count.value++ }

// 声明 props(带类型推断)
// 使用 defineProps 声明组件接收的 props(此处使用 TypeScript 语法进行类型推断)
// 父组件需要传入一个名为 msg 的字符串属性
const props = defineProps<{ msg: string }>()

// 声明 emits
// 使用 defineEmits 声明组件可触发的自定义事件(此处声明一个名为 'update' 的事件,无参数)
const emit = defineEmits<{ (e: 'update'): void }>()

// 暴露方法给父组件(默认不暴露,需显式声明)
// 使用 defineExpose 显式暴露 increment 方法给父组件
// 父组件可通过模板 ref 访问该组件实例,并调用 increment 方法
// 注意:<script setup> 中的变量默认不暴露,需要显式声明才能被父组件访问
defineExpose({ increment })
</script>

☑ 注意事项

  • <script setup> 中的代码‌在每次组件实例创建时执行 ‌,而普通 <script> 仅在模块首次导入时执行一次。
  • 若需使用传统选项(如 inheritAttrs),需配合普通 <script> 块。
  • 默认情况下,‌组件内部变量不会暴露给父组件 ‌,如需暴露,必须使用 defineExpose

☑ 推荐场景

  • 新项目开发,追求‌**代码简洁与开发效率。**‌
  • 复杂逻辑组件,利用组合式 API 集中管理状态。
  • TypeScript 项目,依赖强类型校验。
  • 组件库开发,降低使用者学习成本。

官方文档推荐:在使用 SFC + Composition API 时,‌**优先使用 <script setup>**‌ ‌


✔ 参考资料 ✔

官方介绍组合式 API:setup() | 官方介绍<script setup>

Vue3中的script setup语法糖的使用及说明 | Vue3 script setup 语法糖(升级版)

vue3入门- script setup详解上-CSDN | vue3入门- script setup详解下-CSDN

vue3 语法糖<script setup>| UniApp中App.vue使用Vue3的<script setup>写法详解

相关推荐
小李子呢02112 小时前
前端八股2---Proxy 代理
前端·javascript·vue.js
军军君013 小时前
Three.js基础功能学习十六:智能黑板实现实例三
前端·javascript·css·vue.js·3d·前端框架·threejs
Hello--_--World4 小时前
Vue2的 双端 diff算法 与 Vue3 的 快速diff 算法
前端·vue.js·算法
gongzemin4 小时前
怎么在VS Code 调试vue2 源码
前端·vue.js
烟话64 小时前
Vue3响应式原理【通俗理解】
前端·javascript·vue.js
军军君015 小时前
数字孪生监控大屏实战模板:可视化数字统计展示
前端·javascript·vue.js·typescript·echarts·数字孪生·前端大屏
踩着两条虫6 小时前
VTJ.PRO AI + 低代码实战:接入高德地图
前端·vue.js·ai编程
xiaotao1316 小时前
Vite 与 Webpack 开发/打包时环境变量对比
前端·vue.js·webpack
前端摸鱼匠7 小时前
Vue 3 的defineProps编译器宏:详解<script setup>中defineProps的使用
前端·javascript·vue.js·前端框架·ecmascript