翻了翻自己这两年的Vue 2笔记,发现大部分内容其实都是“踩过坑才记下来的”,而不是从文档里抄来的。Vue 3 出来很久了,但不少老项目、维护中的后台系统,依然跑在 Vue 2 的轨道上,尤其是 2.6 和 2.7 这两个版本,生态成熟、文档齐全、坑基本都被踩平了。这份 vue2笔记说不上是体系化教程,它更像是我在实际项目里反复翻开的那本备忘录——从响应式原理的细节,到组件通信的选型,再到生命周期与 $nextTick 的配合,以及一堆只有真上手才会碰到的边缘情况。
如果你正在维护旧项目,或者从 Vue 3 转过来要接 Vue 2 的锅,又或者刚学完基础准备上手真实业务,这篇笔记应该能帮你省掉一些查资料的弯路。下面我按实战时最常用的几个方向来分享,每一节都会把“为什么这么做”和“实际会遇到什么坑”放一起讲。
1. 为什么还在整理Vue 2笔记:一份面向真实项目的备忘录
1.1 不是教程,是查漏补缺的清单
我整理这份笔记时没有按文档目录走,而是按“项目中真正用过、也真正踩过坑”的知识点来组织。比如响应式系统的实现细节、组件通信到底该选哪种方案、生命周期钩子里的操作时机、以及性能优化时哪些手段性价比最高。文档只会告诉你“可以这样做”,但这份笔记会告诉你“这样做会遇到什么问题、为什么要这样做”。
很多初学者喜欢照着文档一步步敲,但到了项目里就会发现,文档的例子太干净了。真实场景里,数组下标赋值不更新、异步请求的时序错乱、路由切换后状态残留,这些问题在文档里几乎找不到现成答案。所以我按主题把它们整理下来,方便快速定位。
1.2 这份笔记适合谁读
如果你想在 Vue 2 项目里写出更稳的代码,或者已经在 Vue 2 上碰过钉子,这份笔记会很实用。模板语法、事件绑定、表单修饰符这些基础内容我不展开,因为官方文档讲得足够清楚。我重点补充的是原理理解、方案选型判断和问题排查这些“经验型”的内容,希望帮助你在动手前先有个大致判断,减少试错时间。
2. 响应式系统:Object.defineProperty 的边界与三个经典坑
2.1 getter 和 setter 到底做了什么
Vue 2 的响应式核心是 Object.defineProperty。初始化时,框架会遍历 data 对象的每个属性,把它们改造成“访问器属性”,也就是用 getter 和 setter 接管数据的读写。组件渲染时读取了某个属性,会触发 getter,此时框架把这个组件的 Watcher 收集起来;当属性被修改时,setter 触发,框架通过 Dep 通知所有相关的 Watcher 重新渲染。
说白了,这个设计就是给属性装了一扇门:读取时登记身份,修改时喊人处理。组件不知道数据什么时候变,但通过这套门卫机制,它总能在数据变的时候收到通知,从而重新执行渲染逻辑。
2.2 数组和对象的监听盲区
但 Object.defineProperty 有个天然局限——它必须预先知道属性的名字。所以 data 里一开始不存在的属性,后续添加进对象时,Vue 是不知道的,也就不会为它建立响应式。另外,通过索引直接修改数组元素(arr[0] = 'x')也不会触发 setter,因为数组索引的拦截根本不在 defineProperty 的监听范围内。
Vue 2 的做法是改写数组的 7 个变异方法(push、pop、shift、unshift、splice、sort、reverse),让它们能主动通知更新,但索引赋值和 length 修改没有被覆盖。这个局限在很多场景下会造成“数据变了,页面不更新”的诡异现象。
注意:数组索引赋值、直接修改 length、对象新增属性,这三种操作在 Vue 2 中默认都不会触发视图更新。遇到这类问题先确认是不是这些操作。
2.3 $set / $delete 的正确打开方式
遇到上述问题,最简单的解决方案是使用 Vue.set(也就是 this.$set)和 Vue.delete。$set 能显式地为对象添加响应式属性,$delete 能显式地删除属性并触发更新。$set 本身并不是黑魔法,它内部本质上还是调用 defineProperty 把新属性变成响应式的,所以要求目标对象本身是响应式的。
我自己的习惯是:如果对象的结构在未来可能扩展,最好在 data 里先占好位,比如data: { userInfo: { name: '', age: null } }。这不优雅,但能避免大量 $set 满天飞的代码。至于数组,不要用索引赋值,一律用 splice 或者整体新建数组赋值,这是最简单也最可靠的方式。
3. 组件通信方案对比:props、$emit、provide/inject、Event Bus 还是 Vuex
3.1 从 props/$emit 的基础款说起
父传子用 props,子传父用 $emit,这是 Vue 组件通信的基础。优点是简单、单向数据流清晰;缺点是只适合父子层级,跨层级的组件通信会让 props 链变成“钻隧道”。我有一个原则:超过两层还传 props,就要考虑重构了。因为中间层组件只是透传,却要写一遍 props 声明,后续加字段要在多层文件里一起改,维护成本很高。
3.2 $refs 与 provide/inject 的适用场景
$refs 可以在父组件里直接调用子组件暴露的方法,适合“父主动叫子做事”的场景。但要注意,$refs 只能在组件渲染完成后拿到,在 created 里直接 this.$refs.someChild,拿到的多是 undefined。在 mounted 或之后访问就正常了。
provide/inject 是跨层级注入的方案,适合“祖先组件提供数据,任意后代组件直接使用”的情况,比如全局配置对象。但它有个容易忽略的点:provide 传入的值默认不是响应式的,除非传入的是一个 Vue 响应式对象本身。我见过团队用它传一个普通对象,结果孙子组件改了值,整条链路的状态都没跟着变,查了半天才发现是响应式特性丢了。
3.3 Event Bus 的使用边界与清理
Event Bus 本质上就是利用一个 Vue 实例的 $on / $emit 做任意组件间的通信。优点是轻量、不依赖组件层级,兄弟组件或跨多层嵌套时通信特别顺手。缺点是事件流的全局性和不确定性:多个组件监听同一个事件名时,触发顺序不好把控;组件销毁时如果没 $off 掉监听,回调还会继续执行,形成内存泄漏和重复触发。
我建议 Event Bus 只用于低频、临时的通信场景,并且务必在 beforeDestroy 钩子里手动清理监听:
beforeDestroy() { this.$bus.$off('refresh-list', this.refreshList) }能做到用完就清,Event Bus 依然是一个性价比很高的方案。如果发现事件越来越难追踪,就说明该考虑换更严谨的状态管理方案了。
3.4 什么时候真正需要 Vuex
Vuex 的引入成本其实不低,state、getters、mutations、actions、modules 一整套概念,如果只是两个组件偶尔共享一个值,上 Vuex 属于杀鸡用牛刀。但如果你面临这些情况:多页面、多组件共享同一份状态,状态变更需要可追踪,需要支持时间旅行调试,或者多个模块都在读写同一份业务数据,那 Vuex 就非常值得用。
我给团队的经验标准很简单:数据超过两个页面在用,并且变更逻辑有一定复杂度,就放进 Vuex;否则就在组件内自消化。状态管理的目标不是“用的越多越好”,而是职责清晰、变更可追溯。记住,Vuex 只是工具,不要为了架构上的“好看”而过度设计。
4. 生命周期与 $nextTick:操作时机决定代码质量
4.1 created、mounted、activated 的分工
created 在实例创建完成后立即调用,此时 data、computed、methods 已经可用,但 DOM 还没生成,所以不适合做 DOM 操作。在这发起请求、初始化非 DOM 相关的状态比较合适。mounted 在模板渲染并挂载完成后调用,此时可以访问 DOM 节点,也可以初始化第三方库。
但要注意,mounted 只会在组件第一次挂载时执行一次。如果你用 keep-alive 缓存了组件,再次切换到它时不会重新走 mounted,而是走 activated。所以像“每次进入页面都刷新数据”“恢复滚动位置”这类逻辑,应该放在 activated 而不是 mounted 里。
4.2 $nextTick 解决的究竟是什么
Vue 的 DOM 更新是异步的。当你修改数据并期望立刻操作更新后的 DOM 时,你会发现拿到的还是旧值——因为 DOM 更新会在同一事件循环的微任务阶段才执行。$nextTick 就是把回调放到 DOM 更新完成之后再去执行,从而保证你能拿到最新的 DOM 状态。
最常见的例子是弹窗组件:你根据某个状态显示弹窗,紧接着想调用弹窗内部的方法或聚焦某个输入框。如果不包一层 $nextTick,这个输入框还没被渲染到真实 DOM 里,自然拿不到。加了 $nextTick 之后,等更新完成再操作,就能稳定拿到了。
提示:$nextTick 在 Vue 3 中仍然存在,但 Vue 3 推荐使用
nextTick函数或组合式 API 形式。Vue 2 里也直接await this.$nextTick()用起来很顺手。
4.3 在异步与守卫场景中用好生命周期钩子
项目中很常见的场景是在路由守卫里做权限校验和异步数据预取。beforeRouteEnter 有个特殊之处:它拿不到 this——因为组件实例尚未被创建。解决方案是在回调里接收 vm 参数,或者把数据预取逻辑放到 created 中。
这里有个我踩过的坑:在 beforeRouteEnter 里通过next(vm => { ... })做逻辑,以为 vm 拿到手就万事大吉,结果发现如果 DOM 还没完全准备好,直接操作 DOM 依然会翻车,这时候还得配合 $nextTick。所以正确顺序是:先判断数据时机,再判断 DOM 时机,两者对齐后才能稳定操作。
5. 性能优化:从首屏到长列表的实操经验
5.1 路由懒加载与组件懒加载,效果好改造成本低
Vue 2 配合 webpack 做路由懒加载在工程里标准写法是箭头函数动态 import:
{ path: '/dashboard', component: () => import('@/views/Dashboard.vue') }webpack 会把每个懒加载路由单独打包成一个 chunk,用户打开首页时不会加载未访问页面的代码。对中大型后台系统来说,这是首屏提速最划算的手段之一,改动量小、收益明显。
组件级的懒加载则适合用在弹窗、浮层这类非首屏组件上,配合 v-if 条件在点击时才加载对应组件,减少初始 bundle 体积。不过懒加载组件在低网速下会有短暂的白屏或延迟感,建议结合 loading 态或骨架屏使用。如果你想做“首屏秒开”,这一步几乎是必选项。
5.2 v-if 和 v-show 不是随便选的
v-if 是真正的条件渲染,条件为假时组件不会被创建,切换时会经历销毁和重建;v-show 则是用 display:none 控制显示隐藏,组件始终存在。高频切换用 v-show,低频或初始条件明确的场景用 v-if 更省。
另一个容易被忽视的是 key。列表渲染时 key 要尽量使用业务唯一 id,而不是数组索引。用索引当 key 时,如果列表插入或删除了元素,Vue 的 diff 算法可能会出现状态错位——输入框内容被复用、勾选状态错乱,我排查过几次这类问题,改完 key 后现象立刻消失。选择 key 的原则就是:稳定、唯一、不随渲染而变化。
5.3 长列表渲染的降级策略
列表数据特别长(几百上千条)时,直接用 v-for 渲染往往会卡顿。最常见的三个降级方案:分页加载、虚拟滚动、冻结数据。
- 分页加载逻辑简单,缺点是交互不连续,用户得反复翻页。
- 虚拟滚动只渲染可视区域内的节点,性能最优,但方案选型需要匹配数据量。
- 冻结数据要留意:Object.freeze 冻结的对象虽然不能新增或修改属性,但 Vue 的响应式系统也不会对它们做监听,适合纯展示、不变的静态列表。
我实际项目中遇到过一个 2000 行的日志列表,用虚拟滚动后渲染时间从几百毫秒降到了几十毫秒,体验提升非常明显。如果你的业务里已经有成熟的表格组件,优先看它是否内置虚拟滚动,避免重复造轮子。
5.4 computed、watch、methods 三者别混着用
computed 有缓存,只在依赖项改变时重新计算,适合根据多个数据派生一个新值的场景。methods 没有缓存,每次调用都重新执行,适合事件处理或非派生类的逻辑。watch 适合监听某个数据的变化并执行副作用,比如异步请求、手动 DOM 操作、搜索防抖。
一个常见误区是拿 computed 去做有副作用的操作,比如在 getter 里发请求。computed 定位是纯函数式地计算派生数据,副作用放里面既难调试,也可能被缓存策略干扰。我写代码时的判断标准是:能派生出来的数据绝不存 state,能用 computed 的地方不用 methods,需要监听变化触发外部操作时才用 watch。遵循这个标准,组件逻辑会清晰很多。
6. 工程化配置与样式作用域:容易被忽略的实战细节
6.1 scoped 样式不是万能的
Vue 单文件组件中,style 加了 scoped 后,编译器会给模板中每个元素加一个>plugins: [ ['import', { libraryName: 'some-ui', libraryDirectory: 'es', style: 'css' }] ]
配置完成后,以解构方式引用组件,webpack 会自动把对应的 JS 和样式打进来。要注意的是,按需引入后自定义主题色可能需要手动覆盖 CSS 变量,这部分官方文档往往只给示例,真做起来还是得翻开组件样式源码去查。如果你只是用了个别组件,也可以直接import SomeButton from 'some-ui/lib/button',更轻量也更可控。
7. 高频排查清单:项目现场救火记录
7.1 数组更新了,页面纹丝不动
这种问题十有八九是数组索引赋值或直接修改 length 引起的。把arr[0] = 'new'换成arr.splice(0, 1, 'new'),页面立刻就有反应了。如果还是不行,检查一下是不是在 Vuex 的 state 外直接改了数组,或者是不是对数组做了引用替换,但新数组本身不是响应式的。还有一个隐藏场景:从接口拿到的数组嵌套较深,内部对象新增字段时忘了 $set,也会出现类似现象。
7.2 异步请求的时序错乱
页面发起请求后,用户很快切走了路由,请求回来时组件已经销毁。如果此时回调里还在操作数据或 DOM,轻则控制台警告,重则内存泄漏。负责任的做法是在 beforeDestroy 中加一个标志位判断,比如this._isDestroyed = true,回调时先检查。如果项目接入了 axios,也可以在拦截器层面做取消请求的处理。
我给团队的规范是:组件内所有异步回调最前面都判断一下组件是否仍存在。这个习惯养成了,很多棘手问题能直接从源头规避。
7.3 keep-alive 与状态残留
加了 keep-alive 后,组件实例会被缓存,切走不会销毁,回来不会走 mounted,只会走 activated。很多问题就出在这里:你以为每次进入都该重新拉数据、重置表单,但实际上数据还是上次留下的。解决方式是明确划分两类逻辑:一类在 deactivated 时重置临时状态,一类在 activated 时刷新需要更新的数据。不要把所有初始化逻辑堆在 mounted 里了事,这一点在配置了 keep-alive 的后台系统里特别重要。
7.4 事件监听和定时器的泄漏
在 mounted 中通过 window.addEventListener、setInterval 添加的监听和任务,如果不在 beforeDestroy 中 removeEventListener 和 clearInterval,组件销毁后这些东西依然活着。我见过一个项目因为定时器没清理,切页越多请求越频繁,最后浏览器直接卡死。排查方式很简单:在 devtools 的 Performance 或 Sources 里观察事件监听器数量,如果只增不减,就去看哪些组件在 beforeDestroy 没做清理。
除了定时器和事件监听,图表库实例也经常被遗忘。用了某个图表库,却没在组件销毁时调用销毁方法,页面反复切换后同样内存暴涨。任何对全局对象、第三方库实例的接入,都要坚持“创建与销毁成对出现”的原则。
我个人在实际操作中最深的体会,是 Vue 2 的很多问题根源都不在于框架本身,而在于开发者默认它“什么都管”。响应式有边界、异步有时序、生命周期有取舍,真正理解这些边界,比背 API 重要得多。这套 vue2笔记我还在继续补充,如果你也在维护 Vue 2 项目,希望它能在某个排查之夜帮你少走一段弯路。