Vue Function-based API RFC
我花了三天时间死磕Vue Function-based API,最后决定这么用——真香,但别急着全盘重写!
2019年那个深夜,我差点以为Vue要变天
你猜怎么着?
2019年底,我窝在工位上啃着面包,刷到Vue 3的Function-based API RFC。
第一反应——这啥玩意儿?!
第二反应——完了,Vue要变成React了?
那会儿网上吵翻了天。有人说这是Vue自废武功,有人大喊“抄React抄得真香”,还有一堆人在评论区互骂。
我呢,有个臭毛病:光看理论睡不着,必须上手怼一遍。
于是那个周末,我翻出了公司后台管理系统里一个不疼不痒的功能模块,装上了刚出炉的 vue-function-api(后来改名 composition-api),版本号记得死死的:vue-function-api@2.0.1,配的是我那老掉牙的 Vue 2.5.17 项目。
然后就是三天的心跳、踩坑、真香、再踩坑、再真香……
说到Vue 2的逻辑复用,我真是受够了!
你想想啊,我们团队维护着一个后台系统,上百个业务组件,各种列表、表单、详情页。
两个组件都要做搜索、分页、排序,还得在页面一打开就自动拉数据。
最开始,我用 Mixin。
三个Mixin一混,模板里的 search 方法到底是谁提供的?查半天定位累成狗。
最离谱的一次:同事写了个mixin叫 listHandler,我写了个 listHandle——少了个r。两个mixin一起用,直接冲突,整个页面崩了。
created 钩子里十几个Mixin的执行顺序?基本靠猜,猜不中就花半天调试。
后来试过 HOC 和 Renderless 组件。
HOC包装一层组件实例,性能还行,但props一层层往下传,调试时得翻好几层组件结构,脑子不够用。
Renderless更尴尬:为了传递一个函数,slot scope里塞满了 {{ slotProps }},代码一多连自己都看晕了,改个逻辑得从头捋到尾。
所以当我看到RFC里那句“逻辑组合与复用”时,第一反应就是——
救星终于来了!
我的第一个 setup 函数,清爽到飞起
装好插件后,我最想验证一件事:能不能把原来一个列表页的 mounted、data、methods 全都塞进一个 setup 里?
代码长这样:
import { value, computed, watch, onMounted } from 'vue-function-api'
export default {
name: 'UserList',
props: ['query'],
setup(props, ctx) {
const list = value([])
const loading = value(false)
const currentPage = value(1)
const totalPages = computed(() => Math.ceil(list.value.length / 10))
const fetchList = async () => {
loading.value = true
const res = await api.getUsers({ page: currentPage.value })
list.value = res.data
loading.value = false
}
watch(() => props.query, (newQuery) => {
currentPage.value = 1
fetchList()
})
onMounted(fetchList)
return {
list,
loading,
currentPage,
totalPages,
fetchList
}
}
}第一眼——太清爽了!
所有和列表相关的逻辑都待在一个函数里,不再分散到 data、methods、created 里乱跑。
但跑起来……呵呵,坑来了。
踩坑实录:每个坑我都替你跳了
1. `.value` 这个点,真的烦
这个坑估计每个用过Composition API的人都被绊过。
value(0) 返回的是一个带 .value 属性的响应式对象。在 template 里直接用 {{ count }} 没问题,但 在 setup 内部,你必须写 count.value 才能读写。
我写 fetchList 时直接写了 currentPage++,结果页面死活翻不动——“你改了啥?class啊?不是响应式?拜拜了您嘞。”
改成 currentPage.value++ 就好了。
坦白说,每次多敲几个字符—— .value ——都挺烦的。但后来我想通了:这是Vue团队在设计上对原生类型的妥协方案。到了Vue 3,ref 也是一样的设计,不过加上了 isRef 和 unref 辅助函数,稍微好一丢丢。
2. setup 里没有 this,我的$router呢?
以前写 methods 时,我天天用 this.$router、this.$store 这些实例属性,顺手得不行。
但到了 setup 里,this 是 undefined!第一次用我就懵了:我要怎么拿到路由?怎么拿vuex?
后来得用 getCurrentInstance() 或者从参数里解构。
比如在 vue-function-api 里:
import { getContext } from 'vue-function-api'
setup(props, ctx) {
const { $router, $store } = getContext()
}但注意—— getContext 只能在 setup 中同步调用。你在定时器里调它?直接报错!
跟React Hooks的规则一模一样:不能条件调用、不能异步调用。
3. 生命周期钩子的坑:想偷偷懒?没门!
onMounted、onBeforeUnmount 这些函数,必须直接在 setup 顶层同步注册。
不能包在 if 里,不能放在 setTimeout 里。
一开始我想按条件注册 onBeforeUnmount ——比如说,用户是管理员才在离开时清理资源。结果Vue直接抛出:onMounted is called when there is no active component instance。
被教育了。后来我改成通过一个 watch 来决定是否清理。
4. 类型推导,这个真香
这一点我必须要夸!
我们项目里有一部分业务逻辑用 TypeScript 重写过,但 Vue 2 搭配 TypeScript 的体验,谁用谁知道:装饰器满天飞,Vue.extend 写起来像受刑。
而 Composition API 天然支持类型推导——ref 一写,整个链路的类型都稳了。
我用 vue-function-api 时试了一下 computed 的返回类型和 ref 的组合,比 data 那种对象字面量类型推导准得多。妈妈再也不用担心我类型写错了。
和 React Hooks 的对比:它们根本不是一个物种
测试完后,很多同事跑来问我:“这不就是 React Hooks 的 copy 吗?那为啥不用 React?”
我每次都得跟他们解释一遍——用过才知道,差远了。
先看对比:
| 对比维度 | React Hooks | Vue Composition API |
|---------|------------|-------------------|
| 状态创建 | useState 返回 [value, setter] | ref() 或 reactive() 返回响应式对象 |
| 状态更新 | 调用 setter 触发重新渲染 | 直接赋值 .value = newVal,自动触发 |
| 副作用管理 | useEffect,需要手动声明依赖 | watch、watchEffect,自动追踪依赖 |
| 渲染机制 | 每次状态更新都重新执行整个函数组件 | setup 只执行一次,后续通过代理的 getter/setter 追踪 |
| 与模板关系 | JSX 嵌入逻辑 | 模板依然单独声明,逻辑在 setup 中返回 |
| 独立使用能力 | 模式可移植,但需要宿主框架重新实现 | 响应式 API(reactive、computed 等)可直接作为状态管理库使用 |
看到区别没?
React Hooks 的本质是 基于函数执行顺序的闭包机制。每次渲染都重新执行所有Hooks,如果依赖数组写错,你就会拿到闭包里的旧值,bug莫名其妙。所以要记住 useCallback、useMemo 的依赖项,不然就等着哭。
而Vue的Composition API呢? 基于响应式代理。setup只执行一次,后续所有变更靠 Proxy 的 getter/setter 追踪。你不需要担心闭包过期——每读取 count.value,都是最新的值。写起来心理负担小很多。
尤大在说过一句话我特别认同:Vue 走的是 value 增强 路线,React 是 function 增强 路线。
前者把值变成响应式的,随取随用;后者每次渲染都生成一个新的快照,依赖一致性需要手动保障。
一个省心,一个要靠头脑清醒。你选哪个?
真正的宝贝不是 setup,是 Reactivity API
我花了2天时间,把 reactive、computed、watchEffect 抽出来,写了一个独立的 store:
// store.js
import { reactive, computed } from 'vue-function-api'
const state = reactive({
users: [],
page: 1
})
export function useStore() {
const total = computed(() => state.users.length)
const load = (page) => fetch(`/api/users?page=${page}`).then(r => r.json()).then(data => state.users = data)
return { state, total, load }
}然后在任何组件里 import 这个函数,直接用。不需要 Vuex,不需要 Provider。
reactive 返回的对象本身是响应式的,改了自动触发关联组件的更新。
这比 React Hooks + Context 的方案轻了不是一星半点,而且不需要任何高阶组件包裹。
这就是RFC里说的 Advanced Reactivity API ——它脱离了组件的约束,可以作为独立的响应式数据流方案。理论上,你可以把它用在任何JS环境里,配合小程序或Node.js做状态存储,都没问题。
我用它写过一个小玩具,像 Cycle.js 一样用 watchEffect 和 map 做数据流编排。居然跑通了!
不过那是另一个故事了。
回到现实:项目里到底该不该用?
你现在问我:2025年了,Vue 3 早已是正式版,还值不值得用?
我的答案:如果你还在用 Vue 2,而且不想一下子跳 Vue 3,可以用 @vue/composition-api 插件先过渡。vue@2.7 已经内置了 Composition API,官方维护,稳得很。
对已有项目,只在新模块里用,千万不要大规模重构老代码。Mixin 虽然有毛病,但稳定运行的项目,没必要为了新 API 而重写。你的业务不会因为换了个写法就变好。
新项目优先选 Vue 3 + Composition API,因为:
- **类型推导好** —— 团队里写 TypeScript 再也不痛苦了
- **tree shaking 友好** —— 只打包你 import 的函数,不用整个Vue都搬进来
- **逻辑组合方式灵活** —— 能写出像积木一样的逻辑
我个人的习惯是:把复杂业务组件的逻辑提取到 useXxx 函数里。
比如我写个采购订单组件:
- 表单校验 → `useForm`
- 列表加载 → `useList`
- 打印预览 → `usePrint`
然后组件里就变成了:
setup() {
const { form, rules, onSubmit } = useForm(props)
const { list, loadMore } = useList(props)
const { print } = usePrint()
return { form, rules, onSubmit, list, loadMore, print }
}读组件就像读目录一样清楚,每个 hook 的职责一目了然。
最后说几句真心话
Vue 3 的 Composition API 不是银弹。
它依然要和 template 配合,不像 JSX 那样完全函数化。对于简单的组件,setup 甚至比 data + methods 写起来还啰嗦——一个按钮加个数字计数器,写 setup 确实比 Options API 多几行。
但一旦组件逻辑复杂到需要复用或拆解,它的好处就出来了,你会忍不住喊一声:真香。
我对这个 RFC 的评价是:方向对了,实现也不错,该踩的坑我也替你们踩了。
关于 RFC 本身,当年那篇 vue-function-api 已经归档成了历史。现在最新的官方文档在 composition-api.vuejs.org,内容做了大量修订。当初我初读 RFC 时觉得某些 API 设计有争议(比如 value vs ref 的命名,还有 watch 的参数顺序),到正式版都改了。所以,RFC 的阶段就是给你提意见的,别一看到提案就觉得是最终形态。
慢慢来,我最开始也只敢拿一个模块试水。路是一步步走的,但方向对了,跑起来才带劲儿!
如果你觉得 Vue 2 的 mixin 让人头疼,Composition API 是你一直在等的解药。但别激动到把整个项目重写,谨慎地试试水,你会感谢那个周末加班的自己。
读者评论 2