Vue3 发布这么久,我接触过的团队里仍然有不少人停留在"会用
<script setup>写点东西"的阶段,说起 Options API 和 Composition API 的区别,只能答出"前者是选项对象、后者是函数式"这种表面话。这其实挺可惜的,因为这次演进的核心不是语法换了口味,而是组件逻辑组织方式的底层变革,牵涉到响应式系统的重新设计、复用模式的替代、甚至是 diff 算法的优化思路。这篇文章我想抛开面试八股,从一个真实项目从 Vue2 迁到 Vue3 的视角,把这条进化之路上的关键节点讲透。
先给这篇文章定个位:如果你是一个正在从 Vue2 往 Vue3 过渡的开发者,或者已经在用 Vue3 但总觉得 Composition API 用不顺手、不知道什么时候该用ref什么时候该用reactive、对组合式函数的设计边界很模糊的人,这篇文章值得你花二十分钟完整读一遍。全文围绕"为什么要进化""进化解决了什么""迁移时真正要关注的坑"三条线展开,没有粘贴官方文档的翻译腔,基本都是我在实际项目里验证过的结论。
1. Options API 在实际项目中的痛点:问题从哪来
1.1 代码不是按业务切分,而是按"类型"切分
先说个我在旧项目里反复遇到的场景。比如做一个商品列表页面,功能包含:搜索条件筛选、列表分页、选中项管理、批量操作、价格计算。Options API 的组织方式大家都很熟悉:
export default { data() { return { keyword: '', categoryId: null, page: 1, total: 0, list: [], selectedRows: [], loading: false }; }, computed: { filteredList() {}, totalPrice() {}, hasSelected() {} }, watch: { keyword() { this.page = 1; this.fetchList(); }, categoryId() { this.page = 1; this.fetchList(); }, 'page': 'fetchList' }, methods: { fetchList() {}, handleSearch() {}, handleReset() {}, handleSelectionChange() {}, handleBatchDelete() {}, handleExport() {} } };这个组件在文件里大概四百行。表面上看结构规整——响应式数据归 data,计算属性归 computed,方法归 methods。但如果你真的维护过这种组件,你会发现一个特别头疼的问题:你要理解"搜索"这个业务逻辑,必须同时在 data 里找 keyword 和 categoryId,在 methods 里找 handleSearch,在 watch 里找监听逻辑,在 computed 里找筛选结果。一个完整的功能点被物理切割成四块,散落在文件各个位置。
这就像把一个人的档案按"身高:xxx""体重:xxx""喜欢的食物:xxx"分门别类放进不同的抽屉里,当你想知道这个人完整的样子时,得把每个抽屉都翻一遍拼凑全貌。小组件还好,一旦组件超过三百行,这种割裂感会成倍放大。
1.2 mixin 复用:看似省事,追责却难
Options API 时代,跨组件复用逻辑的标准方案是 mixin。当时 Vue2 官方文档也是这么推荐的。但我用下来的感受是:mixin 适合"一次性缝合",不适合"长期维护"。
举一个典型例子。我当时封装了一个paginationMixin,里面包含了分页相关的 data、methods,大概是这样:
// paginationMixin.js export default { data() { return { page: 1, pageSize: 20, total: 0, loading: false }; }, methods: { handleSizeChange(size) { this.pageSize = size; this.page = 1; this.fetchList(); }, handleCurrentChange(page) { this.page = page; this.fetchList(); }, async fetchList() { this.loading = true; try { const res = await this.getListApi(this.buildParams()); this.list = res.data.records; this.total = Number(res.data.total); } finally { this.loading = false; } } } };看着挺方便,对吧?用的时候一行mixins: [paginationMixin]就全部继承。问题在于:当两个纯业务组件各自需要调整分页行为时,mixin 里的代码就开始"长出刺"。组件 A 需要翻页时额外埋点上报;组件 B 需要在翻页前做表单校验;组件 C 的列表接口不走分页,但需要复用 loading 逻辑。每一种变体都要在 mixin 里加参数、加判断分支,甚至有的团队直接在 mixin 里写if (this.someFlag) {},久而久之这个 mixin 变成了一个"把所有案件都往里面塞"的黑洞。
更要命的是"来源不明确"。我在一个老项目里看到过一个组件,页面模板里用了this.$refs、this.showDialog、this.formRules,你以为这些都是当前组件定义的,结果搜索代码发现其中一部分来自一个叫commonMixin的混入、一部分来自validateMixin、还有一部分来自组件的祖父级 mixin。当多个 mixin 合并后,你根本不知道某个数据或方法来自哪里,也不知道会不会被另一个 mixin 覆盖。探测一个字段的来龙去脉,只能靠全局搜索和跑代码验证。
1.3 组件膨胀之后,心智负担指数上升
Options API 还有一个被忽视的问题:代码行数的增长速度,远快于业务复杂度的增长速度。因为每个功能点拆开后,每个"碎片"都会附赠一层模板化的代码。比如加一个"是否展示价格"的开关,你需要:
- data 里加一行
showPrice: true - computed 里加一个
hasPricePermission - methods 里加一个
togglePrice - watch 里可能要加
showPrice() { this.$emit('price-changed') }
四条线同步加代码。如果是 Composition API 的写法,这个功能的代码天然能聚合到一段区域里,阅读时不需要跳跃。
我当时被一个几百行的组件折磨得受不了,把 console.log 打在不同生命周期里,反复对比 props 变化,就为了定位一个"重置按钮点了之后为什么有些字段没清空"的问题。最后发现是因为resetForm方法里漏掉了两个字段、而那两个字段又恰好被watch监听了,触发了一连串预期外行为。当组件大到一定程度,"副作用"在隐性地相互传染,而模板语法无法从设计层面阻止这种失控。
2. Composition API 的设计逻辑:setup 与响应式系统重排
2.1 setup 函数:组件逻辑的"统一入口"
Vue3 组合式 API 的核心载体是setup函数。它之所以被称为"组合式",核心思路是:允许你按照"逻辑关注点"来组织代码,而不是按照"选项类型"来切分代码。
对比感受一下,同样一个"搜索列表"功能在<script setup>里长这样:
<script setup> import { ref, reactive, computed, watch, onMounted } from 'vue'; import { searchGoods } from '@/api/goods'; // 搜索相关逻辑 const keyword = ref(''); const categoryId = ref(null); watch([keyword, categoryId], () => { page.value = 1; fetchList(); }); // 列表与分页 const page = ref(1); const total = ref(0); const list = ref([]); const loading = ref(false); const filteredList = computed(() => list.value.filter(...)); // 选中与批量操作 const selectedRows = ref([]); const hasSelected = computed(() => selectedRows.value.length > 0); const handleSelectionChange = (rows) => { selectedRows.value = rows; }; const handleBatchDelete = () => { // ... }; async function fetchList() { loading.value = true; try { const res = await searchGoods({ keyword: keyword.value, categoryId: categoryId.value, page: page.value }); list.value = res.data.records; total.value = Number(res.data.total); } finally { loading.value = false; } } onMounted(fetchList); </script>很明显,搜索、分页、批量操作这三块逻辑在物理位置上各自成块,维护时不需要再跨 data/computed/methods 跳来跳去。这种"按业务切分"的组织方式,是人脑更容易理解的方式——它符合"局部性原理",看代码时眼睛扫过去就完成了上下文加载。
2.2 ref 与 reactive 的本质区别和使用边界
聊到 Composition API 就绕不开ref和reactive的选择困惑。很多初学者问"这两个到底用哪个",我从实际经验给出结论:
reactive接受一个对象作为参数,返回一个深度响应式的代理对象。ref本质上是一个{ value: 原始值 }的包装对象。它的存在意义是解决"基本类型无法被 Proxy 直接代理"的问题。
所以我理解ref更准确的定位是"给基本类型和普通值穿一件外套,让它们在响应式系统中有一席之地"。reactive则面向对象数据。
但在真实项目里,你会发现用ref的情况远多于reactive。原因很实际:模板中可以自动解包ref,而reactive要用.属性名才能访问。这导致团队规范通常约定:
// 推荐 const list = ref([]); const page = ref(1); // 不推荐 const state = reactive({ list: [], page: 1 });为什么reactive在大型项目里容易出问题?我踩过一个坑:用reactive包裹了一个深层对象,在某个方法里用解构赋值提取了它的两个属性:
const state = reactive({ a: 1, b: 2 }); function foo() { const { a, b } = state; // 这个方法里对 a 的修改不会触发更新! }解构会切断响应式连接。如果你结构化的层级很深,这种隐性问题很难第一时间发现。用ref就没有这个问题,因为每次都是通过.value访问最外层对象,响应式连接始终存在。
不过reactive也不是一无是处。当你的数据天然是一个整体(比如表单对象)、且需要整体操作时,reactive的语义更自然:
const form = reactive({ username: '', password: '', remember: false }); function resetForm() { Object.assign(form, { username: '', password: '', remember: false }); }这两种用法的取舍,我的习惯是:基本类型和数组一律用ref,复杂对象看场景,如果整个对象经常被整体替换或重置,用reactive更省心;如果这个对象的字段会被频繁单独读取和修改,且散落在多个函数中,用ref更安全。
2.3 setup 的执行时机与生命周期映射
setup的执行时机是在组件实例被创建之后、beforeCreate之前。理解这一点很关键:在setup内部,this并不能指向组件实例。因为这时候组件实例还没有完全初始化完毕,this还没有绑定好,所以 Vue3 才不让你在setup里用this。
这个设计有时候会让从 Vue2 过来的人不习惯。我记得团队里有同事刚迁到 Vue3 时,在setup里写this.$route直接炸了,跑上来问为什么。我说你现在这个阶段拿不到this,要用useRoute()来拿路由信息。
生命周期钩子也是映射关系:
| Vue2 Options API | Vue3 Composition API |
|---|---|
beforeCreate | 不需要,直接写在setup里 |
created | 不需要,直接写在setup里 |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
errorCaptured | onErrorCaptured |
需要注意:setup里的代码实际上替代了原来created和beforeCreate阶段的工作。所以初始数据请求可以直接在setup顶层执行,效果和mounted里请求略有差异——setup执行时机更早,如果涉及到子组件 ref 获取,还是要等onMounted。原生 DOM 操作用onMounted是安全选择,因为此时视图已经挂载完成。
3. 组合式函数如何解决真实复用问题
3.1 从 mixin 到 useXxx:命名冲突与来源不明解药
在 Vue2 时代,我受 mixin 折磨最深的一点就是命名冲突是"沉默的"。两个 mixin 里都定义了handleSearch,后者会默默覆盖前者,页面行为变成"薛定谔的猫"。排查这种问题,只能靠读代码看到底 import 了哪些 mixin、顺序如何。
Vue3 的组合式函数从根上解决了这个问题。因为组合式函数是普通函数,变量名在使用时完全由你决定,不存在"自动合并"这回事:
// usePagination.js import { ref } from 'vue'; export function usePagination() { const page = ref(1); const total = ref(0); const pageSize = ref(20); function handleSizeChange(size) { pageSize.value = size; page.value = 1; } function handleCurrentChange(p) { page.value = p; } return { page, total, pageSize, handleSizeChange, handleCurrentChange }; }在组件中使用时:
const { page, total, pageSize } = usePagination();就算两个不同的组合式函数都返回了page,你在使用层也可以给它们起不同的名字,从根本上避免了 mixin 那种"隐式合并"的覆盖问题。更重要的是,组合式函数的所有数据和方法都来自函数的 return 值,代码是从哪来的、哪些作用域可见,是一目了然的。
3.2 组合式函数的组织:一个功能一个函数
组合式函数的设计思路,简单说就是:把一个完整的功能闭环封装起来,外部只暴露必要的数据和操作。我在带团队时总结了一条实践规范:
如果一个功能点的状态 + 操作超过 3 个变量,就应该考虑抽成组合式函数。
举个例子。我项目里有个通用需求:列表接口需要支持搜索关键字、分页、刷新、重置。传统 Options API 写法散落各处;Vue3 可以这样封装:
// useListPage.js import { ref, watch } from 'vue'; export function useListPage(fetcher, options = {}) { const list = ref([]); const loading = ref(false); const total = ref(0); const page = ref(1); const pageSize = ref(options.pageSize ?? 20); const filters = ref(options.filters ?? {}); async function fetchList() { loading.value = true; try { const res = await fetcher({ ...filters.value, page: page.value, pageSize: pageSize.value }); list.value = res.data.records; total.value = Number(res.data.total); } finally { loading.value = false; } } function reset() { page.value = 1; filters.value = {}; fetchList(); } watch([page, pageSize, filters], fetchList, { deep: true }); return { list, loading, total, page, pageSize, filters, fetchList, reset }; }这个函数在项目里谁要用,引入一行就完事。组合式函数的 return 对象就是对外契约,使用者可以按需解构,且不担心命名冲突。在实际复用场景里,这种模式比 mixin 至少省一半排查时间。
3.3 在 defineProps、defineEmits 下的父子组件交互
Vue3 的<script setup>中,父子组件交互的体验也有不少变化。最直观的是defineProps和defineEmits这两个编译宏——它们不需要 import,直接在<script setup>顶层使用即可。
一个典型的子组件交互示例:
<script setup> const props = defineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: '请输入内容' } }); const emit = defineEmits(['update:modelValue', 'focus', 'blur']); function handleInput(e) { emit('update:modelValue', e.target.value); } </script>这里有个细节值得注意:defineProps返回的 props 对象在<script setup>中是响应式的,但你不能再给 props 重新赋值。很多人从 Vue2 过来习惯直接this.someProp = x来改 prop,在 Vue3 里直接警告,正确的做法永远是 emit 事件让父组件改。
v-model的机制也变了。Vue2 的value + input被替换成modelValue + update:modelValue。如果你在项目里兼容过 Vue2,会发现写v-model的体验其实更统一了,支持v-model语法糖和多个v-model:xxx的绑定,这比 Vue2 灵活很多。
4. 从 Options 迁移到 Composition 的工程化路线
4.1 大型项目迁移,先定顺序再动手
很多人问"我手上有一个几百个组件的 Vue2 老项目,怎么迁到 Vue3?"这个问题我实际做过,我的经验是:别想着一步到位全部重写。建议的迁移顺序是:
先升级到 Vue 2.7。Vue 2.7 是官方发布的一个兼容版本,它把 Composition API 带回了 Vue2 生态,让你可以在不改变构建工具链的前提下,先熟悉 Composition API 的写法。
构建工具从 webpack 换到 Vite。Vite 基于 ES module,冷启动速度快不是一点半点。但这一步要注意公司基础设施是否支持,如果项目里有大量依赖 webpack 的插件(比如传统 qiankun 微前端方案),迁移成本可能比较高。
组件逐个迁移。从公共组件、基础组件开始,因为它们被大量复用,迁移收益最高、暴露问题也最集中。
最后处理路由和状态管理。Vue Router 4 和 Pinia 都需要配合 Vue3,这两个一般作为收尾。
我见过失败的仓库迁移案例,主因都是同时改版本 + 改架构 + 改状态管理 + 改 UI 库,四座大山一起压过来,bug 铺天盖地。渐进式迁移虽然周期长,但每一小步都能通过回滚控制风险。
4.2<script setup>语法糖,迁移中必须掌握的写法
严格来说,Vue3.2 之前写 Composition API 需要在<script>里暴露 setup 函数并 return,那体验其实是比较啰嗦的:
<script> export default { setup() { const count = ref(0); function inc() { count.value++; } return { count, inc }; } }; </script>每个变量都要在 return 里写一遍,体感接近"为了响应式而被迫把作用域打平"。<script setup>是官方语法糖,它把"自动暴露顶层绑定"这件事做掉了:
<script setup> const count = ref(0); function inc() { count.value++; } </script>模板里可以直接用count和inc,不用 return。这个改变极大降低了 Composition API 的上手成本。如果你要迁移或新写 Vue3 项目,我强烈建议直接用<script setup>,它不必考虑 setup 返回的繁琐细节,代码量直接减三分之一。
要注意的一点是:<script setup>里不能使用普通的export default,因为它本身就承担了组件定义的作用。如果确实需要和 Options API 混用(比如想用某些 Options API 特有的选项),可以在同一个 SFC 文件里加一个普通的<script>块。
4.3 Vue 2.7 的桥梁作用与迁移陷阱
Vue 2.7 在 Options API 和 Composition API 之间搭了一座桥,我实际用下来觉得它最大的价值是:让老团队可以先在原有项目中试用 Composition API,不需要立即把构建体系推倒重来。但它在细节上和真正的 Vue3 还有差异,迁移时需要注意几点。
一是ref在模板中的解包行为不同。Vue 2.7 里,如果在模板中直接访问ref对象本身,会有兼容性问题;Vue3 则总是解包到.value。
二是依赖收集的时机和深度不同。我遇到过 Vue2.7 中watch一个ref数组,修改arr[0]不触发更新的问题,在 Vue3 中是正常的。这类差异性能通过升级解决,但会导致排查时困惑。
三是很多 Vue2 生态库在 Vue3 下没有对应版本。这是制约迁移的硬性问题,比如一些老牌 UI 库的 Vue3 版本功能不完整,图表库的 Vue3 封装要新增维护成本。所以迁移前必须先盘点项目中所有 npm 依赖的社区支持情况,不要先写代码再发现某个核心库停更了。
5. 响应式与渲染性能的进化:diff 算法背后的优化逻辑
5.1 静态提升与 patchFlag:编译器层面的自动优化
很多人知道 Vue3 虚拟 DOM diff 性能比 Vue2 好,但说不出好在哪里。这里要澄清:Vue3 的 diff 性能优化,主要不是靠运行时逻辑"写得更快",而是靠编译阶段把静态信息和动态信息分离,极大地减少了需要对比的节点数。
看一个最简单的模板:
<div> <span>静态文字</span> <span>{{ msg }}</span> </div>Vue2 编译后的渲染函数,每次重新渲染时都会生成完整的新虚拟 DOM,然后完整地 diff。Vue3 的编译器做了两件事:
一是静态提升(hoistStatic)。编译器识别出<span>静态文字</span>这个节点永远不会改变,于是把它提升到渲染函数外部创建一次,之后每次渲染直接复用同一个 vnode,省去了重复创建的消耗。
二是patchFlag(补丁标记)。像<span>{{ msg }}</span>这种动态节点,编译时会打上1(代表 TEXT 动态文本)之类的标记。diff 时 Vue3 就知道只有这个节点需要对比更新,不需要再递归遍历整个子树。
这类优化让 Vue3 在管理大型列表、复杂页面时性能明显优于 Vue2,关键是这些优化是默认开启、自动作用的,开发者根本不需要手写任何东西。
5.2 key 的作用与最长递增子序列:diff 算法的核心逻辑
Vue3 在 diff 列表时的另一个关键优化是使用最长递增子序列算法,这个点也是面试的高频题。
在 Keyed Children diff 过程中,Vue 需要判断新旧列表中的节点关系,从而安排插入、删除、移动操作。Vue2 在同级比较时使用双端对比,但遇到节点复用错位时,可能会多做几次移动操作。Vue3 引入了最长递增子序列思想:在确定了新列表在旧列表中的相对位置之后,通过查找最长递增子序列,计算出哪些节点不需要移动,只需要移动其余节点,从而把移动操作次数降到最低。
举个例子,旧列表[A, B, C, D, E],新列表[C, A, B, D, E]。Vue3 会找到[A, B, D, E]或者[C, D, E]这类递增序列,确定哪些节点不需要动,尽量只移动 C 或 A。算法复杂度是O(nlogn),比 Vue2 更容易在大型列表下的性能表现更优。
不过普通开发者在实际开发中不太需要手写这个算法,理解它的意义在于:编写列表渲染时,给每个元素加一个稳定且唯一的 key,能最大限度地利用这个算法的优势。如果不加 key,Vue 会走"就地复用"策略,这可能导致组件状态错乱(比如输入框的值串位),这是我在实际项目中遇到过的,排查了很久才定位到是 key 重复的问题。
5.3 性能不是自动来的:模板写法影响优化效果
虽然 Vue3 在编译阶段做了大量优化,但这些优化只有在模板能被静态分析时才有效。如果你大量滥用动态绑定、整块模板使用v-html、或者把组件渲染逻辑全部塞进函数式组件里,优化效果会大打折扣。
我在项目里见过一个反面教材:把整个列表区域都用v-html输出,导致 Vue 无法复用 DOM,每次数据更新都重建整块列表,页面卡到爆。Vue3 的优化思路是"编译器尽力,但开发者别拆台"。合理的做法是:动态部分保持小粒度,静态部分交给编译器提升;列表渲染保证 key 稳定;复杂更新场景用computed避免不必要的重复渲染。
这里还有个容易被忽略的点:shallowRef/shallowReactive可以在深度响应式开销过大的场景下手动降级。比如一个只读的巨大配置对象,根本不需要深度代理,用shallowReactive就能避免不必要的 Proxy 层级,提升性能。不过这类优化属于后期手段,不要在项目初期引入,避免带来不必要的理解成本。
6. 如果真的要从头开始:Vite 搭建 Vue3 项目的流程复盘
最后补充一个实操内容,因为不少读者搜"vue3"其实是为了搭一个能跑的项目。Vite 作为 Vue3 官方推荐的构建工具,搭建体验比我早期用 webpack 配 Vue3 舒服太多了。我把一个最小可行项目的搭建过程复盘在这里。
6.1 创建项目与目录结构设计
使用 npm 创建:
npm create vite@latest my-vue3-app -- --template vueVite 会生成一个极简的骨架项目,包含src/main.js、src/App.vue、vite.config.js等核心文件。这里我建议马上做的两件事是:
- 配置
@路径别名。在vite.config.js中加入:
import { defineConfig } from 'vite'; import path from 'path'; export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, 'src') } } });没这个配置之前,写../components/xxx.vue这种相对路径长到让人怀疑人生。
- 配置开发代理,解决本地开发跨域问题:
server: { proxy: { '/api': { target: 'http://your-api-server.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }6.2 从模板语法到组件交互:一个最小示例
初始化完项目,跑起来之后,大多数人第一件想做的事是:写一个列表页,请求接口、渲染数据、管理加载状态。这里我给出一段可以在项目里直接跑的模板:
<template> <div class="goods-page"> <input v-model="keyword" placeholder="输入关键词" /> <button @click="handleSearch">搜索</button> <div v-if="loading">加载中...</div> <ul v-else> <li v-for="item in list" :key="item.id"> {{ item.name }} - {{ item.price }} </li> </ul> </div> </template> <script setup> import { ref, watch } from 'vue'; import { searchGoods } from '@/api/goods'; const keyword = ref(''); const list = ref([]); const loading = ref(false); async function fetchList() { loading.value = true; try { const res = await searchGoods({ keyword: keyword.value }); list.value = res.data; } finally { loading.value = false; } } function handleSearch() { fetchList(); } // 输入防抖后搜索 watch(keyword, (val) => { clearTimeout(timer); timer = setTimeout(fetchList, 500); }); </script>这个示例虽然简单,但集中展示了 Vue3 日常开发中最常用的几个点:ref创建响应式数据、v-model双向绑定、watch 监听变化、模板中的自动解包、组合式函数(如果把 fetchList 和 keyword 抽出去,就自然变成一个 useSearch 组合式函数)。
7. 写在最后:Composition API 不是银弹,但它确实更适合复杂逻辑
我在团队推广 Composition API 时,遇到的最大阻力不是技术,而是习惯。很多同事在 Options API 里写了三四年,肌肉记忆已经把响应式数据放在 data 里、方法放在 methods 里,突然让他们全混在一个setup里,很不适应。这种感受我能理解,但我想说:Composition API 的定位不是"取代所有 Options API 场景",而是为复杂逻辑提供更合理的组织方式。如果你的组件不超过一两百行,Options API 完全没问题;但当一个组件的逻辑开始变复杂、需要复用、需要测试时,Composition API 的收益就会越来越明显。
最后分享一个我自己的经验:迁移不是二选一的零和游戏。在我负责的一个中大型后台项目中,我采取的是"新组件用 Composition API,老组件保持 Options API 不动,公共逻辑逐步抽成组合式函数"的策略。三个月后,新需求几乎都跑在 Composition API 这条线上,老代码也肉眼可见地沉淀出了十多个可复用的useXxx函数,整个团队对新语法的接受度比预期快很多。如果你的团队正处于观望阶段,我建议你挑一个边缘模块先试点,跑通之后再往核心模块推,这样既控制风险,又能攒下实战经验。至少在 Vue3 这个版本的生命周期内,Composition API 一定是主航道,早一天上手,后面重构时就能少踩一个坑。