Vue.js 框架
在前端开发的世界里,Vue.js 凭借其“平易近人、渐进式、高性能”的设计哲学,占据了半壁江山。无论版本如何从 2.x 演进到 3.x,Vue 的核心始终围绕着两个支柱:响应式数据驱动 与 组件化开发。
1. 核心基石:响应式系统 (Reactivity System)
响应式系统是 Vue 的灵魂。它让你只需修改 JavaScript 中的数据,页面就能自动“魔法般”地更新。理解它的演进,是高级 Vue 开发者的必修课。
1.1 Vue 2 的时代眼泪:Object.defineProperty
Vue 2 在组件初始化时,通过递归遍历 data 对象的所有属性,使用 Object.defineProperty 将它们重写为 getter/setter。
- 读取数据 (
getter):触发依赖收集,把当前正在渲染该数据的组件 (Watcher) 记录下来。 - 修改数据 (
setter):触发更新派发,通知所有记录过的组件重新渲染。
🚨 致命物理缺陷: 由于 defineProperty 只能劫持“已经存在”的属性,这导致了 Vue 2 的两大约束:
- 对象新增/删除属性无效:必须使用
Vue.set(obj, 'newProp', 123)。 - 通过索引修改数组无效:必须使用
arr.splice()。
1.2 Vue 3 的革命:Proxy
Vue 3 彻底抛弃了 defineProperty,全面拥抱 ES6 的 Proxy 对象。
Proxy可以直接代理整个对象(包括对象中未来新增的属性),而不再是劫持单个属性。- 完美支持了数组索引修改、
length修改,甚至对Map、Set等数据结构也能完美监听。 - 极大提升了组件初始化的性能,因为不再需要一开始就深度递归整个数据树。
2. 核心范式:计算属性与侦听器
处理由基础数据衍生出来的逻辑时,Vue 提供了两大利器,认清它们的边界极其重要。
2.1 Computed (计算属性) -> 衍生数据处理
- 特性:自带缓存。如果依赖的数据没变,调用一百次也只计算一次,直接返回缓存值。
- 应用场景:比如购物车总价(单价 * 数量)、过滤后的列表。
- 红线原则:绝对不要在 computed 中执行异步请求或修改其他状态(不要有副作用)!它必须是一个纯粹的“计算并返回结果”的过程。
2.2 Watch (侦听器) -> 副作用处理
- 特性:无缓存,数据变一次就执行一次。
- 应用场景:监听数据变化后执行异步操作(如搜索框输入后发送 Ajax 请求)、修改 DOM 或操作其他非响应式变量。
- 高级用法:
deep: true(深度监听对象内部属性)、immediate: true(组件一创建就立刻执行一次回调)。
3. 核心进阶:虚拟 DOM 与渲染机制
为什么不直接操作原生 DOM? 频繁操作原生 DOM 会引发浏览器的重排(Reflow)和重绘(Repaint),性能极差。
- Virtual DOM (VNode):Vue 在内存中用普通的 JavaScript 对象来描述 DOM 结构。
- Diff 算法:当响应式数据变化时,Vue 会生成一棵“新树”,并与“老树”进行对比(Diff)。
- Patch (打补丁):Diff 算法极其聪明,它只对比同层级节点。找出最小的差异后,一次性将变化应用到真实 DOM 上。
🔑 为什么 v-for 必须绑定 :key?key 是虚拟 DOM 节点的身份证。当列表发生顺序改变(如拖拽排序)或中间插入数据时,如果没有 key,Vue 会傻傻地就地修改元素内容;有了 key,Diff 算法能精准复用并移动旧节点,极大提升渲染性能,并防止内部状态错乱。
4. 核心架构:组件化与逻辑复用
将页面拆分成组件是工程化的第一步,而组件间的通信和逻辑提取是最大的挑战。
4.1 组件通信全家桶
- 父 -> 子:
props(必须遵守单向数据流原则,子组件绝对不能直接修改 props)。 - 子 -> 父:
$emit自定义事件(子组件说:“我被点击了,并传给你这个数据,父组件你自己决定怎么处理吧”)。 - 跨层级透传:
provide/inject(爷爷组件把数据直接扔给曾孙子组件,无论中间隔了多少层)。 - 全局状态:Pinia 或 Vuex。
4.2 逻辑复用的终极形态:Composition API (Vue 3 独有)
- Vue 2 的痛点 (
Mixins):在使用 Mixin 复用逻辑时,常常发生数据命名冲突,且不知道数据到底是从哪个 Mixin 里来的,这叫“隐式依赖”。 - Vue 3 的解法 (
setup+ Hooks):利用ref和reactive,我们可以把一个业务逻辑(比如:获取鼠标当前位置)封装成一个纯函数useMouse()。在任意组件中导入并调用即可,来源清晰,天然支持 TypeScript,完美解决了逻辑复用难题。
5. 核心周边:前端路由与状态管理
真正的企业级 Vue 项目离不开它的官方两大生态基石。
5.1 Vue Router (前端路由)
拦截浏览器的默认跳转行为,实现不刷新页面也能切换视图(SPA 核心)。
- Hash 模式:URL 里带
#(如app.com/#/user)。兼容性最好,因为#后面的改变不会发送给服务器,纯前端处理。 - History 模式:URL 干净漂亮(如
app.com/user)。但存在致命隐患:用户在子页面刷新时,浏览器会向服务器请求这个假路径,导致 404。必须在后端 Nginx 配置try_files $uri /index.html兜底。
5.2 Pinia (新一代状态管理)
Vuex 已经被官方软性淘汰,Pinia 是现在唯一的标准。
- 抛弃了繁琐的 mutations:在 Pinia 中,你可以在
actions里直接进行同步或异步的状态修改,心智负担锐减。 - 极佳的 TS 支持:自动推导类型。
- 扁平化架构:不再有臃肿的单一根 Store,你可以创建无数个独立的 Store(如
useUserStore,useCartStore)。
6. 常见问题 (FAQ) 与避坑指南
6.1 v-if 和 v-show 有什么本质区别?我该怎么选?
- 答:
v-if(条件渲染):它是“真实的”。如果条件为假,它会彻底销毁对应的 DOM 节点和组件实例;条件为真时重新创建。切换开销巨大。v-show(样式切换):不管真假,DOM 节点始终会被渲染并保留在页面中。它仅仅是通过修改 CSS 的display: none / block来控制显示隐藏。- 选择策略:如果是非常频繁地(比如弹窗的开关、Tabs 切换)切换显示状态,无脑用
v-show;如果条件在运行时几乎不怎么改变(比如用户权限校验),用v-if可以减轻初始渲染的负担。
6.2 什么是 nextTick?为什么在修改数据后立刻去获取 DOM 的高/宽,拿到的还是旧的?
- 答:这是 Vue 面试最高频考点之一。
- 底层机制:Vue 的 DOM 更新是异步的。当你执行
this.msg = '新内容'时,DOM 并没有立刻改变。Vue 开启了一个异步微任务队列,把所有的状态修改汇总起来(去重合并),在下一次 Event Loop 的时机才会统一去更新真实 DOM。 - 如何避坑:如果你修改了数据,且立刻需要基于更新后的 DOM 进行操作(比如让刚显示出来的输入框 focus),必须把 DOM 操作写在
$nextTick的回调函数里:
jsthis.showInput = true; this.$nextTick(() => { this.$refs.myInput.focus(); // 此时真实 DOM 已经更新完毕,绝对安全 }); - 底层机制:Vue 的 DOM 更新是异步的。当你执行
6.3 在组件里绑定了一个全局事件 window.addEventListener('resize', xxx),离开页面时忘记解绑会怎样?
- 答:会导致极其严重的内存泄漏 (Memory Leak)。
- 原因:组件虽然被销毁了,但是浏览器的
window对象依然存在。window上挂载着对组件内部方法xxx的引用,导致垃圾回收机制 (GC) 无法回收该组件占用的内存。如果你反复进出这个页面,内存会直线飙升直到浏览器崩溃。 - 避坑指南:在 Vue 2 的
beforeDestroy或 Vue 3 的onBeforeUnmount生命周期钩子里,必须手动调用window.removeEventListener('resize', xxx)进行物理擦屁股。
- 原因:组件虽然被销毁了,但是浏览器的
6.4 为什么 Vue 官方强烈禁止把 v-if 和 v-for 写在同一个 HTML 标签上?
- 答:因为优先级带来的性能灾难。
- 在 Vue 2 编译器中,
v-for的优先级比v-if高。即使你想用v-if="false"隐藏掉整个列表,Vue 也会在底层先傻傻地把包含一万条数据的列表全部循环创建出虚拟节点,然后挨个判断v-if为假并销毁它们。 - 终极解法:在外部 JS 逻辑中利用
computed计算属性,先过滤出你需要显示的数据数组,然后在模板里只循环渲染那个干净的计算属性数组。
- 在 Vue 2 编译器中,