源码要点
面试 Vue 源码时,考察的核心是对 Vue 底层原理的理解,包括响应式系统、虚拟 DOM、模板编译、组件生命周期等核心机制。以下是必须掌握的关键知识点,结合 Vue 2 和 Vue 3 的核心差异进行整理:
一、响应式系统原理(Vue 2 vs Vue 3)
响应式是 Vue 的核心特性,也是面试高频考点,需掌握 Vue 2 和 Vue 3 的实现差异。
1. Vue 2 响应式实现
- 核心 API:
Object.defineProperty(劫持对象属性的getter/setter)。 - 实现流程:
- 通过
Observer类将数据(data)递归转为响应式对象(遍历属性,用Object.defineProperty重写get和set)。 getter中通过Dep(依赖管理器)收集依赖(Watcher),setter中触发Dep通知所有依赖更新。
- 通过
- 关键角色:
Observer:将数据转为响应式(数组通过重写原型方法实现响应式,如push、splice)。Dep:每个响应式属性对应一个Dep,维护依赖列表(Watcher),提供depend(收集)和notify(通知)方法。Watcher:依赖的具体载体(组件渲染、watch、computed都会创建Watcher),接收更新通知后执行回调(如重新渲染组件)。
- 局限性:
- 无法监听对象新增/删除的属性(需用
Vue.set/$set手动处理)。 - 无法监听数组索引修改和长度变化(需通过重写的数组方法触发更新)。
- 无法监听对象新增/删除的属性(需用
2. Vue 3 响应式实现
- 核心 API:
Proxy(代理整个对象,而非单个属性)+Reflect(反射操作)。 - 实现流程:
- 通过
reactive函数创建对象的Proxy代理,拦截get(读取)、set(修改)、deleteProperty(删除)等操作。 get时收集依赖(track函数),set/deleteProperty时触发依赖更新(trigger函数)。
- 通过
- 改进点:
- 天然支持对象新增/删除属性、数组索引修改和长度变化。
- 无需递归初始化为响应式(
Proxy懒代理,访问深层属性时才递归代理)。
- 关键 API 差异:
reactive:处理对象/数组,返回 Proxy 代理。ref:处理基本类型,通过.value访问,底层用reactive包装成{ value: ... }。
二、虚拟 DOM 与 Diff 算法
虚拟 DOM 是 Vue 实现跨平台渲染和高效更新的核心,Diff 算法是优化渲染性能的关键。
1. 虚拟 DOM 原理
- 定义:用 JavaScript 对象(
VNode)描述真实 DOM 结构(如tag、props、children等属性)。 - 作用:
- 跨平台:同一
VNode可渲染为 DOM(浏览器)、原生组件(React Native)等。 - 减少 DOM 操作:通过 Diff 算法计算最小更新量,批量执行 DOM 操作。
- 跨平台:同一
- VNode 结构(简化):
{
tag: 'div', // 标签名
props: { class: 'box' }, // 属性
children: [/* 子 VNode */], // 子节点
key: '1', // 用于 Diff 算法
el: null, // 对应真实 DOM 元素
// 其他标识:静态节点标记、组件类型等
}
2. Diff 算法核心逻辑
- 核心目标:对比新旧
VNode树,找出差异并更新真实 DOM,尽可能复用现有节点。 - Vue 2 Diff 特点:
- 同级比较:只对比同一层级的节点,不跨层级比较(降低复杂度,从 O(n³) 优化为 O(n))。
- key 的作用:作为节点唯一标识,帮助 Diff 算法快速找到可复用节点(无 key 时可能导致节点错误复用,如列表渲染)。
- 流程:
- 先比较节点是否相同(
tag和key一致)。 - 不同则直接替换节点;相同则递归比较
props和children。
- 先比较节点是否相同(
- Vue 3 Diff 优化:
- 静态标记:编译时标记静态节点(如纯文本、无动态绑定的节点),Diff 时直接跳过。
- 最长递增子序列:列表 Diff 时,通过该算法减少节点移动次数(Vue 2 是暴力对比,移动次数更多)。
三、模板编译过程
Vue 的模板(template)需要编译为渲染函数(render)才能执行,编译过程是连接模板和虚拟 DOM 的桥梁。
1. 编译三阶段
-
解析(Parse):将模板字符串解析为抽象语法树(AST)。
- 核心:通过正则匹配标签、属性、文本等,递归构建 AST 节点(如元素节点、文本节点、注释节点)。
- 处理:解析指令(
v-if、v-for)、插值({{ }})、事件(@click)等。
-
优化(Optimize):标记 AST 中的静态节点和静态根节点,避免 Diff 时重复处理。
- 静态节点:不包含动态绑定(如
<div>静态文本</div>),渲染后永远不变。 - 静态根节点:包含静态子节点的非叶子节点,可整体跳过 Diff。
- 静态节点:不包含动态绑定(如
-
生成(Generate):将优化后的 AST 转换为渲染函数(
render函数)。- 渲染函数执行后返回
VNode树,例如:// 模板 <div>{{ message }}</div> 编译为:
function render(h) {
return h('div', this.message);
}
- 渲染函数执行后返回
2. Vue 3 编译时优化
Vue 3 强化了编译时优化,通过编译阶段的分析减少运行时 Diff 开销:
- PatchFlag:为动态节点添加标记(如文本动态、属性动态),Diff 时只处理带标记的节点。
- hoistStatic:将静态节点的创建逻辑提升到渲染函数外,避免每次渲染重新创建。
- cacheHandler:缓存事件处理函数,避免每次渲染创建新函数导致的不必要更新。
四、组件化与生命周期
组件是 Vue 的核心概念,需理解组件的初始化流程和生命周期钩子的触发时机。
1. 组件初始化流程(Vue 2)
- 整体流程:
new Vue()→ 初始化(init)→ 编译($mount)→ 渲染(render)→ 挂载(mount)。 - 关键步骤:
- 初始化阶段:
initState(初始化data、props、computed、watch)、initEvents(初始化事件)。 - 挂载阶段:
$mount调用compileToFunctions编译模板为render函数,执行render生成VNode,再通过patch函数将VNode转为真实 DOM 并挂载到页面。 - 更新阶段:数据变化触发
setter→Dep.notify→Watcher.update→ 重新执行render生成新VNode→patch对比新旧VNode并更新 DOM。
- 初始化阶段:
2. 生命周期钩子实现
- 原理:生命周期钩子本质是在组件初始化的特定阶段执行的回调函数,由 Vue 内部在对应时机触发。
- Vue 2 关键钩子触发时机:
beforeCreate:初始化data、props之前(无法访问this.data)。created:data、props初始化完成(可访问数据,但 DOM 未挂载)。beforeMount:render函数已生成,但未挂载到 DOM。mounted:真实 DOM 挂载完成(可访问 DOM 元素)。beforeUpdate:数据更新后,DOM 重新渲染前。updated:DOM 重新渲染完成。beforeDestroy:组件销毁前(可做清理工作,如清除定时器)。destroyed:组件销毁完成(所有事件监听、子组件均已移除)。
- Vue 3 生命周期变化:
- 组合式 API 中用
onMounted、onUpdated等函数替代选项式钩子,内部通过注册回调到组件实例实现。
- 组合式 API 中用
五、computed 与 watch 原理
computed 和 watch 是处理数据依赖和变化的核心 API,需理解其实现差异。
1. computed 原理
- 核心特性:缓存(依赖不变时,多次访问返回缓存结果)、响应式(依赖变化时自动重新计算)。
- 实现:
computed本质是一个特殊的Watcher(ComputedWatcher),其lazy选项为true(延迟计算)。- 首次访问
computed属性时,触发getter并执行计算函数,收集依赖(依赖的响应式数据)。 - 依赖变化时,标记
computed为“脏”(dirty: true),下次访问时重新计算并更新缓存。
2. watch 原理
- 核心特性:监听数据变化并执行副作用(支持异步操作),可配置
deep(深度监听)、immediate(立即执行)。 - 实现:
- 每个
watch对应一个Watcher(UserWatcher),初始化时会读取一次监听的属性,触发getter收集依赖。 - 当监听的属性变化时,
Watcher触发回调函数(handler)。 - 深度监听:通过
traverse函数递归遍历对象,收集所有子属性的依赖,确保深层变化能被捕获。 - 立即执行:
immediate: true时,初始化后直接执行一次handler。
- 每个
六、全局 API 与实例方法
关键 API 的实现原理能体现对源码的理解深度。
1. Vue.set / this.$set(Vue 2)
- 作用:为响应式对象新增属性并触发更新(解决
Object.defineProperty无法监听新增属性的问题)。 - 原理:
- 若目标是数组,调用重写的
splice方法(会触发setter)。 - 若目标是对象,判断属性是否已存在:存在则直接赋值;不存在则用
Object.defineProperty为该属性添加getter/setter,并触发Dep.notify通知更新。
- 若目标是数组,调用重写的
2. nextTick 原理
- 作用:在下次 DOM 更新循环结束后执行回调(常用于修改数据后获取更新后的 DOM)。
- 原理:
- 基于 JavaScript 的事件循环(Event Loop),优先使用微任务(
Promise.then、MutationObserver),降级使用宏任务(setImmediate、setTimeout)。 - Vue 内部维护一个回调队列,
nextTick将回调加入队列,DOM 更新后(flushSchedulerQueue)批量执行队列中的回调。
- 基于 JavaScript 的事件循环(Event Loop),优先使用微任务(
3. v-model 原理
- 作用:表单元素的双向绑定(语法糖)。
- 原理:
- 对普通表单元素(如
input):等价于:value="xxx"+@input="xxx = $event.target.value"。 - 对组件:等价于
:value="xxx"+@input="xxx = $event",子组件通过$emit('input', newValue)触发更新。
- 对普通表单元素(如
- Vue 3 变化:支持自定义
modelValue和update:modelValue事件,更灵活(如<Child v-model:title="title" />)。
七、Vue 3 重要新特性原理
Vue 3 基于 Composition API 重构,需掌握核心变化的实现。
1. Composition API 实现
- setup 执行时机:在
beforeCreate之前执行,此时组件实例未完全初始化(this为undefined)。 - 响应式集成:
setup中通过ref/reactive创建响应式数据,返回后暴露给模板和其他选项。 - 生命周期钩子:
onMounted等函数内部通过getCurrentInstance获取当前组件实例,将钩子回调注册到实例的生命周期队列中。
2. 模块化设计
Vue 3 采用 monorepo 架构,将核心功能拆分为独立模块:
@vue/reactivity:响应式系统(reactive、ref、effect等)。@vue/compiler-core:模板编译核心逻辑。@vue/runtime-core:运行时核心(虚拟 DOM、组件渲染等)。- 各模块解耦,可单独使用(如用
@vue/reactivity作为独立的响应式库)。
八、常见面试题延伸
- 为什么 data 必须是函数?:避免组件复用时代理同一个对象(引用类型共享),函数每次返回新对象确保实例独立。
- v-if 和 v-show 的区别?:
v-if是动态创建/销毁节点(编译时条件渲染),v-show是通过display: none控制显示(始终渲染节点)。 - Vue 2 中数组为什么不能通过索引修改?:
Object.defineProperty无法监听数组索引的setter,需通过重写push、splice等方法触发更新。 - Vue 3 性能提升在哪里?:
Proxy响应式优化、编译时静态标记、Diff 算法优化(最长递增子序列)、按需引入等。
总结
面试 Vue 源码的核心是理解“数据驱动视图”的完整链路:从响应式数据的创建(Observer/Proxy),到模板编译为渲染函数,再到虚拟 DOM 的生成与 Diff 算法,最终通过生命周期管理组件的渲染与更新。需重点掌握 Vue 2 和 Vue 3 的实现差异,尤其是响应式系统和编译优化的演进。