setup 语法糖从 0 到 1 实战教程

本文是 Vue3 组合式 API 系列的进阶篇,聚焦<script setup> 语法糖的全部用法、实战场景、避坑技巧 ,从基础入门到高级实战,手把手教你吃透 setup 语法糖,大幅提升 Vue3 开发效率。

前置要求 :掌握 Vue3 组合式 API 基础(ref、reactive、computed 等),了解 Vue2 选项式 API 与 Vue3 组合式 API 的区别,具备基础 Vue3 项目开发能力。

一、前言:为什么要使用

在 Vue3 推出组合式 API 初期,我们使用setup() 函数作为组合式逻辑的入口,虽然解决了选项式 API 逻辑碎片化的问题(比如一个业务逻辑的代码分散在 data、methods、computed 中),但在实际开发中存在三个明显痛点,严重影响开发效率:

  1. 手动暴露冗余 :所有需要在模板中使用的变量、方法,都必须手动通过return 暴露,代码冗余且容易遗漏,一旦遗漏就会导致模板渲染失败,排查起来耗时费力;
  2. 组件注册繁琐 :组件导入后,必须在 components 选项中手动注册(如 components: { XXX }),哪怕是常用的基础组件,也需要重复注册,增加不必要的代码量;
  3. TS 适配不佳:与 TypeScript 结合时,类型声明繁琐,需要手动定义接口、标注类型,开发体验不够流畅,难以发挥 TS 的类型校验优势。

为了解决这些问题,Vue3.2 版本正式推出 <script setup> 语法糖,它并非新增功能,而是 setup() 函数的"语法糖简化版"------保留了组合式 API 的所有功能,同时大幅简化代码、提升开发效率,目前已成为 Vue3 开发的主流写法,几乎所有 Vue3 项目(包括 Vue3 + TS 项目)都会优先使用。

先看一组直观对比,快速感受语法糖的优势:

1.1 传统 setup() 函数写法

vue 复制代码
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">+1</button>
    <HelloWorld />
  </div>
</template>

<script>
import { ref } from 'vue'
import HelloWorld from './HelloWorld.vue'

export default {
  // 手动注册组件,哪怕只导入一个也需要写
  components: { HelloWorld },
  // 手动定义 setup 函数,作为组合式逻辑入口
  setup() {
    // 定义响应式变量
    const count = ref(0)
    // 定义方法
    const increment = () => count.value++
    
    // 手动 return 暴露,模板才能使用,少写一个就报错
    return {
      count,
      increment
    }
  }
}
</script>

1.2 <script setup> 语法糖写法

vue 复制代码
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">+1</button>
    <HelloWorld />
  </div>
</template>

<!-- 只需在 script 标签上添加 setup 关键字,无需其他多余配置 -->
<script setup>
import { ref } from 'vue'
// 导入组件自动注册,无需手动写 components 选项
import HelloWorld from './HelloWorld.vue'

// 顶层声明的变量、方法,自动暴露给模板,无需 return
const count = ref(0)
const increment = () => count.value++
</script>

对比可见,语法糖写法删除了冗余的export defaultcomponents 注册、return 暴露,代码量减少近一半,且逻辑更清晰------所有组合式逻辑都能在顶层直接编写,无需嵌套在 setup 函数中。接下来,我们从 0 到 1 实战掌握它的所有用法,覆盖基础、进阶、实战、避坑全场景。

二、基础入门:<script setup> 核心基础用法

这一部分是语法糖的基础,也是日常开发中最常用的内容,必须熟练掌握。核心原则:<script setup> 内部的代码,本质上就是 setup() 函数的函数体,所有规则与 setup() 函数一致,只是简化了写法。

2.1 基本使用:语法格式与自动暴露

<script setup> 的使用非常简单,只需在 <script> 标签上添加 setup 关键字即可,无需额外配置,核心规则如下(重点记):

  • 顶层声明的变量、函数、类 ,会自动暴露给模板使用,无需手动 return;注意:仅顶层声明有效,嵌套在函数内部的变量/方法不会自动暴露。
  • 语法糖内部的代码,相当于在 setup() 函数内部执行,this 指向 undefined(刻意设计,避免开发者依赖 this,更符合组合式 API "脱离 this" 的设计理念)。
  • 默认情况下,不能与传统的<script> 标签同时使用(即一个组件中不能有两个 script 标签);特殊场景(如需要配置组件选项)可结合使用,后续进阶部分会讲解。
  • 语法糖内部无法直接访问组件的选项式 API(如 data、methods、computed 等),因为它本身就是组合式 API 的简化写法,建议全程使用组合式 API 编写逻辑。

实战示例:基础变量与方法

vue 复制代码
<template>
  <div class="base-demo">
    <h3>基础用法演示</h3>
    <p>姓名:{{ name }}</p>
    <p>年龄:{{ age }}</p>
    <p>是否成年:{{ isAdult }}</p>
    <button @click="changeName">修改姓名</button>
    <button @click="changeAge">增长年龄</button>
  </div>
</template>

<script setup>
// 1. 基础变量(非响应式,仅演示自动暴露)
// 注意:非响应式变量修改后,模板不会自动更新
const name = '张三'

// 2. 响应式变量(ref 包装基本类型)
import { ref, computed } from 'vue'
const age = ref(20)

// 3. 计算属性(自动暴露,无需 return)
const isAdult = computed(() => age.value >= 18)

// 4. 顶层函数,自动暴露给模板
const changeName = () => {
  // 非响应式变量修改,模板不更新
  name = '李四' // 无效,模板仍显示"张三"
  console.log(name) // 控制台打印"李四",但模板无变化
}

const changeAge = () => {
  // 响应式变量修改,需通过 .value 操作
  age.value++
  // 计算属性会自动响应依赖变化
}
</script>

关键注意点:

  • 非响应式变量(如 const name = '张三')修改后,模板不会更新,因为 Vue 无法监听基本类型的直接赋值,需使用 ref 或 reactive 包装为响应式。
  • ref 包装的响应式变量,在 script 中修改时需加 .value,模板中使用时无需加 .value(Vue 自动解包)。

2.2 组件导入与自动注册

这是 <script setup> 最实用的特性之一:组件导入后自动注册,无需在 components 选项中手动声明,大幅减少冗余代码。

基本用法

vue 复制代码
<template>
  <div>
    <!-- 直接使用导入的组件,无需注册 -->
    <HelloWorld />
    <UserCard :name="name" />
  </div>
</template>

<script setup>
// 导入组件,自动注册
import HelloWorld from './HelloWorld.vue'
import UserCard from './UserCard.vue'

// 定义传递给子组件的变量
const name = '张三'
</script>

进阶用法:重命名组件

如果导入的组件名称与当前组件内的变量/函数重名,或想简化组件名称,可使用 ES6 解构重命名:

vue 复制代码
<template>
  <div>
    <!-- 使用重命名后的组件名称 -->
    <Hello />
    <Card :name="name" />
  </div>
</template>

<script setup>
// 重命名导入,避免命名冲突
import { default as Hello } from './HelloWorld.vue'
import { default as Card } from './UserCard.vue'

const name = '张三'
</script>

进阶用法:动态导入组件

结合动态导入(懒加载),优化组件加载性能,适用于组件体积较大或按需加载的场景:

vue 复制代码
<template>
  <div>
    <!-- 动态组件,按需渲染 -->
    <component :is="AsyncComponent" />
    <button @click="showComponent = true">显示组件</button>
  </div>
</template>

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

// 动态导入组件(懒加载),自动注册
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'))

// 控制组件是否显示
const showComponent = ref(false)
</script>

注意:动态导入组件时,需使用 defineAsyncComponent 包裹,这是 Vue3 提供的专门用于动态导入组件的 API,与 setup 语法糖兼容。

2.3 响应式数据:ref、reactive 与解构

setup 语法糖中,响应式数据的使用与 setup() 函数完全一致,核心还是 ref(包装基本类型)和 reactive(包装引用类型),但需注意解构时的响应式丢失问题。

1. ref 用法(推荐用于基本类型)

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

// 基本类型响应式(number、string、boolean 等)
const count = ref(0)
const name = ref('张三')
const isShow = ref(false)

// 修改响应式数据(必须加 .value)
const increment = () => {
  count.value++
}

const changeName = () => {
  name.value = '李四'
}

const toggleShow = () => {
  isShow.value = !isShow.value
}
</script>

2. reactive 用法(推荐用于引用类型)

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

// 引用类型响应式(对象、数组)
const user = reactive({
  name: '张三',
  age: 20,
  address: {
    province: '广东',
    city: '深圳'
  }
})

const list = reactive([1, 2, 3, 4])

// 修改响应式数据(无需 .value,直接修改属性)
const updateUser = () => {
  user.age = 21
  user.address.city = '广州'
}

const addItem = () => {
  list.push(5)
}
</script>

3. 解构响应式数据(避坑重点)

直接解构 reactive 包装的对象,会导致响应式丢失------修改解构后的变量,不会同步更新模板。解决方案:使用 toRefstoRef 解构。

vue 复制代码
<template>
  <div>
    <p>姓名:{{ name }}</p>
    <p>年龄:{{ age }}</p>
    <button @click="changeInfo">修改信息</button>
  </div>
</template>

<script setup>
import { reactive, toRefs, toRef } from 'vue'

const user = reactive({
  name: '张三',
  age: 20
})

// 错误写法:直接解构,响应式丢失
// const { name, age } = user 
// 修改 name 不会更新模板

// 正确写法1:使用 toRefs 解构(适用于所有属性)
const { name, age } = toRefs(user)

// 正确写法2:使用 toRef 解构(适用于单个属性)
// const name = toRef(user, 'name')
// const age = toRef(user, 'age')

const changeInfo = () => {
  // 解构后需加 .value 修改
  name.value = '李四'
  age.value = 21
}
</script>

关键总结:

  • ref 包装的变量,无论在 script 中还是解构后,都需加 .value 修改。
  • reactive 包装的对象,直接修改属性无需 .value,但解构时必须用 toRefs/toRef,否则丢失响应式。

三、进阶用法:setup 语法糖核心特性

掌握基础用法后,我们来看 setup 语法糖的进阶特性,这些特性能解决更多复杂场景的需求,进一步提升开发效率,也是面试中常考的知识点。

3.1 与传统 script 标签结合使用

默认情况下,一个组件中只能有一个 <script setup> 标签,但在某些场景下(如需要配置组件选项:name、inheritAttrs、props 校验等),可以结合传统的 <script> 标签使用------两个 script 标签共存,各司其职。

核心规则:传统 script 标签用于配置组件选项(export default 导出),setup 语法糖用于编写组合式逻辑,两者互不冲突。

vue 复制代码
<template>
  <div>
    <p>{{ name }}</p>
  </div>
</template>

<!-- 传统 script 标签:配置组件选项 -->
<script>
export default {
  // 配置组件名称(用于调试、递归组件等)
  name: 'MyComponent',
  // 关闭属性继承(避免 attrs 自动绑定到根元素)
  inheritAttrs: false,
  // 组件props校验(也可在 setup 中用 defineProps 定义)
  props: {
    name: {
      type: String,
      required: true,
      default: '默认名称'
    }
  }
}
</script>

<!-- setup 语法糖:编写组合式逻辑 -->
<script setup>
// 可直接使用传统 script 中定义的 props
import { useAttrs } from 'vue'

// 获取组件 attrs(因 inheritAttrs: false,需手动绑定)
const attrs = useAttrs()
console.log(attrs)
</script>

常见使用场景:

  • 需要配置组件 name(用于调试、递归组件、KeepAlive 缓存等)。
  • 需要配置 inheritAttrs、components(虽然 setup 可自动注册,但特殊场景可手动配置)。
  • 需要编写复杂的 props 校验(虽然 setup 中可通过 defineProps 定义,但传统 props 选项写法更灵活)。

3.2 Props 定义与校验(defineProps)

在 setup 语法糖中,无法直接使用传统的 props 选项,需通过 Vue3 提供的 defineProps 宏函数定义 props,支持 props 校验、默认值等功能,与传统 props 选项完全兼容。

注意:defineProps 是宏函数,无需导入,可直接在 setup 语法糖中使用(Vue 自动注入)。

基础用法:简单 props 定义

vue 复制代码
<template>
  <div>
    <p>父组件传递的名称:{{ name }}</p>
    <p>父组件传递的年龄:{{ age }}</p>
  </div>
</template>

<script setup>
// 基础写法:数组形式(仅定义 props 名称,无校验)
// const props = defineProps(['name', 'age'])

// 推荐写法:对象形式(支持校验、默认值)
const props = defineProps({
  name: {
    type: String, // 类型校验
    required: true, // 是否必传
    message: 'name 为必填项,且必须是字符串' // 校验失败提示
  },
  age: {
    type: Number,
    required: false,
    default: 18, // 默认值
    validator: (value) => {
      // 自定义校验规则:年龄必须大于 0
      return value > 0
    }
  }
})

// 使用 props(无需 .value,直接使用)
console.log(props.name)
console.log(props.age)
</script>

进阶用法:与 TypeScript 结合(类型推导)

在 Vue3 + TS 项目中,defineProps 支持通过 TypeScript 类型直接定义 props,无需编写繁琐的对象形式,TS 会自动进行类型校验,开发体验更好。

vue 复制代码
<script setup lang="ts">
// 方式1:直接通过类型定义 props(无默认值)
const props = defineProps<{
  name: string
  age?: number // 可选属性
  gender: 'male' | 'female' // 联合类型
}>()

// 方式2:结合 withDefaults 定义默认值(推荐)
const props = withDefaults(
  defineProps<{
    name: string
    age?: number
    gender?: 'male' | 'female'
  }>(),
  {
    age: 18,
    gender: 'male'
  }
)

// 使用 props
console.log(props.name)
console.log(props.age)
</script>

关键注意点:withDefaults 是 Vue3 提供的宏函数,用于给 TS 类型定义的 props 设置默认值,无需导入,直接使用。

3.3 事件触发:defineEmits

在 setup 语法糖中,子组件向父组件触发事件,需通过 defineEmits 宏函数定义事件,替代传统的 emits 选项,支持事件校验、类型定义等功能。

注意:defineEmits 也是宏函数,无需导入,直接使用。

基础用法:简单事件触发

vue 复制代码
<!-- 子组件 Child.vue -->
<template>
  <button @click="handleClick">触发父组件事件</button>
  <button @click="handleSend">触发事件并传参</button>
</template>

<script setup>
// 基础写法:数组形式(仅定义事件名称)
// const emit = defineEmits(['click', 'send'])

// 推荐写法:对象形式(支持事件参数校验)
const emit = defineEmits({
  // 无参数事件
  click: () => true,
  // 有参数事件,校验参数格式
  send: (data: string) => {
    return typeof data === 'string'
  }
})

// 触发事件
const handleClick = () => {
  emit('click')
}

// 触发事件并传递参数
const handleSend = () => {
  emit('send', '子组件传递的参数')
}
</script>
vue 复制代码
<!-- 父组件 Parent.vue -->
<template>
  <Child @click="handleChildClick" @send="handleChildSend" />
</template>

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

const handleChildClick = () => {
  console.log('子组件触发了 click 事件')
}

const handleChildSend = (data) => {
  console.log('子组件传递的参数:', data)
}
</script>

进阶用法:与 TypeScript 结合

vue 复制代码
<script setup lang="ts">
// 通过 TS 类型定义事件(推荐,类型自动校验)
const emit = defineEmits<{
  (e: 'click'): void // 无参数事件
  (e: 'send', data: string): void // 有参数事件
}>()

// 触发事件(参数类型错误会报错)
const handleSend = () => {
  emit('send', '正确参数') // 正常触发
  // emit('send', 123) // TS 报错,参数类型必须是 string
}
</script>

3.4 插槽使用:defineSlots

在 setup 语法糖中,可通过 defineSlots 宏函数定义组件的插槽,支持插槽类型校验(主要用于 Vue3 + TS 项目),替代传统的 slots 选项。

注意:defineSlots 仅用于类型定义和校验,无需手动注册插槽,模板中可直接使用插槽。

vue 复制代码
<!-- 子组件 Child.vue -->
&lt;template&gt;
  &lt;div&gt;
    <!-- 默认插槽 -->
    &lt;slot&gt;默认内容&lt;/slot&gt;
    <!-- 具名插槽 -->
    <slot name="title"&gt;默认标题&lt;/slot&gt;
    <!-- 作用域插槽 -->
    <slot name="item" :data="list"></slot>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const list = ref([1, 2, 3])

// 定义插槽类型(TS 项目推荐,非 TS 项目可省略)
defineSlots<{
  // 默认插槽
  default?: () => any
  // 具名插槽
  title?: () => any
  // 作用域插槽(指定传递给父组件的参数类型)
  item?: (props: { data: number[] }) => any
}>()
</script>
vue 复制代码
<!-- 父组件 Parent.vue -->
<template&gt;
  &lt;Child&gt;
    <!-- 默认插槽 -->
    <div>父组件默认插槽内容</div&gt;
    <!-- 具名插槽 -->
    <template #title>
      <h2>父组件标题插槽</h2>
    </template>
    <!-- 作用域插槽 -->
    <template #item="slotProps">
      <div v-for="item in slotProps.data" :key="item">{{ item }}</div>
    </template>
  </Child>
</template>

3.5 访问组件实例:useAttrs 与 useSlots

在 setup 语法糖中,this 指向 undefined,无法通过 this 访问组件实例的 attrs、slots 等属性,需通过 useAttrsuseSlots 两个 API 访问。

1. useAttrs:访问组件的 attrs

attrs 包含父组件传递的、未被 props 接收的属性(如 class、style、自定义属性等),与传统的 this.$attrs 功能一致。

vue 复制代码
<template>
  <div :class="attrs.class" :style="attrs.style">
    {{ attrs.customAttr }}
  </div>
</template>

<script setup>
import { useAttrs } from 'vue'

// 获取 attrs 对象
const attrs = useAttrs()

// 访问 attrs 中的属性
console.log(attrs.customAttr)
console.log(attrs.class)
</script>

2. useSlots:访问组件的 slots

slots 包含父组件传递的所有插槽内容,与传统的 this.$slots 功能一致,主要用于在 script 中操作插槽(如判断插槽是否存在)。

vue 复制代码
<script setup>
import { useSlots } from 'vue'

// 获取 slots 对象
const slots = useSlots()

// 判断默认插槽是否存在
console.log(slots.default) // 存在则返回插槽内容,不存在则返回 undefined

// 判断具名插槽是否存在
console.log(slots.title)
</script>

四、实战场景:setup 语法糖常用组合用法

结合前面的知识点,我们通过几个实战场景,演示 setup 语法糖的常用组合用法,覆盖日常开发中最常见的场景,帮助你快速上手。

4.1 场景1:基础页面开发(变量、方法、组件引入)

vue 复制代码
<template>
  <div class="home-page">
    <Header title="首页" />
    <div class="content">
      <h3>欢迎来到首页,{{ username }}</h3>
      <p>当前登录时长:{{ loginTime }} 秒</p>
      <button @click="logout">退出登录</button>
    </div>
    <Footer />
  </div>
</template>

<script setup>
// 引入组件(自动注册)
import Header from './components/Header.vue'
import Footer from './components/Footer.vue'

// 响应式变量
import { ref, onMounted } from 'vue'
const username = ref('张三')
const loginTime = ref(0)

// 方法
const logout = () => {
  alert('退出登录成功')
  // 实际开发中可结合路由跳转
}

// 生命周期钩子(直接使用,无需注册)
onMounted(() => {
  // 模拟登录时长统计
  setInterval(() => {
    loginTime.value++
  }, 1000)
})
</script>

<style scoped>
.home-page {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
.content {
  flex: 1;
  padding: 20px;
}
</style>

4.2 场景2:子父组件通信(props + emit)

vue 复制代码
<!-- 子组件 TodoItem.vue -->
<template>
  <div class="todo-item" :class="{ completed: props.completed }">
    <input type="checkbox" v-model="props.completed" @change="handleChange">
    <span>{{ props.content }}</span>
    <button @click="handleDelete">删除</button>
  </div>
</template>

<script setup>
// 定义 props
const props = defineProps({
  content: {
    type: String,
    required: true
  },
  completed: {
    type: Boolean,
    default: false
  },
  index: {
    type: Number,
    required: true
  }
})

// 定义 emit
const emit = defineEmits(['change', 'delete'])

// 触发事件(传递参数)
const handleChange = () => {
  emit('change', props.index, props.completed)
}

const handleDelete = () => {
  emit('delete', props.index)
}
</script>
vue 复制代码
<!-- 父组件 TodoList.vue -->
<template>
  <div class="todo-list">
    <h3>待办列表</h3>
    <input 
      type="text" 
      v-model="inputVal" 
      @keyup.enter="addTodo"
      placeholder="请输入待办内容"
    >
    <TodoItem 
      v-for="(item, index) in todoList" 
      :key="index"
      :content="item.content"
      :completed="item.completed"
      :index="index"
      @change="handleTodoChange"
      @delete="handleTodoDelete"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import TodoItem from './TodoItem.vue'

// 响应式数据
const inputVal = ref('')
const todoList = ref([
  { content: '学习 setup 语法糖', completed: false },
  { content: '完成 Vue3 实战', completed: true }
])

// 添加待办
const addTodo = () => {
  if (!inputVal.value.trim()) return
  todoList.value.push({
    content: inputVal.value,
    completed: false
  })
  inputVal.value = ''
}

// 修改待办状态
const handleTodoChange = (index, completed) => {
  todoList.value[index].completed = completed
}

// 删除待办
const handleTodoDelete = (index) => {
  todoList.value.splice(index, 1)
}
</script>

4.3 场景3:Vue3 + TS 组合使用(完整示例)

vue 复制代码
<template>
  <div class="user-info">
    <h3>用户信息</h3>
    <p>姓名:{{ user.name }}</p>
    <p>年龄:{{ user.age }}</p>
    <p>性别:{{ user.gender }}</p>
    <button @click="updateAge">年龄+1</button>
  </div>
</template>

<script setup lang="ts">
// 定义用户类型接口
interface User {
  name: string
  age: number
  gender: 'male' | 'female'
}

// 响应式数据(指定类型)
import { ref, computed } from 'vue'
const user = ref<User>({
  name: '张三',
  age: 20,
  gender: 'male'
})

// 计算属性(类型自动推导)
const isAdult = computed(() => user.value.age >= 18)

// 方法(指定参数和返回值类型)
const updateAge = (): void => {
  user.value.age++
}

// 打印用户信息(类型校验)
console.log(user.value.name)
// console.log(user.value.address) // TS 报错,User 接口中无 address 属性
</script>

五、避坑指南:setup 语法糖常见错误与解决方案

在使用 setup 语法糖的过程中,新手容易遇到一些问题,这里整理了最常见的 5 个坑,附上解决方案,帮助你避免踩坑。

5.1 坑1:解构 reactive 数据导致响应式丢失

错误表现:解构 reactive 包装的对象后,修改解构后的变量,模板不更新。

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

const user = reactive({ name: '张三', age: 20 })
// 错误:直接解构,响应式丢失
const { name, age } = user

const changeName = () => {
  name = '李四' // 模板不更新
}
</script>

解决方案:使用 toRefs 或 toRef 解构,修改时加 .value。

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

const user = reactive({ name: '张三', age: 20 })
// 正确:使用 toRefs 解构
const { name, age } = toRefs(user)

const changeName = () => {
  name.value = '李四' // 模板正常更新
}
</script>

5.2 坑2:忘记给 ref 变量加 .value 修改

错误表现:修改 ref 包装的变量时,忘记加 .value,导致变量修改失败,模板不更新。

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

const count = ref(0)

const increment = () => {
  // 错误:忘记加 .value
  count++ // 无效,count 仍为 0
}
</script>

解决方案:修改 ref 变量时,必须加 .value;模板中使用时无需加。

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

const count = ref(0)

const increment = () => {
  // 正确:加 .value
  count.value++
}
</script>

5.3 坑3:同时使用两个 script 标签,未区分功能

错误表现:在一个组件中使用两个 script 标签,且都写了组合式逻辑,导致逻辑冲突、变量未定义。

vue 复制代码
<!-- 错误写法 -->
<script>
// 传统 script 标签写组合式逻辑
import { ref } from 'vue'
const count = ref(0)
</script>

<script setup>
// 语法糖中也写逻辑,导致 count 未定义
console.log(count) // 报错:count is not defined
</script>

解决方案:两个 script 标签分工明确------传统 script 标签仅用于配置组件选项(name、props 等),setup 语法糖用于编写组合式逻辑,且组合式逻辑仅在 setup 语法糖中编写。

5.4 坑4:使用 this 访问组件实例

错误表现:在 setup 语法糖中使用 this,试图访问 props、emit、attrs 等,导致报错(this is undefined)。

vue 复制代码
<script setup>
// 错误:使用 this
console.log(this.props.name) // 报错:Cannot read property 'props' of undefined
</script>

解决方案:放弃使用 this,通过 defineProps、defineEmits、useAttrs 等 API 访问对应内容。

5.5 坑5:组件导入后未使用,导致报错

错误表现:导入组件后未在模板中使用,Vue3 会报"组件未使用"的警告,部分严格模式下会报错。

vue 复制代码
<script setup>
// 错误:导入组件但未使用
import HelloWorld from './HelloWorld.vue'
</script>

解决方案:要么在模板中使用导入的组件,要么删除未使用的导入语句;如果确实需要导入但不使用(如动态导入备用组件),可在导入语句后加 // @ts-ignore(TS 项目)或忽略警告。

六、总结与拓展

6.1 核心总结

<script setup> 语法糖是 Vue3 组合式 API 的简化写法,核心优势是简化代码、提升效率,无需手动 return 暴露、无需手动注册组件,与 TypeScript 结合良好,是当前 Vue3 开发的主流选择。

核心知识点梳理:

  • 基础用法:添加 setup 关键字,顶层变量/方法自动暴露,无需 return。
  • 组件导入:自动注册,支持重命名、动态导入。
  • 响应式数据:ref(基本类型)、reactive(引用类型),解构用 toRefs/toRef。
  • 进阶特性:defineProps(props 定义)、defineEmits(事件触发)、defineSlots(插槽定义)。
  • 避坑重点:避免解构 reactive 丢失响应式、ref 变量修改加 .value、不使用 this。

6.2 拓展延伸

  1. 与其他 API 结合:setup 语法糖可与 Vue3 其他 API 无缝结合,如useRouter(路由)、useStore(Pinia/Vuex)、useFetch(请求)等,后续会单独讲解。

  2. 生命周期钩子:setup 语法糖中,可直接使用 Vue3 的生命周期钩子(如 onMounted、onUpdated 等),无需注册,直接导入使用即可。

  3. 兼容性:setup 语法糖从 Vue3.2 版本开始支持,如果你使用的是 Vue3.0 或 3.1 版本,需升级 Vue 版本才能使用。

掌握 setup 语法糖,能大幅提升你的 Vue3 开发效率,减少冗余代码,让组合式逻辑更清晰。建议多动手实战,结合本文的示例,尝试在项目中使用,快速吃透所有用法。

相关推荐
颜酱2 小时前
回溯算法实战练习(2)
javascript·后端·算法
周淳APP2 小时前
【React Fiber架构+React18知识点+浏览器原生帧流程和React阶段流程相串】
前端·javascript·react.js·架构
reasonsummer2 小时前
【白板类-01-01】20260326水果连连看01(html+希沃白板)
前端·html
HelloReader2 小时前
Qt Quick 视觉元素、交互与自定义组件(七)
前端
We་ct2 小时前
LeetCode 153. 旋转排序数组找最小值:二分最优思路
前端·算法·leetcode·typescript·二分·数组
程序员阿峰2 小时前
前端3D·Three.js一学就会系列:第二 画线
前端·three.js
HelloReader2 小时前
Qt Quick Controls 控件库、样式与布局(八)
前端
兔司基2 小时前
Node.js/Express 实现 AI 流式输出 (SSE) 踩的坑:为什么客户端会“瞬间断开连接”?
前端
yuki_uix2 小时前
一次 CR 引发的思考:我的 rules.ts 构想,究竟属于哪种开发哲学?
前端·ai编程