"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 运行时、生命周期信号 。
这五个子系统协同完成"数据驱动视图"的承诺,也构成了我们在面试桌上、调试器中、性能分析工具里反复看到的那张全景图。