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

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

相关推荐
慢半拍iii43 分钟前
JAVA Web —— A / 网页开发基础
前端
gnip1 小时前
pnpm 的 monorepo架构多包管理
前端·javascript
42fourtytoo1 小时前
天津大学智算2026预推免机试第二批题目及代码c++
开发语言·c++·面试
新手村领路人2 小时前
Firefox自定义备忘
前端·firefox
乖女子@@@2 小时前
css3新增-网格Grid布局
前端·css·css3
百思可瑞教育3 小时前
使用UniApp实现一个AI对话页面
javascript·vue.js·人工智能·uni-app·xcode·北京百思可瑞教育·百思可瑞教育
伐尘3 小时前
【CE】图形化CE游戏教程通关手册
前端·chrome·游戏·逆向
不想吃饭e3 小时前
在uniapp/vue项目中全局挂载component
前端·vue.js·uni-app
非凡ghost3 小时前
AOMEI Partition Assistant磁盘分区工具:磁盘管理的得力助手
linux·运维·前端·数据库·学习·生活·软件需求
UrbanJazzerati4 小时前
前端入门:margin居中、border、box-radius、transform、box-shadow、mouse事件、preventDefault()
前端·面试