Skip to content

改过一个 2000 行的 Vue 2 组件:一个「资产详情」页面,同一套逻辑被拆散在 datacomputedmethodswatch 四个区块里。改一个「按部门筛选」的需求,要在五个地方来回跳,还得时刻记着哪些变量之间互相依赖。组件一大,Options API 的「按类型分区块」就开始拖后腿了。

这篇讲 Composition API(组合式 API,<script setup> 那套):它解决了什么、和 Options API 怎么对照、生命周期去哪了,以及最核心的「逻辑复用」为什么从 mixin 演进成了 composable。

一、解决什么问题:Options 的逻辑碎片化

Vue 2 的 Options API 把代码按类型分区:data 放数据、methods 放方法、watch 放监听、computed 放计算属性。功能少时很整齐,功能一多就出问题——同一个功能的代码散落四处

javascript
// 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 换了个组织维度:按功能聚合。同一个功能的 refcomputedwatch 写在一起:

html
<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不能——setupthis 绑定之前运行,这也是老代码迁移时最常见的坑
返回什么对象(选项式)或直接写 <script setup>,顶层声明的变量自动暴露给模板
做什么定义响应式数据、computed、watch、生命周期钩子,把「这个功能的全部状态」聚在一起

三、ref / reactive / computed / watch:与 Options 对照

组合式 API 不是新概念,是把 Options 里的能力换了一种表达。对照表记起来很快:

Options APIComposition API说明
data()ref() / reactive()响应式数据
computed: {}computed(() => ...)计算属性
watch: {}watch(source, cb)监听;还多了 watchEffect
methods: {}普通函数就是函数,不需要 this
beforeMount ~ beforeUnmountonMounted() ~ onBeforeUnmount()生命周期钩子,onXxx 形式
mixins: []直接调函数(composable)逻辑复用,见第四节

一个迁移例子,Options 写法 → 组合式写法:

javascript
// 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++ } },
}
html
<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(组合式函数)换了个思路:逻辑就是普通函数,接收参数、返回需要的东西,组件里显式解构:

javascript
// 抽一个「跟踪鼠标位置」的逻辑,纯函数,无任何隐式依赖
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 } // 显式返回,来源一目了然
}
html
<script setup>
import { useMouse } from './useMouse'

// 谁提供的、返回了啥,全在眼前;取名随意,绝不冲突
const { x, y } = useMouse()

// composable 之间还能自由组合
const { result } = useDebounce(x, 300)
</script>

Mixin 与 Composable 逻辑复用对比

命名约定:composable 函数一律 useXxx 开头——看到 use 就知道它是「带响应式状态的函数」,IDE 直接跳转定义,来源永远查得到。

五、Options 还是 Composition?什么时候用哪个

场景建议原因
新项目 / 新组件Composition<script setup>Vue 3 主推,逻辑复用方便
几十行的小组件Options 也无妨类型分区一眼到底,没必要上组合式
老项目渐进迁移先加新功能用组合式,存量不动两种 API 可以在同一组件共存
需要被多处复用的逻辑必须抽 composablemixin 的坑不该再踩

一句话:组件小怎么都行,组件一大、逻辑一多、要复用时,Composition API 是唯一舒服的答案

响应式数据怎么工作的,见 Vue 响应式原理详解;组合式里 props/emit 怎么玩,见 组件通信全景;还没跑过 Vue 的,先看 快速入门;Vue 2 与 Vue 3 的版本差异,见 Vue 版本演进