vue3-<script setup>是Vue3.2+引入编译语法糖与编译器宏以及useSlots()和useAttrs()

<script setup>-组合式API编译时语法糖

单文件组件(SFC)中使用组合式API编译时语法糖,该语法在同时使用单文件组件与组合式API是默认推荐的。

主要优点:

更少的样板代码:无需手动export default,无需setup()函数包裹。

能够使用纯的ts声明和自定义事件。

更好的运行性能(其模板会被编译成同一作用域内的渲染函数,避免上下文代理对象)

主要特性:

顶层绑定自动暴露给模板:在<script setup>中声明的变量,函数,导入内容,无需return,可直接在模板中使用.

自动defineComponent:使用<script setup>的租价会自动包裹在defineComponent()中,无需手动调用。

简洁的props和emits定义:

javascript 复制代码
<script setup>
const props = defineProps({
  msg: String
})

const emit = defineEmits(['change'])

function handleChange() {
  emit('change', 'new value')
}
</script>

异步支持直接await:

javascript 复制代码
<script setup>
const data = await fetch('/api/data').then(r => r.json())
</script>

<script setup>中编译器宏

在<script setup>中,编译器宏是一种特殊的函数,不需要导入,可以直接在<script setup>中使用,这些宏在编译阶段会被处理,不能在普通的逻辑代码如(if,console.log或普通函数内部)中调用,只能在<script setup>的顶层作用域使用。

defineProps()-声明组件接收的props

返回值:返回一个包含所有prop值得响应对象(只读)

javascript 复制代码
<script setup>
// 选项式语法 (Options Syntax)
const props = defineProps({
  msg: String,
  list: {
    type: Array,
    required: true
  }
})

// TypeScript 语法 (Type-Only Syntax) - 推荐 TS 用户使用
// 无需运行时验证,类型仅在编译时检查
const props = defineProps<{
  msg?: string
  list: number[]
}>()
</script>
defineEmits()-声明组件可以触发的事件

返回值:返回emit函数,用于触发事件

javascript 复制代码
<script setup>
// 选项式语法
const emit = defineEmits(['change', 'update'])

// TypeScript 语法
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

function notify() {
  emit('change', 123)
}
</script>
defineExpose()-对外暴露显式指定那些属性/方法可被父组件通过ref访问

默认情况下,<script setup>组件时关闭的,父组件通过模板引用(ref)访问子组件实例时,无法访问到子组件内部定义的变量或方法

子组件对外暴露属性和方法

javascript 复制代码
<!-- Child.vue -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
function increment() {
  count.value++
}

// 显式暴露
defineExpose({
  count,
  increment
})
</script>

父组件进行访问

javascript 复制代码
<!-- Parent.vue -->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'

const childRef = ref(null)

// 现在可以访问 childRef.value.count 和 childRef.value.increment()
</script>
defineModel()-简化组件间的双向数据绑定

以前实现父子组件双向绑定需要手动编写props,emit事件以及computed计算属性来实现,具体操作如下

子组件

javascript 复制代码
<script setup>
// 第一个模型 (对应 v-model)
const name = defineModel('name', { type: String, default: 'Guest' })

// 第二个模型 (对应 v-model:age)
const age = defineModel('age', { type: Number, default: 0 })

function growUp() {
  age.value++
}
</script>

<template>
  <div>
    <label>名字: <input v-model="name" /></label>
    <br />
    <label>年龄: <input v-model="age" /></label>
    <button @click="growUp">长大一岁</button>
  </div>
</template>

父组件

javascript 复制代码
<template>
  <!-- 使用参数化 v-model -->
  <MultiModel 
    v-model:name="userName" 
    v-model:age="userAge" 
  />
</template>
withDefaults()-仅配合TS使用

当使用ts语法定义defineProps时,无法直接设置默认值。withDefaults用于补充默认值。

javascript 复制代码
<script setup lang="ts">
interface Props {
  msg?: string
  labels?: string[]
}

// 设置默认值
const props = withDefaults(defineProps<Props>(), {
  msg: 'Hello',
  labels: () => ['new', 'feature'] // 对象/数组默认值需用工厂函数
})
</script>

组合式API-useSlots()和useAttrs()

对应选项式API中 this.slots this.attrs。组合式API需显式导入。

useAttrs()-属性透传,精细控制属性绑定

应用场景:封装原生的<input>组件。父组件可能会传递各种原生属性(比如placeholder,disabled,class,style)等

如果不使用useAttrs,需要在defineProps里把每个原生属性都定义一次.。

子组件

javascript 复制代码
<template>
  <!-- 
    这是一个多根节点结构 (Fragment):
    1. 外层有个 div 包裹
    2. 内部才是 input
    Vue 不知道把 class/style 给谁,所以默认会报警告或绑定到外层。
    需要用 useAttrs 手动绑定到 input 上。
  -->
  <div class="input-wrapper">
    <label v-if="label" class="label">{{ label }}</label>
    
    <!-- 核心:将 attrs 全部绑定到 input 元素上 -->
    <input 
      v-bind="attrs" 
      class="native-input"
      :value="modelValue"
      @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
    />
    
    <span class="suffix">px</span>
  </div>
</template>

<script setup lang="ts">
import { useAttrs, defineOptions } from 'vue'

// 1. 定义简单的 props (只定义业务相关的)
defineProps<{
  modelValue: string | number
  label?: string
}>()

// 2. 定义事件
defineEmits<{
  'update:modelValue': [value: string]
}>()

// 3. 【关键】关闭自动继承
// 手动把 attrs 绑定到 input 上,如果不关闭,
// 属性会同时出现在外层的 div 和内部的 input 上(导致重复 class 等)
defineOptions({
  inheritAttrs: false
})

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

// 可以在逻辑中读取特定属性(例如用于调试或条件判断)
// 注意:attrs 是响应式的,如果父组件动态改变 class,这里也会变
console.log('当前绑定的额外属性:', attrs)
</script>

<style scoped>
.input-wrapper {
  display: flex;
  align-items: center;
  gap: 8px;
}
.native-input {
  border: 1px solid #ccc;
  padding: 4px 8px;
  /* 父组件传入的 class 会合并到这里 */
}
</style>

父组件

javascript 复制代码
<template>
  <div>
    <!-- 
      注意:我们并没有在 SmartInput 的 props 里定义 
      placeholder, disabled, @focus, style, class 
      但它们都能完美工作!
    -->
    <SmartInput 
      v-model="searchText"
      label="搜索"
      placeholder="请输入关键词..."
      disabled="false"
      class="custom-highlight" 
      style="border-color: blue;"
      @focus="handleFocus"
    />
  </div>
</template>

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

const searchText = ref('')

const handleFocus = () => {
  console.log('Input focused!')
}
</script>
useSlots()-动态渲染插槽

应用场景:权限控制组件,根据某些条件(用户权限,数据是否为空),动态决定是否渲染某个插槽,对插槽的内容进行包装。

子组件:接收一个role属性。当前用户有权限则渲染默认插槽,如果没有权限就渲染'无权限'提示,或者渲染特定的插槽。

javascript 复制代码
<template>
  <!-- 常规情况:直接渲染 -->
  <div v-if="hasPermission" class="content-box">
    <slot />
  </div>

  <!-- 无权限情况:动态决定渲染什么 -->
  <div v-else class="error-box">
    <!-- 
      这里演示用 v-if 判断插槽是否存在。
      虽然模板里可以直接写 <slot name="denied">,
      但如果逻辑更复杂(比如要根据 slots.denied 的返回值类型做处理),
      就需要用到 useSlots。
    -->
    <slot name="denied">
      <p>🚫 您没有权限查看此内容。</p>
    </slot>
  </div>
</template>

<script setup lang="ts">
import { useSlots, computed } from 'vue'

const props = defineProps<{
  requiredRole: string
  currentRole: string
}>()

// 1. 获取所有插槽函数
const slots = useSlots()

// 2. 计算是否有权限
const hasPermission = computed(() => {
  return props.currentRole === props.requiredRole
})

// 3. 【高级用法】在 JS 中检查插槽是否存在
// 有时候我们需要知道父组件是否传了特定的具名插槽,以便做不同的逻辑处理
const hasCustomDeniedSlot = computed(() => {
  // slots.denied 是一个函数,如果父组件没传,它就是 undefined
  return !!slots.denied
})

// 模拟一个场景:如果有自定义 denied 插槽,我们在控制台打个日志
if (hasCustomDeniedSlot.value && !hasPermission.value) {
  console.log('检测到用户使用了自定义的无权限插槽内容')
}

// 4. 【极端高级用法】手动渲染插槽 (通常在 render 函数中用得多,模板中少见)
// 假设我们需要把插槽内容作为参数传给某个第三方 JS 库
function processSlotContent() {
  if (slots.default) {
    // 调用插槽函数,得到 VNode 数组
    const vnodes = slots.default() 
    console.log('默认插槽生成的虚拟节点:', vnodes)
    // 这里可以对 vnodes 进行修改、过滤或包装,然后再返回
    return vnodes
  }
  return null
}
</script>

父组件

javascript 复制代码
<template>
  <!-- 场景 1: 有权限,显示正常内容 -->
  <PermissionWrapper required-role="admin" current-role="admin">
    <h1>管理员仪表盘</h1>
    <p>这里是敏感数据...</p>
  </PermissionWrapper>

  <!-- 场景 2: 无权限,使用默认的拒绝提示 -->
  <PermissionWrapper required-role="admin" current-role="user">
    <h1>普通用户仪表盘</h1>
  </PermissionWrapper>

  <!-- 场景 3: 无权限,使用自定义的拒绝提示 (触发 denied 插槽) -->
  <PermissionWrapper required-role="vip" current-role="user">
    <template #denied>
      <div class="custom-alert">
        🔒 成为 VIP 才能解锁此功能!
        <button>立即升级</button>
      </div>
    </template>
    <h1>VIP 专属内容</h1>
  </PermissionWrapper>
</template>
相关推荐
卸任1 天前
Vibe coding工具使用心得
前端·ai编程
brien91 天前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance1 天前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M1 天前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写1 天前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李1 天前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易1 天前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding1 天前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马1 天前
前沿视界
前端
kyriewen1 天前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员