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 运行时、生命周期信号

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

相关推荐
麦麦大数据17 小时前
F032 材料科学文献知识图谱可视化分析系统(四种知识图谱可视化布局) | vue + flask + echarts + d3.js 实现
vue.js·flask·知识图谱·数据可视化·论文文献·1024程序员节·科研图谱
web打印社区18 小时前
使用React如何静默打印页面:完整的前端打印解决方案
前端·javascript·vue.js·react.js·pdf·1024程序员节
喜欢踢足球的老罗18 小时前
[特殊字符] PM2 入门实战:从 0 到线上托管 React SPA
前端·react.js·前端框架
小光学长18 小时前
基于Vue的课程达成度分析系统t84pzgwk(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
前端·数据库·vue.js
Baklib梅梅19 小时前
探码科技再获“专精特新”认定:Baklib引领AI内容管理新方向
前端·ruby on rails·前端框架·ruby
南方以南_19 小时前
Chrome开发者工具
前端·chrome
YiHanXii20 小时前
this 输出题
前端·javascript·1024程序员节
Dream it possible!20 小时前
LeetCode 面试经典 150_链表_合并两个有序链表(58_21_C++_简单)
leetcode·链表·面试·1024程序员节
楊无好20 小时前
React中ref
前端·react.js
程琬清君20 小时前
vue3 confirm倒计时
前端·1024程序员节