改过一个 2000 行的 Vue 2 组件:一个「资产详情」页面,同一套逻辑被拆散在
data、computed、methods、watch四个区块里。改一个「按部门筛选」的需求,要在五个地方来回跳,还得时刻记着哪些变量之间互相依赖。组件一大,Options API 的「按类型分区块」就开始拖后腿了。
这篇讲 Composition API(组合式 API,<script setup> 那套):它解决了什么、和 Options API 怎么对照、生命周期去哪了,以及最核心的「逻辑复用」为什么从 mixin 演进成了 composable。
一、解决什么问题:Options 的逻辑碎片化
Vue 2 的 Options API 把代码按类型分区:data 放数据、methods 放方法、watch 放监听、computed 放计算属性。功能少时很整齐,功能一多就出问题——同一个功能的代码散落四处:
// Options API:一个「部门筛选」功能的代码分布在四个区块
export default {
data() {
return { dept: '', deptList: [] } // ① 数据在这里
},
computed: {
filteredAssets() { // ③ 过滤逻辑在这里
return this.assets.filter((a) => a.dept === this.dept)
},
},
watch: {
dept(val) { // ④ 监听在这里
this.loadAssets(val)
},
},
methods: {
loadAssets(dept) { // ② 方法在那里
// 请求接口、更新 deptList
},
},
}组件只有几十行时无所谓,到几百上千行,「这个功能还牵扯哪些代码」要靠人脑跨区块拼。Composition API 换了个组织维度:按功能聚合。同一个功能的 ref、computed、watch 写在一起:
<script setup>
import { ref, computed, watch } from 'vue'
// 一个「部门筛选」功能,全部代码聚在一处
const dept = ref('')
const deptList = ref([])
const filteredAssets = computed(() =>
assets.value.filter((a) => a.dept === dept.value)
)
watch(dept, (val) => loadAssets(val))
</script>二、setup:组合式代码的入口
Vue 3 组件里,组合式代码在 setup() 中执行(<script setup> 是它的语法糖,编译时自动包一层)。几个关键点:
| 问题 | 答案 |
|---|---|
| 什么时候执行 | 组件实例创建时,早于 beforeCreate 之后、created 之前 |
能访问 this 吗 | 不能——setup 在 this 绑定之前运行,这也是老代码迁移时最常见的坑 |
| 返回什么 | 对象(选项式)或直接写 <script setup>,顶层声明的变量自动暴露给模板 |
| 做什么 | 定义响应式数据、computed、watch、生命周期钩子,把「这个功能的全部状态」聚在一起 |
三、ref / reactive / computed / watch:与 Options 对照
组合式 API 不是新概念,是把 Options 里的能力换了一种表达。对照表记起来很快:
| Options API | Composition API | 说明 |
|---|---|---|
data() | ref() / reactive() | 响应式数据 |
computed: {} | computed(() => ...) | 计算属性 |
watch: {} | watch(source, cb) | 监听;还多了 watchEffect |
methods: {} | 普通函数 | 就是函数,不需要 this |
beforeMount ~ beforeUnmount | onMounted() ~ onBeforeUnmount() | 生命周期钩子,onXxx 形式 |
mixins: [] | 直接调函数(composable) | 逻辑复用,见第四节 |
一个迁移例子,Options 写法 → 组合式写法:
// Options
export default {
data() { return { count: 0 } },
computed: { double() { return this.count * 2 } },
watch: { count(v) { console.log('count:', v) } },
mounted() { console.log('mounted') },
methods: { inc() { this.count++ } },
}<script setup>
import { ref, computed, watch, onMounted } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
watch(count, (v) => console.log('count:', v))
onMounted(() => console.log('mounted'))
const inc = () => count.value++
</script>注意到关键差异:组合式里普通函数直接调用(inc()),不需要 this——这也意味着逻辑可以随便抽出去复用,引出第四节。
四、逻辑复用:从 mixin 到 composable
这是 Composition API 最值钱的部分。Vue 2 复用逻辑靠 mixin:把 data/methods 塞进一个对象,组件 mixins: [xxx] 引进来。麻烦随之而来:
- 来源不明:组件里用了
this.x,但x到底来自组件还是哪个 mixin?得挨个翻。 - 命名冲突:组件和 mixin 都定义了
data()里的同名变量,合并策略打架,优先级靠背。 - 隐式依赖:改一个 mixin,所有引用它的组件全部受影响,改动不可控。
Vue 3 的 composable(组合式函数)换了个思路:逻辑就是普通函数,接收参数、返回需要的东西,组件里显式解构:
// 抽一个「跟踪鼠标位置」的逻辑,纯函数,无任何隐式依赖
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
const update = (e) => { x.value = e.clientX; y.value = e.clientY }
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y } // 显式返回,来源一目了然
}<script setup>
import { useMouse } from './useMouse'
// 谁提供的、返回了啥,全在眼前;取名随意,绝不冲突
const { x, y } = useMouse()
// composable 之间还能自由组合
const { result } = useDebounce(x, 300)
</script>命名约定:composable 函数一律 useXxx 开头——看到 use 就知道它是「带响应式状态的函数」,IDE 直接跳转定义,来源永远查得到。
五、Options 还是 Composition?什么时候用哪个
| 场景 | 建议 | 原因 |
|---|---|---|
| 新项目 / 新组件 | Composition(<script setup>) | Vue 3 主推,逻辑复用方便 |
| 几十行的小组件 | Options 也无妨 | 类型分区一眼到底,没必要上组合式 |
| 老项目渐进迁移 | 先加新功能用组合式,存量不动 | 两种 API 可以在同一组件共存 |
| 需要被多处复用的逻辑 | 必须抽 composable | mixin 的坑不该再踩 |
一句话:组件小怎么都行,组件一大、逻辑一多、要复用时,Composition API 是唯一舒服的答案。
响应式数据怎么工作的,见 Vue 响应式原理详解;组合式里 props/emit 怎么玩,见 组件通信全景;还没跑过 Vue 的,先看 快速入门;Vue 2 与 Vue 3 的版本差异,见 Vue 版本演进。
