"new Vue 之后发生了什么?"
"数据改变后,Vue 又做了什么?"
两个问题看似老生常谈,却足以勾勒出 Vue 2 整个运行时架构:
实例化、响应式系统、调度器、虚拟 DOM、补丁算法、组件递归、生命周期信号 。
下文以一次完整的 mount → render → patch → update 链路为主线,给出逐帧级别的剖析。

1. 实例化阶段
1.1 前期准备
构造函数内部首先执行 _init,其职责包括
- 规范化选项 :调用
mergeOptions把全局 mixin、extends、mixins 与实例选项融合。 - 建立内部属性树 :
initLifecycle构建$parent / $children / $refs等指针;initEvents建立事件中心;initRender绑定createElement别名并声明$slots、$scopedSlots。 - 生命周期钩子
beforeCreate:此时还未注入任何用户状态,无法访问data / computed / methods。
1.2 状态注入与响应式化
initState 按顺序处理
initProps:校验类型,把 props 变成响应式,并代理到 vm 实例;initMethods:绑定上下文;initData:遍历返回对象,通过observe创建Observer,递归地把每个属性转成getter / setter;initComputed:为每个计算属性创建惰性的Watcher;initWatch:对用户watch选项创建对应的Watcher。
随后触发 created。此时响应式系统已就绪,但尚未生成 DOM。
1.3 模板 → render 函数
$mount 流程根据平台区分:
- 若存在手写
render,直接使用; - 否则,运行时编译器执行
compileToFunctions,把模板字符串解析为 AST,再优化、生成代码字符串,最终通过new Function得到render函数。
1.4 挂载与首帧渲染
-
beforeMount:此时vm.$el已指向挂载点,但仍是原始占位节点。 -
创建 渲染 Watcher :
jsnew Watcher( vm, function updateComponent() { vm._update(vm._render(), hydrating); }, noop, { isRenderWatcher: true } );该 Watcher 的求值函数
updateComponent把 render → patch 封装成一次原子更新。 -
首次执行
updateComponent:_render()执行render.call(vm, ...),返回 VNode Tree。_update(prevVNode, nextVNode)调用__patch__。由于prevVNode为空,进入 create path :递归createElm生成真实节点,遇到组件 VNode 则递归进入子组件的实例化流程。
-
mounted:DOM 已插入文档,组件树整树可见。
2. 响应式系统
2.1 依赖收集
在渲染函数执行期间,任何对响应式属性的读取都会触发 Dep.depend():
当前 渲染 Watcher 被压入 Dep.subs 数组,完成"谁依赖我"的登记。
2.2 变更通知
响应式属性被写入时,setter → Dep.notify() 遍历 subs,调用每个 Watcher 的 update()。
渲染 Watcher 的 update 把自身放入 异步队列:
js
queueWatcher(this);
nextTick(flushSchedulerQueue) 把队列清空,确保同一轮事件循环内的多次数据变更只触发一次重渲染。
3. 重新渲染:diff 与补丁
3.1 调度阶段
beforeUpdate:此时 DOM 仍是旧状态,适合读取布局或手动保存滚动位置。- 调度器执行渲染 Watcher 的
run(),再次调用updateComponent。
3.2 重新求值
_render()生成新的 VNode 树;- 旧的依赖被 清除 (
resetDep()),新的依赖被再次收集,实现"按需追踪"。
3.3 虚拟 DOM diff
_update(prevVNode, nextVNode) 进入 patch:
- 同层比较:O(n) 时间复杂度,基于双端对比的优化策略;
- 节点类型不一致:直接替换,旧节点走销毁链路;
- 节点类型一致 :
- 普通元素 →
patchVnode,比对属性、子节点; - 组件 → 调用组件自身的
updateComponent,递归进入本流程; - 文本 → 直接替换
textContent。
- 普通元素 →
3.4 销毁链路
当 diff 算法发现组件需要被移除时,执行
js
oldComponentInstance.$destroy();
其内部顺序为
beforeDestroy:实例仍完全可用;- 递归销毁子组件;
- 解绑所有指令、事件、Watcher;
destroyed:实例与其 DOM 解耦,等待 GC。
3.5 更新后钩子
diff 与补丁完成后触发 updated,此时 DOM 已与新状态保持同步。
4. 全链路鸟瞰
text
new Vue(options)
├─ beforeCreate
├─ initState (props, data, computed, watch)
├─ created
├─ beforeMount
├─ render() → VNode
├─ patch(null, VNode) → 真实 DOM
└─ mounted
响应式属性变更
├─ setter → Dep.notify()
├─ Watcher.update() → queueWatcher
├─ nextTick → flushSchedulerQueue
├─ beforeUpdate
├─ render() → new VNode
├─ patch(oldVNode, newVNode)
└─ updated
5. 工程启示
- 在
created内进行网络请求可避免首屏阻塞; - 任何导致布局抖动的读取应在
beforeUpdate完成; - 避免在
updated中直接修改响应式数据,否则可能触发级联更新; - 组件级缓存(
keep-alive)通过复用实例,绕过完整的 mount → destroy 链路,显著提升性能。
结语
掌握 new Vue 之后的完整调用链,等价于把 Vue 2 运行时拆解成五个子系统:
选项合并、响应式追踪、异步调度器、虚拟 DOM 运行时、生命周期信号 。
这五个子系统协同完成"数据驱动视图"的承诺,也构成了我们在面试桌上、调试器中、性能分析工具里反复看到的那张全景图。