提起 “vue2笔记” 这几个字,我第一反应不是语法手册,而是这几年项目里折腾出来的那叠踩坑记录。从 2.2 版本一路用到 2.6,Vue 2 的笔记我改了无数版,最早的文档早就丢了,剩下来的每一段都带着项目现场的味道:数据不更新、组件失忆、路由报错、状态管理越改越乱。这文章就当一份整理过的公开版 Vue 2 笔记,把我对框架核心机制的理解、亲测顺手的项目结构、以及高频问题排查一起写出来。适合正在学 Vue 2 的前端同学,也适合准备接手老项目的朋友花十分钟过一遍,少走弯路。
1. 搭建 Vue 2 笔记体系:从机制到项目结构
1.1 学习 Vue 2 必须建立的三层认知
我整理 Vue 2 笔记的时候,最初犯过一个特别常见的错误:按官方文档的目录顺序记录,模板语法抄一遍、指令列表抄一遍、API 再抄一遍。结果到项目里遇到问题,笔记根本帮不上忙。后来我把整份笔记推倒重来,换成了“三层认知”的结构,才真正顺手。
第一层,是底层机制层。核心就三样:响应式系统、虚拟 DOM 和组件实例。响应式决定了数据怎么驱动视图,虚拟 DOM 决定了更新性能的上限,组件实例决定了你写的配置到底会跑进哪个生命周期。这三样不熟悉,后面所有特性都是空中楼阁。我笔记里这一层画满了箭头和时序,虽然丑,但每次卡住都能回到这里重新推导。
第二层,是实际应用层。包括常用的指令、组件通信方式、计算属性和监听器的分工、生命周期钩子的真实触发时机。这一层是你天天在写的东西,也是最容易“会用但说不清为什么”的地方。笔记里必须给每个技巧配一个失败案例,说明不这样做会踩什么坑,否则过三个月再看,就只剩一堆正确但无用的结论。
第三层,是工程化层。Vue 2 的应用很少是单文件孤军奋战,脚手架、路由、状态管理、组件拆分、目录规范都在这一层。如果说前两层解决“怎么写组件”,这一层解决的是“怎么把几十上百个组件组织成一个能长期维护的系统”。我看到的绝大多数 Vue 2 老项目,问题都不是组件写得烂,而是工程结构从一开始就缺约束。
1.2 我的 Vue 2 笔记库是怎么梳理的
我现在的笔记库分四个目录,每个目录对应一个真正会遇到的场景。
第一个目录叫“机制速查”,里面放响应式相关的代码片段、生命周期表格、虚拟 DOM 的更新策略笔记。这部分不追求长篇大论,追求的是十分钟内能找到答案。
第二个目录叫“组件通信”,从最简单的 props 单向数据流,到 $emit、事件总线、provide / inject、插槽,每条都搭配一个我真实做过的功能场景。比如“列表页筛选条件如何传给子组件”“弹窗关闭后如何通知父组件刷新数据”。
第三个目录叫“状态与路由”,Vuex 的 modules 组织、action 和 mutation 的分工、路由的懒加载、导航守卫的典型用法。这些单独看文档都懂,但组合起来容易出现边界问题。
第四个目录叫“坑位记录”,这是最重要的目录。我每遇到一个排查超过半小时的问题,就固定记一笔,包含现象、原因、解决方案和我当时的错误判断。这份笔记最后几十条,就成了这篇博客后面“高频问题”那一节的真实素材库。
2. Vue 2 核心机制拆解:响应式、组件与生命周期
2.1 响应式原理:Object.defineProperty 的能力边界
Vue 2 的响应式机制,核心是基于 Object.defineProperty 做数据劫持。初始化 data 时,Vue 会递归遍历每个属性,把它们转换成 getter / setter,并在组件渲染时建立依赖收集关系。数据变化时,setter 会通知依赖,触发组件重新渲染。
这个机制最出名的一个坑,就是“新增属性不响应”。我在第一个正式项目里就撞上了:接口返回的数据里没有某个字段,前端拿到后手动加了一个,结果页面死活不更新。原因很简单,新增属性在前端并不存在于初始化时的响应式转换过程中,自然没有对应的 setter。
解决办法有几种。最简单的是提前在 data 里声明好这个字段,哪怕初始值为 null。如果要动态添加,就需要用 Vue.set,在数组或对象上明确告诉 Vue 这是一个新属性。批量添加多个新属性时,可以先用 Object.assign 生成新对象,再整体赋值给原变量,一次性触发更新。
数组的索引赋值和 length 修改也是盲区。比如 this.arr[0] = 'xxx' 修改了值,视图不会动。这是因为 Vue 2 对数组的处理里,索引赋值没有被完整拦截,只能通过 splice 或 Vue.set 来触发响应。项目里遇到类似需求,我都直接在笔记里标红:不要相信索引赋值,要用数组方法。
2.2 生命周期钩子的执行时序与真实用途
Vue 2 的生命周期,每个钩子不是摆设,每个都有最适合干的事。我排查问题的时候,判断依据经常就是“这段代码到底该放哪个钩子里”。
beforeCreate 阶段,实例刚初始化,data 和 methods 都还没准备好。这个钩子我用得不多,偶尔会在顶层混合里做初始化判断,避免依赖数据时的报错。
created 阶段,data 和 methods 已经可用,但 DOM 还没挂载。这是一个非常适合拉接口数据的阶段,因为此时你拿到的数据可以再去影响后续渲染,而不会因为渲染早于数据导致空白。我笔记里特别注明:created 里可以做异步请求,但不要在这里操作 DOM,因为还没渲染出来。
beforeMount 到 mounted 的过渡是关键阶段。mounted 之后,DOM 已经挂载完成,可以访问 this.$el,做一些依赖真实布局的计算。这里常见的坑是,弹窗组件或者图表组件在 mounted 里初始化时,如果容器宽高还是 0,就要考虑用 this.$nextTick 等 DOM 稳定后再执行。
beforeDestroy 是清理资源的黄金时期。我在这个钩子里清过定时器、解绑过 window 上的事件监听、销毁过图表实例。不清理的后果就是内存泄漏,单页应用里来回切页面,内存占用只升不降。
组件还有 activated / deactivated 这一组钩子,配合 keep-alive 使用。项目里有标签页切换、列表页状态保留需求时,把“每次进入都要刷新数据”的逻辑放进 activated 里,比放在 mounted 里更准确,因为 keep-alive 组件首次挂载后,mounted 不会再次触发。
2.3 computed、watch 与 methods 的最佳分工
这三样东西,新手最容易混着用。我在笔记里给自己定了一个很简单的判断规则。
computed 适合“根据已有数据计算出一个新值”,并且需要被模板直接引用。计算属性有缓存,依赖不变就不会重新计算。我有一段很典型的代码,价格列表里把含税价算出来:
computed: { totalPriceWithTax() { return this.items.reduce((sum, item) => { return sum + item.price * item.count * 1.1; }, 0); } }只要 items 不变,这个计算属性不会重复执行,性能上很稳。
watch 适合“监听数据变化后执行一个副作用”,比如监听搜索关键词变化,触发远程搜索。它不追求返回值,而是你去派发请求或调用方法。但要小心深度监听,监听一个对象内部属性变化时,直接写 watch: { obj: { deep: true } } 会好使,可一旦对象层级很深、数据量很大,性能开销会很感人。我的建议是尽量监听具体字段,比如 obj.name,不要动不动就挂 deep。
methods 里放的就是普通函数,需要被事件触发,或者被别的方法调用。它不像 computed 有缓存,每次访问都会重新执行。模板里尽量少直接调用 methods 去做数据展示,因为那意味着每次渲染都要重新执行一整段逻辑。
这三者记住一句话:要展示派生数据用 computed,要响应变化做事情用 watch,要交互触发行为用 methods。笔记里我写的最多的反思,就是把 watch 当成 computed 用,结果多写一堆没必要的代码。
3. Vue 2 实操记录:从零实现一个可维护的中小型项目
3.1 脚手架与项目目录的拆分思路
我笔记里的实操章节,固定从一个模拟后台管理系统开始。这类系统特别适合练手 Vue 2 的工程化能力,因为它包含登录状态、路由控制、列表筛选、弹窗交互、权限按钮这些常见模块。
创建项目我习惯用官方脚手架,vue create 生成标准结构以后,先做目录规划。我的习惯是:
src/ ├── api/ # 接口请求集中管理 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Vuex 状态管理 ├── views/ # 页面级组件 ├── utils/ # 工具函数 └── App.vue目录本身不复杂,关键约束在边界:api 目录里只放请求方法,不掺杂业务逻辑;views 里的页面只负责组合组件和数据流转,不要把大量样式和交互塞在一起;components 里的公共组件要尽量“少业务”,只接收 props、对外抛出事件,这样换一个项目还能复用。
我踩过的最大坑是 components 目录变成一个垃圾堆,最后里面什么都有,既有无状态的按钮封装,又有走后端接口的业务卡片。后来我给自己立了一条规矩:如果组件里用到了 Vuex 的 dispatch,或者直接调用了 api 模块,它就不算公共组件,应当离 views 更近一些。
3.2 组件通信的实践组合拳
Vue 2 里的组件通信方式很多,我在笔记里把它们按使用频率排了个序,也标注了各自的坑。
父传子,最标准的方式是 props。子组件里声明需要的字段和类型,父组件绑定对应变量。这里要注意单向数据流的规则:子组件不能直接修改 props 里的对象或数组,否则会在某些情况下面临难以定位的联动变化。实际项目里,我遇到的请求是修改 props 里的某个字段,正确做法是让子组件通过 $emit 通知父组件去改。
子传父,用的是事件。我写过一个搜索框的例子:
// 子组件 this.$emit('search', this.keyword);父组件监听 @search 并执行自己的方法。这个模式非常够用,绝大多数兄弟组件之间需要通信时,我会先把状态提升到它们的公共父组件,而不是急着用事件总线。
跨多级组件传递数据时,可以用 provide / inject。父组件提供数据,任意深层的子组件都能注入。这对第三代、第四代组件层级特别友好,但缺点是不容易追踪数据来源,我的笔记里规定“只在公共上下文里用”,比如传一个全局配置对象,不允许用来传递频繁变化的状态。
插槽也是一个容易被低估的通信方式。父组件可以往子组件里塞布局和内容,作用域插槽甚至可以让子组件把内部数据交还给父组件定制展示。我常写的表格组件,就是通过作用域插槽把当前行数据暴露出来,让使用方决定这列显示什么按钮和标签。
3.3 状态管理与路由的前后端联动
状态管理用 Vuex 时,我笔记里最强调的一件事是“单向数据流”:组件里通过 dispatch 触发 action,action 里做异步操作或复杂逻辑,然后 commit mutation,mutation 里唯一修改 state。不要在组件里直接修改 state,一旦项目变大,极难排查是谁把状态改了。
目录上,Vuex 用 modules 拆分是必须的。我把用户信息、应用配置、业务数据分到不同 module 里,每个 module 自成一个目录,包含 state、getters、mutations、actions。这样多人协作时,文件冲突明显减少。
路由这一块,最常用的就是管理后台的动态路由需求。用户登录后根据权限加载不同的路由表,我用 router.addRoutes 动态注册。这个操作在 Vue 2 里很成熟,但有几个细节要记:
- 动态添加的路由要定义在固定的根路由之前,否则可能被通配符路由拦截。
- 退出登录时,需要重置路由表,不然下一个用户登录会看到上一个用户的路由。
- 路由懒加载用 import() 后,页面切换时会有短暂的加载过程,要留意操作反馈。
导航守卫也值得单独记一笔。我在全局前置守卫里做登录态判断,在页面内部用 beforeRouteEnter 做异步数据的预加载。比如进入一个详情页前判断是否有缓存数据,没有就去请求接口,提前把数据放进 Vuex 或组件的 data 里,这样页面一出来就有内容,用户体验比先渲染空白再刷新好很多。
4. Vue 2 高频问题与排错技巧实录
4.1 视图没有更新,但 console 里数据是新的
这类问题在 Vue 2 的排查记录里能占三分之一。表象是:控制台打印数据已经是新值,页面显示还是旧的。
优先检查这几类情况:
| 现象 | 可能原因 | 常规解法 |
|---|---|---|
| 对象新增属性后不更新 | 属性不在原始响应式系统中 | Vue.set / 重新赋值对象 |
| 数组索引修改不更新 | 索引赋值未被完整拦截 | splice / Vue.set |
| 多层嵌套对象未更新 | 深层属性未正确响应 | 尽量扁平化数据层级 |
| 通过异步更新导致的“闪烁” | DOM 还没同步到新状态 | this.$nextTick 里处理依赖 DOM 的操作 |
我笔记里有个真实案例。某列表页有一个操作记录数组,接口返回后前端对其中一个元素做字段追加。这个字段初始化时不存在,所以不管怎么改,页面都没反应。当时我浪费时间翻了模板逻辑,最后才发现是响应式边界的问题。补上 Vue.set 后立刻正常。
从那以后,我给自己写了一条固定检查路径:先看字段是否在初始 data 里,再看数组操作是否用了 vue 原生方法,最后看是否有异步覆盖赋值。走完这三步,九成问题都能定位。
4.2 组件销毁后内存泄漏与事件残留
单页应用里组件被销毁是家常便饭,但很多人没有同步清理资源。最常见的是 window.addEventListener,在 mounted 里绑定了 scroll / resize 事件,组件销毁后事件还挂在全局对象上,触发的方法还在执行,轻则重复触发,重则内存泄漏。
我在笔记里写了一个标准动作模板:
mounted() { this.handleScroll = () => { // 滚动逻辑 }; window.addEventListener('scroll', this.handleScroll); }, beforeDestroy() { window.removeEventListener('scroll', this.handleScroll); }要点是把回调函数存成具名变量,这样 remove 时才能精准移除。匿名函数在 add 和 remove 之间无法对应,这是个坑。
定时器也是重灾区。尤其页面里 setInterval 做轮询,如果组件销毁时没有清,轮询还会继续跑。我习惯在 data 或实例上保存 timer 的 id,beforeDestroy 统一 clear。图表实例也一样,销毁前调用 dispose 类方法,不然 canvas 对象会一直占着资源。
4.3 列表渲染顺序错乱:key 的重要性
Vue 2 里列表渲染的 key,不是可选项,而是必须认真对待的。很多人为了省事,直接把 index 当成 key,结果列表要插入或删除时,状态错乱。
举个例子。一个可拖拽排序的列表,每项有自己的选中状态。如果用 index 当 key,当第一项被删除后,第二项 index 变成了 0,复用组件时会把原来第一项的内部状态带过来,表现就是选中状态莫名其妙转移了。
笔记里我给 key 选型定了几个原则:
- 稳定:不能每次渲染都变。
- 唯一:同列表内不能重复。
- 不用 index:除非列表是纯静态且不适合删除/插入。
后端返回数据里 id 一般优先用作 key。分页数据里如果 id 可能重复,就要考虑用组合字段,比如 type + id。总之,key 直接决定组件复用和更新策略,省这一步后面全是坑。
4.4 重复点击路由报错与导航守卫误判
Vue 2 里重复点击同一个路由链接,有时会在控制台抛一个“NavigationDuplicated”的报错。这本质是重复导航被拦截,不影响功能,但对看控制台的人来说极具迷惑性。
我处理方法是在路由配置里加一个简单的补丁,重写 Router.prototype.push,捕获重复导航错误。这类代码网上有很多版本,我的笔记里记的是最精简的一种,确保有捕获就不会把错误抛到控制台。
评论区里有个常被忽略的场景:push 到同一个路径,但带不同的 query,比如商品详情页切换某个参数。这时候用 name + params 或 path + query 要特别注意比较,避免误认为同一路由而跳过更新。我踩过的情况是进入新参数后页面数据没刷新,因为组件的 created 没有重新触发,而组件实例被复用了。解法是监听 $route 的变化,再接一个新的数据请求逻辑。
5. 横向拓展:Vue 2 笔记里的进阶技巧与迁移思考
5.1 性能优化的几个真实抓手
Vue 2 项目做性能优化,我的经验是不急于上所谓的高级技巧,先把几个基础层面拉满。
第一是合理使用 computed。前面说过 computed 有缓存,同样的计算逻辑在模板里生成方法会反复执行。我在一个报表页面里把总额、均值、占比全部挪到 computed 之后,渲染耗时明显下降。
第二是 v-for 配合 v-if 的问题。Vue 2 里 v-for 的优先级高于 v-if,每次渲染都要先渲染整个列表,再逐个判断。大列表时开销很大。我通常把需要过滤后的列表先放到 computed 里,模板里只遍历过滤结果。
第三是 keep-alive 的妙用。对深度浏览的页面,比如带有大数据量表格的页面,用 keep-alive 缓存组件实例,切走再回来就不用重新请求和渲染。代价是内存占用增加,所以只缓存高频切换页,不缓存低频页面。
第四是函数式组件。简单展示型组件可以用函数式组件来减少实例开销,项目内公共的纯展示组件会考虑这种方式。不过劝大家不要滥用,函数式组件写起来少了很多生命周期能力,适合的才是最好的。
5.2 从 Vue 2 迁移新版本时的笔记对照
现在不少新项目已经转向 Vue 3,但 Vue 2 老代码还会维护很久。我在笔记里逐渐把“如果要迁移,需要注意什么”也记了下来。
响应式系统变化最大。Vue 3 用 Proxy 替代了 Object.defineProperty,新增属性和数组索引赋值都不再是问题,迁移前做的很多 hack 代码可以删掉。但组合式 API 和选项式 API 的思维不同,老团队的迁移成本不在于 API 名称,在于组件拆分方式被完全打破。
同样 name 的东西也要重新核对生命周期。beforeDestroy 变成了 beforeUnmount,destroyed 变成了 unmounted,事件清理的时点大体对应,但要注意细节差异,比如依赖注入的变化影响 provide / inject 的使用方式。
迁移笔记最好的保存方式,不是写下“Vue 3 怎么用”,而是把 Vue 2 项目里那些坑,逐一对照新框架里是否还存在。比如“新增属性不响应”这条,在 Vue 3 里基本消失,但“过度响应带来的性能负担”反而更常见,因为 Proxy 可以拦截更多操作,操作感知变强之后,依赖追踪粒度如果不做控制,更新可能更频繁。
这种对照记录特别有价值,因为迁移不是重新学一门新框架,而是搞清旧经验哪些保留、哪些作废。某些 Vue 2 时代养成的防御型写法,在新的代码库里不但没用,还会徒增复杂度。
6. 个人体会:Vue 2 笔记最值得保留的部分
我的 Vue 2 笔记最珍贵的不是那些从文档里抄来的标准用法,而是“当时我为什么那样做”的上下文。项目里的人会换,接口会变,但问题的本质总是相似:数据到底何时被收集依赖,组件究竟在哪个生命周期被创建和销毁,状态流是否足够清晰。
最后分享一个我的习惯。每解决完一个 Vue 2 的高难度问题,我会在笔记末尾加一个“如果下次遇到速度更快吗”的答案。这比单纯记错误原因更能逼自己提炼出判断标准。比如看到“页面不更新”,不再急着一处处查代码,而是先按响应式边界、事件监听、路由复用三条思路快速过一遍。
这套笔记方法帮助我在接手好几个旧项目时,都能在短时间内定位问题。Vue 2 迟早会被后续版本替代,但调试思路、状态管理观念、组件抽象能力,这些底层素养不会过期。如果你也在维护一份 vue2笔记,建议把踩过的坑单独成册,而不是只收藏正确写法。毕竟,那种“能跑但不知道为什么”的代码,才是项目里最贵的隐性债务。