精通 Vue 2 核心原理,关键在于从"会用"转向"能解释、能追溯、能复现"。你不必背下所有源码,但需要能清晰回答:数据变化后,Vue 如何知道要更新视图?视图更新的最小代价是如何计算的?模板如何变成可执行的代码?
一句话判断标准:
给你一段 Vue 2 代码,你能在脑中推演出从"修改一个
data属性"到"真实 DOM 更新"的完整链路,并解释每个环节的机制与边界。
下面按五大核心机制 + 源码学习方法展开,并附上一份可操作的进阶路径。
🧭 先建立心智地图:Vue 2 核心原理包含什么
| 核心模块 | 回答的核心问题 | 关键概念 |
|---|---|---|
| 响应式系统 | 数据变化,Vue 怎么知道? | Object.defineProperty、Dep、Watcher、依赖收集、派发更新 |
| 虚拟 DOM 与 Diff | 知道变化后,如何最小化更新真实 DOM? | VNode、patch、sameVnode、双端对比 |
| 模板编译 | template 如何变成可执行的代码? |
parse → AST、optimize → 静态标记、generate → render 函数 |
| 组件化 | 组件如何独立、可复用、可通信? | 生命周期、Vue.extend、配置合并、事件机制 |
| 异步更新与 nextTick | 多次改数据,为什么只更新一次? | 更新队列、去重、微任务/宏任务降级 |
这五大模块构成了一个闭环 :响应式系统 负责感知 变化,异步更新队列 负责调度 ,虚拟 DOM + Diff 负责高效计算 ,模板编译 提供了执行起点 ,组件化 则是这一切在组件粒度上的封装。
⚙️ 模块一:响应式系统------数据劫持与依赖管理
这是 Vue 2 一切魔法的基础。
1. 核心三件套:Observer、Dep、Watcher
Observer:在new Vue()初始化时,递归遍历data,使用Object.defineProperty将每个属性转换为 getter/setter 。getter 负责收集依赖 ,setter 负责派发更新。这是 ES5 的特性,也决定了 Vue 2 无法通过 polyfill 支持 IE8 及以下。Dep:每个响应式属性(更准确地说是每个被defineReactive处理过的 key)都通过闭包维护一个Dep实例。Dep是一个依赖收集器 ,内部用一个subs数组存放所有依赖该属性的Watcher。Watcher:代表一个依赖该数据的"观察者" 。在 Vue 2 中,每个组件实例对应一个渲染 Watcher 。当组件渲染时,会"触碰"到响应式数据的 getter,此时Watcher会被Dep收集起来。此外,computed和watch也会创建各自的Watcher。
2. 完整链路:一次数据修改发生了什么?
- 收集依赖(初始化/渲染时) :渲染
Watcher执行render函数 → 访问data属性 → 触发 getter → 当前Dep将渲染Watcher收集到自己的subs数组中。 - 派发更新(数据修改时) :修改
data属性 → 触发 setter → 通知Dep→Dep遍历subs,调用每个Watcher的update方法。 - 调度执行 :
Watcher.update并不会立即重新渲染,而是将自身推入一个异步更新队列(见模块五)。
3. 必须理解的"检测边界"
Vue 2 的 Object.defineProperty 方案存在固有边界,这也是原理层面的高频追问:
- 对象 :无法检测属性的新增 和删除 。必须使用
Vue.set/vm.$set和Vue.delete来触发响应式。 - 数组 :无法通过索引直接设置项(
arr[0] = newVal)或修改length来触发更新。Vue 通过重写数组的 7 个变更方法 (push、pop、shift、unshift、splice、sort、reverse)来拦截这些操作。
学到什么程度 :能手写一个极简的 Observer + Dep + Watcher 模型,并解释 Vue.set 为什么能突破对象新增属性的限制。
🌳 模块二:虚拟 DOM 与 Diff------最小化 DOM 操作
响应式系统只负责"通知",真正计算"改哪里"的是 Diff 算法。
1. 核心概念:VNode 与 patch
VNode:用 JavaScript 对象描述真实 DOM 的轻量结构,包含tag、data、children、text等字段。patch:对比新旧两棵VNode树,找出差异并执行最小化 DOM 操作的过程。Vue 2 的 Diff 算法由 Snabbdom 修改而来,复杂度为 O(n)。
2. Diff 的核心策略:同层比较 + 双端对比
- 同层比较 :Vue 2 的 Diff 采用深度优先、同层比较的策略。它不会跨层级移动节点,如果父节点不同,会直接销毁旧子树、创建新子树,而不是尝试复用跨层的节点。
sameVnode判断 :在对比子节点列表时,先通过key和tag等判断两个VNode是否为"相同"节点。如果相同,则复用真实 DOM 元素并进行patchVnode更新;如果不同,则创建新节点、删除旧节点。- 双端对比 :在
updateChildren中,Vue 2 使用首尾指针 的方式,通过 4 次比较(旧首 vs 新首、旧尾 vs 新尾、旧首 vs 新尾、旧尾 vs 新首)来快速判断节点的复用情况,这是 Diff 高效的关键。
3. 为什么 key 很重要?
key 是 sameVnode 判断的重要依据。在列表渲染中,使用稳定的 key 可以帮助 Diff 算法正确复用节点,避免因索引变化导致的状态错乱 和不必要的销毁重建。这也是原理与最佳实践的直接连接点。
学到什么程度 :能用文字或伪代码描述 updateChildren 的双端对比流程,并能解释"为什么在列表中使用索引作为 key 可能导致 bug"。
🛠️ 模块三:模板编译------从 template 到 render
你在 .vue 文件中写的 <template>,并不是浏览器直接执行的,Vue 2 在运行时或构建时将其编译为 render 函数。
1. 渲染三巨头:parse → optimize → generate
parse:使用正则解析模板字符串,将标签、指令、属性、文本等转换为抽象语法树(AST)。AST 是用数据形式描述模板特征的结构。optimize:遍历 AST,标记静态节点 (不包含任何动态绑定的节点)。静态节点在后续更新中可以被完全跳过,这是 Vue 2 模板编译层面最重要的性能优化。staticRoot用于标记静态子树的根,使得 Diff 时可以整树跳过。generate:将优化后的 AST 转换为render函数的字符串 ,例如_c('div',[_c('span'),_v(num)])。这个字符串最终会被new Function转换为可执行的函数。
2. 运行时 vs 构建时编译
- 完整版(
vue.js) :包含编译器,可以在浏览器中编译template,体积较大。 - 运行时版(
vue.runtime.js) :不包含编译器,体积更小,需要配合vue-loader在构建时完成编译。理解这个区别,能解释为什么在大型项目中推荐使用.vue单文件组件 + 构建时编译。
学到什么程度 :能手写一个简化版的 parse,将一段简单 HTML 模板转换为 AST 对象;能解释静态标记如何提升 Diff 性能。
🧩 模块四:组件化------独立单元的生命周期与通信
组件是 Vue 2 中一切原理运行的"容器"。
1. 生命周期:从创建到销毁的完整流程
Vue 2 的生命周期分为四个阶段,每个钩子都对应着内部特定的初始化或清理工作:
| 阶段 | 关键钩子 | 内部发生了什么 |
|---|---|---|
| 创建 | beforeCreate → created |
初始化事件、生命周期、data 响应式、methods、computed、watch |
| 挂载 | beforeMount → mounted |
模板编译(如需要)→ 创建渲染 Watcher → 首次 render → patch 生成真实 DOM |
| 更新 | beforeUpdate → updated |
数据变化 → 调度渲染 Watcher → 重新 render → patch 更新 DOM |
| 销毁 | beforeDestroy → destroyed |
解除所有 Watcher、移除事件监听器、销毁子组件 |
父子组件生命周期顺序 是高频考点:创建阶段 由外到内(父 created → 子 created),挂载阶段 由内到外(子 mounted → 父 mounted)。
2. 配置合并与 Vue.extend
- 当你使用
Vue.component或components: { ... }注册组件时,Vue 内部会通过Vue.extend基于基础 Vue 构造函数创建一个子类。 - 组件初始化时,会执行选项合并 :将全局混入(
mixin)、父组件传入的配置、组件自身配置进行合并,这是mixins和extends生效的原理基础。
3. 事件机制:$on / $emit 与 $emit 的底层
每个 Vue 实例都是一个事件中心 (Event Bus 的基础)。$on 将回调注册到实例的 _events 对象中,$emit 根据事件名查找并依次执行回调。父子组件通信(props / $emit)在运行时正是基于这一套事件机制。
学到什么程度 :能手写一个极简的 Vue.extend 和选项合并逻辑;能画出父子组件在创建、挂载、更新、销毁各阶段的生命周期执行顺序。
⏳ 模块五:异步更新队列与 nextTick
这是连接"数据变化"与"DOM 更新"的调度器。
1. 为什么需要异步更新?
如果每次修改数据都同步触发重新渲染,一个同步代码块中修改 N 次数据,就会触发 N 次渲染,造成巨大的性能浪费。Vue 2 的做法是:将同一事件循环中触发的所有 Watcher 推入一个队列,去重后,在下一个微任务(或宏任务)中一次性执行 。这保证了无论一个事件循环中修改了多少次数据,组件都只重新渲染一次。
2. nextTick 的原理与降级策略
Vue.nextTick(cb) 的作用是将回调推迟到下一次 DOM 更新循环之后执行 。在修改数据后立即使用 $nextTick,可以获取到更新后的真实 DOM。
其底层实现是一个异步任务调度器,会按以下顺序尝试降级,以兼容不同环境:
Promise.then(微任务,现代浏览器首选)MutationObserver(微任务,兼容旧版 Android)setImmediate(宏任务,IE / Node.js)setTimeout(fn, 0)(宏任务,最终兜底)
学到什么程度 :能完整解释"修改数据 → Watcher 入队 → 去重 → 微任务中 flushQueue → 执行 Watcher.run → patch → DOM 更新"的完整时序,并能说出 nextTick 在微任务和宏任务之间切换的原因。
🧪 如何学习源码:读、调、写、画
1. 从"小"入手,不要通读全文
Vue 2 源码体量庞大,通读容易迷失。正确的策略是按模块拆解,一次只盯一个机制 。例如,先只看 src/core/observer/ 目录,理解 Observer、Dep、Watcher 三个文件之间的关系。
2. 调试优于阅读
在本地克隆 Vue 2 源码后,使用 http-server 或配置 sourcemap,在浏览器中打断点,跟随调用栈观察一个具体功能的完整流程。例如,在 defineReactive 的 getter 中打断点,看它何时被谁触发。
3. 手写迷你版,是检验理解的唯一标准
"能读懂"和"能写出来"之间有巨大鸿沟。推荐从以下顺序手写:
Observer+Dep+Watcher:实现数据变化 → 视图更新的最小闭环。patch的简化版 :实现基于sameVnode和简单双端对比的 Diff。parse的简化版:将简单模板解析为 AST。Vue.extend+ 选项合并:理解组件构造函数的创建。
开源社区有 mini-vue2 等参考项目,可以对照学习,但务必自己动手写一遍。
4. 画图,而不是只做笔记
用流程图画出:
- 响应式系统的依赖收集与派发更新链路
- 从
new Vue()到mounted的完整初始化流程 updateChildren的双端对比指针移动图
图能暴露你理解中的断点。
🗺️ 可操作的学习路径(建议 6-8 周)
| 阶段 | 时间 | 核心任务 | 检验标准 |
|---|---|---|---|
| 1. 响应式闭环 | 第 1-2 周 | 通读 Observer/Dep/Watcher 相关源码;手写迷你响应式模型;理解 Vue.set 原理 |
能手写一个数据变化驱动视图更新的 demo |
| 2. 虚拟 DOM 与 Diff | 第 3 周 | 阅读 patch 和 updateChildren 源码;手写简化版 Diff;深入理解 key 的作用 |
能解释双端对比的 4 次比较,能说出索引 key 的问题 |
| 3. 模板编译 | 第 4 周 | 阅读 parse/optimize/generate;手写简化版 parse;理解静态标记 |
能将简单模板手动推导为 render 函数字符串 |
| 4. 组件化与生命周期 | 第 5 周 | 阅读组件初始化、Vue.extend、选项合并源码;画出父子生命周期顺序 |
能手写极简 Vue.extend,能准确说出父子生命周期顺序 |
| 5. 异步更新与 nextTick | 第 6 周 | 阅读 next-tick.js 和 scheduler;理解宏微任务降级 |
能完整描述一次数据修改到 DOM 更新的微任务时序 |
| 6. 综合与输出 | 第 7-8 周 | 写一篇源码分析文章;或手写一个包含以上核心机制的 Mini-Vue2 | 能向他人清晰讲解 Vue 2 的核心运行链路 |
💎 验证自己"精通"了的标志
能全部做到,说明 Vue 2 核心原理已经扎实:
- 能画出
Observer、Dep、Watcher三者协作的完整依赖关系图。 - 能解释
Vue.set和数组重写方法分别解决了Object.defineProperty的什么局限。 - 能描述
updateChildren的双端对比流程,并说明key在其中的作用。 - 能说出
parse、optimize、generate各自负责什么,以及静态标记如何提升性能。 - 能准确列出父子组件生命周期钩子的执行顺序,并解释每个阶段内部做了什么。
- 能完整推演一次数据修改到 DOM 更新的微任务时序,并解释
nextTick的降级策略。 - 能手写一个不超过 200 行、包含响应式和简易 Diff 的 Mini-Vue 模型。
Vue 2 的核心原理是一套自洽的、数据驱动的视图更新体系 。精通它,不仅是为了应对面试,更是为了在遇到"数据变了视图没变""列表渲染错乱""性能瓶颈"时,能直接定位到原理层面的原因,而不是停留在"试一下 $set"或"换个 key"的经验层面。