Vue 2 渲染链路剖析

"new Vue 之后发生了什么?"

"数据改变后,Vue 又做了什么?"

两个问题看似老生常谈,却足以勾勒出 Vue 2 整个运行时架构:
实例化、响应式系统、调度器、虚拟 DOM、补丁算法、组件递归、生命周期信号

下文以一次完整的 mount → render → patch → update 链路为主线,给出逐帧级别的剖析。

1. 实例化阶段

1.1 前期准备

构造函数内部首先执行 _init,其职责包括

  1. 规范化选项 :调用 mergeOptions 把全局 mixin、extends、mixins 与实例选项融合。
  2. 建立内部属性树initLifecycle 构建 $parent / $children / $refs 等指针;initEvents 建立事件中心;initRender 绑定 createElement 别名并声明 $slots$scopedSlots
  3. 生命周期钩子 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 挂载与首帧渲染

  1. beforeMount :此时 vm.$el 已指向挂载点,但仍是原始占位节点。

  2. 创建 渲染 Watcher

    js 复制代码
    new Watcher(
      vm,
      function updateComponent() {
        vm._update(vm._render(), hydrating);
      },
      noop,
      { isRenderWatcher: true }
    );

    该 Watcher 的求值函数 updateComponentrender → patch 封装成一次原子更新。

  3. 首次执行 updateComponent

    • _render() 执行 render.call(vm, ...),返回 VNode Tree
    • _update(prevVNode, nextVNode) 调用 __patch__。由于 prevVNode 为空,进入 create path :递归 createElm 生成真实节点,遇到组件 VNode 则递归进入子组件的实例化流程。
  4. 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 调度阶段

  1. beforeUpdate:此时 DOM 仍是旧状态,适合读取布局或手动保存滚动位置。
  2. 调度器执行渲染 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();

其内部顺序为

  1. beforeDestroy:实例仍完全可用;
  2. 递归销毁子组件;
  3. 解绑所有指令、事件、Watcher;
  4. 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 运行时、生命周期信号

这五个子系统协同完成"数据驱动视图"的承诺,也构成了我们在面试桌上、调试器中、性能分析工具里反复看到的那张全景图。

相关推荐
初遇你时动了情3 分钟前
JS中defineProperty/Proxy 数据劫持 vue3/vue2双向绑定实现原理,react 实现原理
javascript·vue.js·react.js
阿华的代码王国18 分钟前
【Android】RecyclerView实现新闻列表布局(1)适配器使用相关问题
android·xml·java·前端·后端
lovebugs25 分钟前
Java并发编程:深入理解volatile与指令重排
java·后端·面试
汪子熙41 分钟前
Angular 最新的 Signals 特性详解
前端·javascript
Spider_Man42 分钟前
前端路由双雄传:Hash vs. History
前端·javascript·html
南方kenny1 小时前
CSS Grid 布局:从入门到精通,打造完美二维布局
前端·javascript·css
小泡芙丫1 小时前
从买房到代码:发布订阅模式的"房产中介"之旅
前端·javascript
企鹅吧1 小时前
前端导出 pdf 与 跑马灯效果 最佳实践
前端·javascript·vue.js
南方kenny1 小时前
移动端适配的利器:lib-flexible 原理与实战
前端·javascript·react.js