Vue 组合式API 与 <script setup> 的深度解析

以下是关于 Vue 组合式API<script setup> 的深度解析与最佳实践指南:


一、组合式API核心思想

1. 逻辑关注点分离
将组件逻辑按功能而非选项(data/methods)拆分,解决选项式API的碎片化问题:

// 传统选项式API的碎片化问题
export default {
  data() { return { count: 0 } },    // 状态分散
  methods: { increment() { ... } }, // 方法分离
  mounted() { ... }                 // 生命周期分离
}

// 组合式API聚合逻辑
function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  onMounted(() => console.log('Mounted!'))
  return { count, increment }
}

2. 响应式系统核心

  • ref:包装基本类型为响应式对象(通过.value访问)
  • reactive:创建深层响应式对象(适合复杂数据结构)
  • toRefs:解构reactive对象保持响应性
const user = reactive({ name: 'Alice', age: 30 })
const { name, age } = toRefs(user) // 解构后仍保持响应性

二、<script setup> 语法革命

1. 语法糖本质
编译时转换为标准组合式API,减少样板代码:

<!-- 编译前 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<!-- 编译后 -->
<script>
export default {
  setup() {
    const count = ref(0)
    return { count } // 自动暴露顶层绑定
  }
}
</script>

2. 核心特性

  • 自动暴露:顶层变量/函数直接模板可用
  • 编译器宏defineProps, defineEmits, defineExpose
  • 顶层await:直接使用异步操作
<script setup>
// Props声明
const props = defineProps({
  title: String,
  defaultValue: { type: Number, default: 0 }
})

// 事件发射
const emit = defineEmits(['update:title'])

// 暴露给父组件
defineExpose({ internalMethod: () => {...} })

// 异步数据获取
const data = await fetchData()
</script>

三、最佳实践方案

1. 逻辑复用模式
创建可组合函数(Composables):

// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

// 组件中使用
<script setup>
import { useMouse } from './useMouse'
const { x, y } = useMouse()
</script>

2. 状态管理策略

  • 小型应用:直接使用reactive共享状态
  • 大型项目:组合provide/inject + Pinia
// 共享状态
const globalState = reactive({ user: null })

// 提供状态
provide('globalState', globalState)

// 注入使用
const injectedState = inject('globalState')

3. 类型安全增强
配合TypeScript实现完整类型推断:

<script setup lang="ts">
interface User {
  id: number
  name: string
}

// 类型化Props
const props = defineProps<{
  user: User
  showDetail: boolean
}>()

// 类型化Emit
const emit = defineEmits<{
  (e: 'update:user', payload: User): void
  (e: 'delete'): void
}>()
</script>

四、性能优化技巧

1. 响应式优化

  • 使用shallowRef/shallowReactive避免深层响应
  • 利用computed缓存计算结果
const heavyList = shallowRef([]) // 仅跟踪.value变化
const filteredList = computed(() => 
  heavyList.value.filter(item => item.isActive)

2. 生命周期控制

  • 使用watchEffect自动清理副作用
  • 及时清除定时器/事件监听器
watchEffect((onCleanup) => {
  const timer = setInterval(() => {...}, 1000)
  onCleanup(() => clearInterval(timer))
})

3. 组件优化

  • 使用v-memo避免不必要的重渲染
  • 拆分大组件为逻辑集中的小型组合
<template>
  <div v-memo="[dependency]">
    <!-- 仅当dependency变化时重渲染 -->
  </div>
</template>

五、常见问题解决方案

1. 模板引用处理
使用ref+defineExpose实现组件通信:

<!-- Child.vue -->
<script setup>
const inputRef = ref(null)
defineExpose({ focus: () => inputRef.value.focus() })
</script>

<template>
  <input ref="inputRef">
</template>

<!-- Parent.vue -->
<template>
  <Child ref="childRef" />
</template>

<script setup>
const childRef = ref(null)
const focusInput = () => childRef.value.focus()
</script>

2. 路由状态管理
与Vue Router深度集成:

import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

watch(() => route.params.id, (newId) => {
  fetchData(newId)
})

3. 样式作用域
结合scoped与CSS变量:

<style scoped>
.button {
  color: var(--theme-color);
}
</style>

六、升级迁移策略

1. 渐进式迁移

  • 新组件使用<script setup>
  • 旧组件逐步重构为组合式API
  • 使用@vue/compat进行兼容过渡

2. 工具链支持

  • Vite默认支持组合式API
  • Vue CLI需升级至v5+
  • ESLint配置vue/setup-compiler-macros

通过组合式API与<script setup>的配合,开发者可以构建出 更灵活、更易维护 的Vue 3应用。关键要把握:

  1. 逻辑关注点聚合:通过组合函数实现高内聚
  2. 响应式精准控制:理解ref/reactive适用场景
  3. 工程化实践:结合TypeScript和现代构建工具提升质量
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=no" /> <title>payCar</title> <style> *{ margin: 0; padding: 0; box-sizing: border-box; } body{ background: lightyellow; text-align: center; font-size: 16px; } .main-img{ display: inline-block; vertical-align: middle; width: 100%; box-shadow: 0 2px 8px rgba(0,0,0,0.1); max-width: 1200px; border-radius: 4px; } .container { display: flex; flex-direction: column; align-items: center; width: 100%; min-height: calc(100vh - 100px); margin-top: 8px; padding: 0 4px; } .container table{ border-collapse: collapse; width: 100%; max-width: 1200px; background-color: white; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 12px; } .main-header td{ text-align: center; border: whitesmoke 1px solid; height: 2rem; background-color: #fafafa; font-weight: bold; } .payCar td{ text-align: center; border: whitesmoke 1px solid; height: 8rem; vertical-align: middle; padding: 4px; } table tr:hover { background-color: ghostwhite; } .payImage{ width: 60px; height: 60px; border-radius: 4px; object-fit: cover; } .btn1{ margin: 0 2px; border: none; background-color: whitesmoke; border-radius: 4px; cursor: pointer; width: 2rem; height: 2rem; transition: background-color 0.2s; } .btn1 :hover{ box-shadow: inset 0px 5px 10px rgba(0, 0, 0, 0.5); } .payNum{ width: 3.5rem; height: 2.2rem; text-align: center; border: 1px solid #ccc; border-radius: 4px; } .empty{ font-size: 18px; color: #666; padding: 40px 0; } .total-container{ width: 100%; max-width: 1200px; } .total-container span{ padding: 10px 10px; font-size: 18px; color: orangered; } .total-container button{ padding: 5px 10px; background-color: dodgerblue; color: white; border: none; } @media (max-width: 770px) { body { font-size: 16px; } .main-header td{ font-size: 12px; } .payNum{ width: 1.5rem; height: 2rem; } } @media (max-width: 320px) { .dj{ display: none; } } </style> </head> <body> <div id="app" class="app-container"> <!-- 顶部--> <div class="banner-box"> <!-- 面包屑--> </div> <div class="main"> <img src="../img/【哲风壁纸】动漫-卡提希娅.png" class="main-img"> <div class="container"> <table> <tr class="main-header"> <td width="12%">选中</td> <td width="20%">图片</td> <td width="20%" class="dj">单价</td> <td width="28%">个数</td> <td width="20%">小计</td> </tr> <tr class="payCar" v-for="(item,index) in fruitList" :key="item.id"> <td><input type="checkbox" v-model="item.isChecked"></td> <td ><img :src="`../img/${item.imageName}.png`" :alt="item.name" class="payImage"></td> <td >{{item.price }}</td> <td > <button class="btn1" @click="numAdd(item)">+</button> <input type="text" class="payNum" v-model.number="item.num"> <button class="btn1" @click="numDel(item)">-</button> </td> <td>{{(item.price * item.num).toFixed(2)}}元</td> </tr> <tr class="total-container" v-if="fruitList.length > 0"> <td > <label>全选<input type="checkbox" v-model="allCheck" ></label></td> <td colspan="3"></td> <td> <span>{{totalPrice}}元</span><button>结算</button></td> </tr> </table> <div class="empty" v-if="fruitList.length===0">🛒空空如也</div> </div> </div> <!-- 空车--> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> const app = new Vue({ el: '#app', data: { fruitList: [ { id: 1, imageName:'橙子', isChecked: true, num: 1, price: 6, }, { id: 2, imageName:'香蕉', isChecked: true, num: 1, price: 4, }, { id: 3, imageName:'猕猴桃', isChecked: true, num: 1, price: 5, } ] }, methods: { numAdd(item) { item.num++; }, numDel(item) { if (item.num > 1) { item.num--; }else if (item.num === 1) { this.fruitList = this.fruitList.filter(i => i.id !== item.id) } }, }, computed:{ totalPrice(){ return this.fruitList.reduce((total,item)=>{ return item.isChecked? total +(item.price*item.num) :total; },0) }, allCheck:{ get(){ if (this.fruitList.length === 0) return false return this.fruitList.every(item => item.isChecked) }, set(checked){ // 将所有商品的isChecked设为全选框的状态(true/false) this.fruitList.forEach(item => { item.isChecked = checked }) } } }, watch:{ fruitList:{ deep: true, handler(){ this.fruitList.forEach(item => { if (item.num<1|| isNaN(item.num)) { item.num = 1 } }) } } } }) </script> </body> </html>转成vue3
最新发布
10-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小赖同学啊

感谢上帝的投喂

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值