ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue3实战入门:从响应式原理到Composition API踩坑笔记

Vue3实战入门:从响应式原理到Composition API踩坑笔记 这段时间我在折腾公司的后台管理系统顺手把 Vue3 从头学了一遍。前两天有同事问我“Vue3 到底怎么入门”我反而不太答得上来——因为学过的东西如果不整理很快又变成一团浆糊。这篇笔记就是我从安装环境、创建第一个项目到看懂 ref 和 reactive、实际改出几个页面的全过程记录。不打算写成教科书更多是记录我作为一个普通前端在面对 Vue3 时的真实操作、踩坑和最终想明白的问题。如果你也是那种不喜欢空谈概念、喜欢直接动手的类型这篇笔记的节奏应该比较对味。1. 重新认识 Vue3它到底改了什么1.1 从 Vue2 带过来的“惯性”是最需要警惕的有一说一我刚开始学 Vue3 时最大的阻力不是语法而是脑子里那套 Vue2 的思维定势。比如写 data、写 methods、写 computed这些 Options API 的写法在 Vue3 里依然能用但框架已经在鼓励你换一种组织代码的方式了。如果只是照着旧习惯写学到的 Vue3 其实是“披着 Vue3 外衣的 Vue2”过阵子该换的还是得换。先把最底层的差异聊透。Vue2 的响应式靠的是 Object.defineProperty对属性做拦截Vue3 换成了 ES6 的 Proxy能够直接代理整个对象。这意味着之前很多“天然做不到”的操作比如给对象动态添加新属性、通过数组下标改值在 Vue3 里都变成了常规操作。加上 Proxy 在代理嵌套对象时不需要像 Vue2 那样递归地把每一层都 defineProperty 一遍性能上也有明显改善依赖收集和更新触发都更精准。另一个容易被忽略的变化是模板和组件的灵活性。Vue3 支持了 Fragment组件可以拥有多个根节点不用再在外面包一层无意义的 div。新增的 Teleport 能把弹窗、下拉这类内容直接传送到 body 下面避免被父组件的 overflow 或 z-index 影响Suspense 则让处理异步组件时的加载态书写得更直观。这些特性不是花架子在实际做中后台项目时Teleport 对弹窗类组件的帮助尤其大我在 Vue2 时代经常要为弹窗的层级问题头疼。1.2 Options API 和 Composition API我的选择标准先做一个最直白的区分Options API 把同一个功能的数据、方法、计算属性拆散到 data、computed、methods、watch 这些区块里组件一复杂你就得在同一份文件里来回跳逻辑稍微多几个关联阅读成本立刻上来。Composition API 允许你按“这个功能需要哪些状态、哪些方法”来组织代码把相关逻辑放在一起再用自定义函数抽出来复用。如果用表格对比一下会更清楚维度Options APIComposition API代码组织按选项类型强行分块按功能模块自由组合逻辑复用依赖 mixin命名冲突难避免自定义组合式函数天然隔离适用场景简单组件、原型快速验证复杂组件、跨组件逻辑复用TypeScript 支持有支持但不顺滑类型推导更自然我的实际感受是几十行的小组件用 Options API 反而直观所有东西摆在那儿一目了然但一旦超过两三百行或者组件里有多个独立业务模块Composition API 的优势就非常明显。后者的精髓不只是把 data 和 methods 放到一个 setup 里而是你可以把一个完整的功能块抽成函数例如把“表单校验逻辑”做成 useFormValidation把“表格加载逻辑”做成 useTableData组件里只留编排代码。学习路径上我给的建议很简单先看懂 Options API因为存量项目里大量代码还是它你进任何旧团队都可能要维护然后尽快切到 Composition API因为新项目、新文档、招聘方要求的都是它。我在自己的项目里已经几乎全部用script setup写法了后面第 4 章再细说。2. 开发环境搭建与第一个项目2.1 为什么我直接选了 Vite在 Vue3 刚发布的那段时间官方推荐还是 Vue CLI 配合 webpack。但现在再开新项目我基本无脑选 Vite。原因很直接它开发服务器的启动速度是秒级的改动后的热更新也是毫秒级反馈。对一个需要反复调样式、改组件的人来说这个体验差距太重要了等到项目大了你会更感谢它。Vite 之所以快是因为开发模式下它没有把整个项目打包起来。传统 webpack 是把所有模块编译成 bundle 再启动服务器项目越大启动越慢Vite 利用浏览器原生 ES Module 的能力启动时只做模块的按需转换浏览器请求哪个模块它才编译哪个模块。这种思路在开发阶段省掉了一大堆无用编译。这里要提醒一个容易踩的坑Vite 虽然快但要求 Node.js 版本不能太低。官方对 Node 18 的支持比较完善我实际用的版本是 20运行node -v先确认一下环境。如果版本太旧npm create vuelatest可能会报错或者生成项目后依赖安装阶段出现兼容性问题。2.2 初始化项目的完整过程创建 Vue3 项目现在最标准的方式是执行npm create vuelatest这个命令会进入交互式引导过程中会依次询问是否使用 TypeScript、JSX、Vue Router、Pinia、Vitest、ESLint、Prettier 等。新手容易在这里犯选择困难症。我的建议是第一次练手时按需选择 TypeScript 和 Vue Router 就够了其他可以先不勾等理解了再手动加。TypeScript 选上不是因为 Vue2 时代那种“工程化装样子”而是 Vue3 的类型推导能力已经做得很好选上之后写代码时编辑器会给你很多即时反馈这本身就是最好的学习辅助。项目创建完按提示进入目录并启动cd vue3-project npm install npm run dev启动成功后在浏览器打开localhost:5173能看到官方默认的演示首页。这个页面很值得花十分钟点一遍里面的按钮实际上演示了count在 Template、Options API、Composition API 三种写法下的实现算是一个免费的对比学习入口。还有个小细节脚手架默认生成的 vite.config.js 里开发端口是 5173如果被占用Vite 会自动往上涨一个端口。如果你需要固定端口可以在配置里显式写export default defineConfig({ server: { port: 5173, strictPort: true } })2.3 Edge 浏览器里遇到的一个奇怪问题我在这台 Windows 电脑上开发时遇到过 Vue3 项目在 Edge 浏览器里出现窗口最小化后无法正常恢复的情况准确说是页面最小化后点击任务栏图标窗口会弹一下又自动收回去只能强制结束任务再重开。一开始我以为是 Edge 本身的 bug后来发现这是开发环境下Edge 的“睡眠标签页”功能把后台标签页挂起导致 Vite 建立的 WebSocket 长连接被回收页面状态和热更新通道一起卡死了。排查思路是这样的先在任务管理器里看 Edge 的内存占用发现该标签页的 CPU 和网络活动都归零基本确认被挂起。然后在 Edge 设置里把“让标签页进入睡眠状态以提高性能”关闭同时把该站点加入“永不睡眠”的例外列表问题就不再复现。这个现象在生产环境的正常浏览场景里比较少见因为生产环境走的是静态文件加载不太依赖长连接但开发调试时热更新基本离不开 WebSocket所以遇到类似症状不要急着怀疑框架先查浏览器标签节流策略。3. 响应式核心ref、reactive 与常见陷阱3.1 ref 为什么承担了“万能对象”的角色热搜词里有一句很形象的总结uni-app 和 Vue3 里ref 是万能对象。这个说法不算夸张。ref 的核心作用是用一个对象包裹任意值让基础类型的变量也能具备响应式能力——你修改count.value所有依赖它的模板和计算属性都会自动更新。没有 ref 的话let count 0这种普通变量在 setup 里改一百次模板都不会有任何反应。在模板中 ref 会自动解包所以你在 template 里写{{ count }}就能读到值不需要写成count.value。但到了 JavaScript 逻辑中必须通过.value访问。这是一个新手最容易漏掉的点忘写.value页面不报错但数据怎么都不更新。我建议把它当成一条肌肉记忆来练写多了自然就顺手了。我之前看到有人在群里问“ref 和 reactive 到底选哪个”一个比较实用的结论是能选 ref 就选 ref。因为 ref 可以存对象配合toRefs或直接通过.value操作能覆盖绝大多数场景。尤其在script setup下ref 是使用最顺手的基础响应式工具类型推导也最友好。3.2 reactive 的使用边界reactive 的直接用法是传一个对象返回响应式代理访问属性不需要.value写起来会少一层。但它的限制也明显不能对基础类型使用不能随意整体替换。最典型的一个坑是如果你写let state reactive({ count: 0 }); state reactive({ count: 1 })原来的响应式链就断了。顺带一提用 ES6 解构const { count } state之后count 也会变成普通变量跟响应式没有任何关系。那 reactive 的优势在哪在明确需要深层对象并且习惯用“状态对象”组织数据的场景它写起来比一堆.value更清爽。比如放一组表单字段const form reactive({ username: , password: , remember: false })模板里直接form.username绑定不需要到处.value。不过如果组件逻辑复杂我更倾向于用 ref 包一个大对象配合 toRefs 拆出来用这样平衡了两边的优点减少解构丢失响应式的风险。3.3 响应式丢失的典型场景下面这几个坑我在实际开发里都遇到或者见到过值得记下来把 reactive 对象解构后在模板中使用改动完全无效。解决办法是用 toRefs 或者直接用 ref。用普通函数对响应式对象做深拷贝比如 JSON.parse(JSON.stringify(obj)) 后赋值回去响应式关系经常丢。因为 JSON 序列化会把 Proxy 还原成普通对象。往响应式数组里 push 一个完全普通的新对象后续对这个对象做修改不会自动更新。需要保证数组里的元素本身也是响应式代理的一部分或者用 ref 包整个数组。二次封装响应式对象时只保留部分属性结果新对象属性不是响应式。这时需要判断是否要用 computed 派生而不是复制。我的调试经验是遇到页面不更新的情况先别急着怀疑 Vue 出 bug90% 是响应式链在某一步断了。在代码里多打印isRef(value)和isReactive(value)能快速定位问题节点。Vue 提供了这两个内置类型守卫用起来很省事。4. 组合式 API 的实战笔记4.1 setup 的两种写法Composition API 的入口是 setup 函数但实际开发里我更推荐直接写script setup语法糖。它最大的优点是不用手动 return 数据和函数所有在script setup顶层声明的内容模板里都能直接用。这在组件代码多的时候非常省事少写一堆 return。对比一下两种写法。传统 setupexport default { setup() { const count ref(0) function addCount() { count.value } return { count, addCount } } }语法糖写法script setup import { ref } from vue const count ref(0) function addCount() { count.value } /script第二种写法的另一个好处是组件引入后不需要注册即可在模板中使用。比如你 import 了一个子组件直接ChildComponent /就能渲染。我现在的项目全部采用这种写法它是 Vue3 官方文档推荐的方向也是绝大多数新项目的标准形态。4.2 生命周期钩子变化Vue3 把原来的 destroy 相关钩子改名了beforeDestroy变成beforeUnmountdestroyed变成unmounted另外新增了onRenderTracked和onRenderTriggered可以在调试时追踪组件到底因为哪条依赖变动而重新渲染。组合式 API 下的生命周期不是写在选项里的而是作为函数调用需要哪个就 import 哪个。比如import { onMounted, onUnmounted } from vue onMounted(() { console.log(组件挂载完成) }) onUnmounted(() { console.log(组件即将卸载) })这里有个和 Vue2 不同的小细节在 Options API 中created和data的执行顺序有明确关系而在script setup中整个模块顶层代码就相当于在 setup 内部执行你不需要也不太容易区分“创建前”和“创建后”实际也无所谓因为顶层代码本来就在挂载之前跑完。用到最多的是onMounted比如初始化接口请求、注册事件监听。4.3 组件通信的几种方式Vue3 组件通信方式和 Vue2 差不多但语法上更清爽。父子组件传值父传子用definePropsconst props defineProps({ title: { type: String, default: } })子组件里直接使用props.title模板里直接用title。子传父用defineEmitsconst emit defineEmits([change]) emit(change, value)跨层级传值用 provide / inject。我用它做全局主题和用户登录态比较多。它比 Vuex/Pinia 轻量适合不需要复杂状态管理的场景。这里我踩过一个坑provide 一个 ref 时如果直接提供ref对象本身子组件拿到的是响应式的但如果你提供的是ref.value比如provide(theme, theme.value)那子组件拿到的就是普通值后续修改主组件里的 theme子组件不会跟着变。所以要么直接provide(theme, theme)要么用工厂函数包一下。5. 实际开发中的高频场景记录5.1 动态增删表单行中后台系统里经常有“一行一组输入项用户可以自行添加删除”的需求。很多人一上来就按 index 做 key 绑定结果删除中间行后输入框内容对错位。正确的做法是每一行给一个唯一 id用 ref 数组来维护。完整写法script setup import { ref } from vue const rows ref([ { id: 1, name: , age: } ]) let nextId 2 function addRow() { rows.value.push({ id: nextId, name: , age: }) } function removeRow(index) { rows.value.splice(index, 1) } /script template div div v-for(row, index) in rows :keyrow.id classrow input v-model.trimrow.name placeholder姓名 / input v-model.numberrow.age placeholder年龄 / button typebutton clickremoveRow(index)删除/button /div button typebutton clickaddRow新增一行/button /div /template关键点一个是 key 用 row.id 而不用 index另一个是v-model.number可以顺手把输入框内容转成数字类型否则 age 绑定到后端时会变成字符串等到联调才发现就晚了。如果你用的是 Element Plus套路完全一样只是表单控件换成el-input、el-button而已。5.2 引入 Element Plus 的正确姿势国内做后台管理系统Element Plus 应该是最常用的 Vue3 组件库。引入方式有两种全量引入和按需自动引入。全量引入最简单import ElementPlus from element-plus import element-plus/dist/index.css app.use(ElementPlus)缺点是打包体积大首次加载慢。生产项目我推荐按需自动引入配合 unplugin-auto-import 和 unplugin-vue-components 两个插件。安装完依赖后在 vite.config.js 里加配置import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] })这样组件里直接写el-button不需要手动 import 样式和组件打包时会自动把用到的部分打进去。需要注意的坑是自动导入默认只解析模板中的组件如果你在 JS 代码里主动调用了ElMessage、ElNotification这类函数式组件还是要手动导入样式或者额外配置。5.3 修改 Tabs 标签页样式的小窍门Element Plus 的 Tabs 组件样式经常要改但改起来总是不生效根本原因是组件内部样式被 scoped 隔离了。Vue 的 scoped 会给元素加上 data 属性选择器子组件内部的元素不在当前组件 DOM 里自然覆盖不到。这时候需要用:deep()穿透到子组件style scoped .custom-tabs :deep(.el-tabs__nav-wrap) { background: linear-gradient(135deg, #e3f2fd, #fff); border-radius: 8px; } .custom-tabs :deep(.el-tabs__item) { font-weight: 500; } .custom-tabs :deep(.el-tabs__item.is-active) { color: #2563eb; } /style:deep()的作用是让后面的子选择器不受 scoped 限制。但要注意前面一定要有一个能定位到当前组件内部容器的选择器比如.custom-tabs这样才不至于把全站所有 el-tabs 都改掉。如果你不用 scoped 而是单独写一个全局样式文件就不需要:deep()了但得自己管理类名冲突中大型项目我更推荐 scoped :deep()的组合。6. 面试题向的学习备忘6.1 面试官常问的高频问题最近帮朋友模拟面试时发现 Vue3 的面试题翻来覆去就集中在几个点上。我顺手把它们整理出来当作自己的复习提纲Vue3 和 Vue2 的响应式原理有什么不同为什么选 Proxyref 和 reactive 有什么区别各自适合什么场景Options API 和 Composition API 如何选择setup 语法糖解决了什么问题computed 和 watch 的区别是什么什么时候用哪个Vue3 中 v-if 和 v-for 的优先级和 Vue2 有什么变化如何设计一个团队通用的组合式函数前几个问题这篇笔记前面已经覆盖了大半。最后一个“组合式函数设计”值得多说一句它本质就是利用 Composition API 把可复用逻辑封装成函数比如表单、表格、权限校验。面试时你能现场写一个 useThrottle 或者 useFetch说服力比背十遍概念都强。6.2 我会怎么答核心问题关于响应式原理我的回答思路是Vue2 的 Object.defineProperty 只能劫持已有属性新增属性要额外调用 Vue.set数组变化也不能完全监听Vue3 的 Proxy 直接代理整个对象新增属性、数组 index 修改都能拦截而且在递归代理上有更优雅的处理。关于 v-if 和 v-for 优先级Vue2 中 v-for 优先所以同一节点上同时使用两者时v-if 拿不到 v-for 的循环变量Vue3 反过来v-if 优先级高于 v-for会导致循环变量在判定时未定义。所以最优解还是建议不要在同一元素上同时用这两个确实需要过滤时可以用 computed 先派生出列表。关于 computed 和 watch我会强调 computed 适合由现有状态派生新值有缓存watch 适合在状态变化后做有副作用的操作比如请求接口、写缓存。尤其在公司项目里我们用 watch 监听路由变化来更新页面标题这种场景用 computed 不合适。这些题没有标准答案面试官更看重你能不能结合项目讲清楚。我自己在项目里真实处理过响应式丢失的问题把排查过程说出来比背概念要加分得多。这也是我写这篇笔记的初衷——把学到的、踩过的、验证过的东西串成一条线知识才真正是自己的。我这边的后续计划是继续记录更进阶的内容第二篇会重点写 Composition API 下的逻辑复用封装和 TypeScript 的落地实践做得差不多了再发出来。
返回列表