Skip to content

Vue 3 渲染器驱动核心:从数据变化到 DOM 更新的完整链路

本章深入剖析 Vue 3 组件实例、渲染副作用与异步调度的完整机制,揭示"自动更新"背后的底层原理。

前言

在前面的章节中,我们已经学习了 VNode 结构、Patch 路由和 Diff 算法。但这些机制都是"被动执行"的——当你修改一个响应式数据时,究竟是谁在驱动这些机制运转?更新是如何触发的?为什么多次修改数据只会触发一次重渲染?

本章将打通组件实例渲染副作用异步调度的完整闭环。


组件挂载流程:从 VNode 到实例

processComponent:路由分发的入口

patch 函数遇到组件类型的 VNode 时,会调用 processComponent

ts
// 伪代码
function processComponent(n1, n2, container) {
  if (n1 == null) {
    // 首次挂载
    if (n2.shapeFlag & COMPONENT_KEPT_ALIVE) {
      // KeepAlive 组件:激活缓存的实例
      parentComponent.ctx.activate(n2, container, ...)
    } else {
      // 普通组件:创建新实例
      mountComponent(n2, container, ...)
    }
  } else {
    // 更新现有组件
    updateComponent(n1, n2, ...)
  }
}

设计思路:KeepAlive 不需要创建新实例,而是复用已缓存的实例,这是性能优化的典型做法。

mountComponent:三步走的挂载策略

``ts // 伪代码 function mountComponent(vnode, container, parentComponent) { // Step 1:创建组件实例对象 const instance = createComponentInstance(vnode, parentComponent)

// Step 2:初始化 props、slots、setup() setupComponent(instance)

// Step 3:建立渲染副作用(连接响应式与渲染) setupRenderEffect(instance, vnode, container) }


三个清晰的步骤,体现了职责分离的设计哲学。

### createComponentInstance:实例的数据结构

一个组件实例本质上是一个包含所有信息的对象:

组件实例的六大类别:

【核心身份】 uid: 唯一标识符 vnode: 组件的 VNode type: 组件定义 parent: 父组件实例

【渲染相关】 render: 渲染函数 subTree: render 返回的 VNode(真实渲染内容) effect: 响应式副作用 update: 触发重渲染的函数

【代理与访问】 proxy: 公开代理(模板中的 this) ctx: 渲染上下文对象 accessCache: 属性访问缓存

【状态管理】 props: 父传递的属性 data: data() 返回的状态 setupState: setup() 返回的状态 slots: 插槽对象 attrs: 未声明的属性

【生命周期】 isMounted: 是否已挂载 isUnmounted: 是否已卸载 bm/m/bu/u/bum/um: 生命周期钩子


**关键区分**:

vnode vs subTree: vnode = ← 父组件中的表示 subTree =

← 组件 render 返回的内容


### setupComponent:状态初始化

``ts
// 伪代码
function setupComponent(instance) {
  // 1. 解析 props 和 slots
  initProps(instance, instance.vnode.props)
  initSlots(instance, instance.vnode.children)

  // 2. 创建属性访问缓存
  instance.accessCache = Object.create(null)

  // 3. 创建公开代理(this)
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)

  // 4. 执行 setup() 函数
  if (instance.type.setup) {
    const setupResult = instance.type.setup(
      instance.props,      // 第一个参数:响应式 props
      setupContext         // 第二个参数:{ emit, slots, expose }
    )
    handleSetupResult(instance, setupResult)
  }
}

关键优化accessCache 是一个性能优化。在模板中频繁访问 this.xxx 时,每次都需要判断属性来自 setupState、data、props 还是 ctx。accessCache 记录了每个属性的来源,避免重复的 hasOwn() 调用。


实例代理:this 的本质

属性访问的五层优先级

当你在模板中访问 this.msg 时,Vue 需要从多个位置查找这个属性。为了兼容 Composition API 和 Options API,Vue 设计了一个优先级链:

第一层:setupState(setup() 返回的状态)
   ↓ 如果找到,缓存并返回
第二层:data(data() 返回的状态)
   ↓ 如果找到,缓存并返回
第三层:props(父传递的属性)
   ↓ 如果找到,缓存并返回
第四层:ctx($attrs、$slots 等)
   ↓ 如果找到,缓存并返回
第五层:globalProperties(全局属性)

为什么 setupState 优先级最高?

这是 Vue 3 推荐 Composition API 的设计体现。如果 Options API 的 data 优先级更高,同名属性会导致 setup 返回的状态被覆盖,造成困惑。

代理的 set 拦截:保护机制

``ts // 伪代码 proxy.set = function(key, value) { // 规则 1:setupState 可修改(会触发响应式更新) if (hasOwn(setupState, key)) { setupState[key] = value return true }

// 规则 2:data 可修改(会触发响应式更新) if (hasOwn(data, key)) { data[key] = value return true }

// 规则 3:props 只读,禁止修改 if (hasOwn(props, key)) { warn(不能修改 props "${key}",props 是只读的) return false }

// 规则 4:$ 开头的内置属性只读 if (key[0] === '$') { warn(不能修改 "${key}",这是 Vue 的内置属性) return false }

// 规则 5:其他属性允许动态添加 ctx[key] = value return true }


**核心约束**:
- Props 是单向数据流,禁止直接修改
- setupState 和 data 修改会触发响应式系统
- $ 开头的内置属性受保护

---

## 渲染副作用:自动更新的引擎

### setupRenderEffect:连接响应式与渲染

这是 Vue "自动更新"的核心。渲染函数被包装成一个响应式副作用,当依赖的数据变化时,副作用会自动运行。

``ts
// 伪代码
function setupRenderEffect(instance, vnode, container) {
  // 第一步:定义渲染更新函数
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // ======= 首次挂载 =======
      // 执行 beforeMount 钩子
      invokeHooks(instance.bm)

      // 执行渲染函数,生成 VNode
      const subTree = renderComponentRoot(instance)
      instance.subTree = subTree

      // 递归 patch,转换为真实 DOM
      patch(null, subTree, container, ...)

      // 保存 DOM 引用
      vnode.el = subTree.el

      // 异步执行 mounted 钩子
      queuePostRenderEffect(() => invokeHooks(instance.m))

      instance.isMounted = true
    } else {
      // ======= 更新阶段 =======
      // 如果父组件传入了新 VNode,需要更新 props
      if (instance.next) {
        updateComponentPreRender(instance, instance.next)
      }

      // 执行 beforeUpdate 钩子
      invokeHooks(instance.bu)

      // 重新执行渲染函数,生成新 VNode
      const nextTree = renderComponentRoot(instance)
      const prevTree = instance.subTree
      instance.subTree = nextTree

      // Diff 并更新 DOM
      patch(prevTree, nextTree, container, ...)

      // 异步执行 updated 钩子
      queuePostRenderEffect(() => invokeHooks(instance.u))
    }
  }

  // 第二步:将渲染函数包装成响应式副作用
  const effect = new ReactiveEffect(componentUpdateFn)

  // 第三步:设置调度器
  // 关键:当依赖变化时,不直接运行 componentUpdateFn
  // 而是将 job 推入调度器队列,实现批量更新
  effect.scheduler = () => queueJob(instance.job)

  // 第四步:保存引用供后续调用
  instance.effect = effect
  instance.update = effect.run.bind(effect)
  instance.job = effect.runIfDirty.bind(effect)

  // 第五步:首次执行,完成初始挂载
  instance.update()
}

时序总结

mount 路径:
  beforeMount → render → patch → DOM 插入 → queuePostRenderEffect(mounted)

update 路径:
  beforeUpdate → render → patch → DOM 更新 → queuePostRenderEffect(updated)

生命周期钩子的执行时机

钩子          执行时机              同/异步   说明
──────────────────────────────────────────────────
beforeMount   render 前,首次       同步     在首次渲染前
mounted       patch 后,首次        异步     DOM 已插入,可访问 DOM
beforeUpdate  render 前,更新       同步     在重新渲染前
updated       patch 后,更新        异步     DOM 已更新,可访问新 DOM

为什么 mounted 和 updated 异步执行?

  1. 性能:同步执行会阻塞渲染流程
  2. DOM 保证:异步执行时,DOM 肯定已更新
  3. 批量处理:多个组件的钩子在同一 tick 统一执行

异步调度:批量更新的核心

问题:为什么多次修改数据只更新一次?

ts
// 用户代码
this.count++
this.count++
this.count++

// 如果没有调度器,会触发三次 render
// 但实际上只会 render 一次,原因就在调度器

queueJob:去重与排序

当响应式数据变化时,会触发 effect.scheduler,它将 job 加入队列:

ts
// 伪代码
function queueJob(job) {
  // 去重:同一个 job 不重复加入
  if (job 已在队列中) {
    return
  }

  // 排序:按 job.id(组件创建顺序)递增排列
  queue.push(job)
  queue.sort((a, b) => a.id - b.id)

  // 触发 flush
  queueFlush()
}

两个关键机制

  1. 去重:同一 tick 内,同一 job 只加入一次,多次状态变化合并为一次更新
  2. 排序:按 id 递增排列,保证父组件先于子组件更新

flushJobs:执行队列

微任务执行时,调用 flushJobs 来执行队列中的所有 job:

ts
// 伪代码
function flushJobs() {
  try {
    // 遍历并执行队列中的每个 job
    for (let i = 0; i < queue.length; i++) {
      const job = queue[i]
      callJob(job) // 执行组件更新函数
    }
  } finally {
    // 清理队列,防止内存泄漏
    queue.length = 0

    // 执行 post 队列(mounted、updated 钩子)
    flushPostFlushCbs()

    // 递归处理新入队的任务
    if (queue.length > 0) {
      flushJobs()
    }
  }
}

三个队列的执行顺序

Vue 的调度器维护三个队列,执行顺序严格:

【Pre 队列】 watch 的 flush: 'pre'

【Main 队列】组件更新(patch)

【Post 队列】mounted、updated 钩子

使用场景

ts
// pre 队列:在组件更新前执行
watch(() => this.count, () => { ... }, { flush: 'pre' })

// main 队列:组件更新本身
// effect.scheduler → queueJob(instance.job)

// post 队列:在 DOM 更新后执行
this.$nextTick(() => {
  // 访问最新的 DOM
})

nextTick:微任务的应用

``ts // 伪代码 const resolvedPromise = Promise.resolve() let currentFlushPromise = null

function nextTick(fn) { // 如果 fn 不提供,返回当前的 flush Promise // 用户可以通过 await nextTick() 等待 DOM 更新 const p = currentFlushPromise || resolvedPromise return fn ? p.then(fn) : p }

function queueFlush() { if (!currentFlushPromise) { // 将 flushJobs 推入微任务队列 currentFlushPromise = resolvedPromise.then(flushJobs) } }


**关键理解**:

用户代码执行: this.count++ this.$nextTick(() => console.log('DOM updated'))

JavaScript 事件循环: ┌─ Call Stack 执行(用户代码) │ ├─ this.count++ │ │ → trigger() → scheduler → queueJob() │ │ → queueFlush() → resolvedPromise.then(flushJobs) │ └─ nextTick 收集回调 │ └─ Microtask Queue 执行 ├─ flushJobs() 执行所有 job(patch、render) └─ nextTick 回调执行(此时 DOM 已更新)


**为什么用 Promise?**

Promise 的 `.then()` 回调进入**微任务队列**,执行时机是:
- 当前宏任务结束后
- 浏览器开始重绘前

这确保了 nextTick 的回调能在 DOM 更新后、浏览器重绘前执行。

---

## 完整流程演示

从数据变化到 DOM 更新的完整链路:

【步骤 1】用户代码修改响应式数据 this.count++

【步骤 2】响应式系统 trigger() 收集依赖的 effects 调用 effect.scheduler() → queueJob(instance.job)

【步骤 3】调度器去重并排序 if (job 不在队列中) queue.push(job) queue.sort() // 按 id 递增排列

【步骤 4】触发 flush 机制 queueFlush() → Promise.resolve().then(flushJobs) 此时返回控制权给事件循环

【步骤 5】事件循环进入微任务队列 flushJobs() 执行所有排队的 job

【步骤 6】执行 job = instance.job = effect.runIfDirty ├─ 调用 componentUpdateFn() │ ├─ 执行 beforeUpdate 钩子 │ ├─ 调用 renderComponentRoot(instance) │ ├─ 执行 patch(prevTree, nextTree) │ └─ 执行 updated 钩子(异步) └─ 返回

【步骤 7】执行 post 队列 flushPostFlushCbs() 执行所有 post 钩子

【步骤 8】用户 nextTick 回调执行 此时 DOM 已完全更新


---

## shouldUpdateComponent:更新守门员

并非每次父组件更新都需要触发子组件重渲染。`shouldUpdateComponent` 判断是否真的需要更新:

``ts
// 伪代码
function shouldUpdateComponent(oldVNode, newVNode) {
  // 规则 1:有指令或过渡效果,必须更新
  if (newVNode.dirs || newVNode.transition) {
    return true
  }

  // 规则 2:动态插槽必须更新
  if (newVNode.patchFlag & DYNAMIC_SLOTS) {
    return true
  }

  // 规则 3:只检查动态 props
  if (newVNode.patchFlag & PROPS) {
    for (const key of newVNode.dynamicProps) {
      if (newVNode.props[key] !== oldVNode.props[key]) {
        return true  // props 有变化,需要更新
      }
    }
    return false  // props 没变,跳过更新
  }

  // 规则 4:完全没有优化标记,全量比较
  return hasPropsChanged(oldVNode.props, newVNode.props)
}

优化策略

编译优化路径:只检查编译器标记为动态的 props,跳过静态 props 运行时路径:全量比较所有 props


总结:完整的驱动链路

graph TD
    A["响应式数据变化"] -->|trigger| B["ReactiveEffect.scheduler()"]
    B --> C["queueJob(instance.job)"]
    C --> D["queueFlush() → Promise.then()"]
    D --> E["微任务执行"]
    E --> F["flushJobs()"]
    F --> G["按 id 排序执行"]
    G --> H["componentUpdateFn()"]
    H --> I["beforeUpdate → render → patch → DOM"]
    I --> J["flushPostFlushCbs()"]
    J --> K["mounted/updated 钩子"]
    K --> L["nextTick 回调"]

核心机制

  1. 响应式系统:自动追踪依赖,数据变化时 trigger effect
  2. 调度器:去重、排序、批量执行,避免重复更新
  3. 副作用:包装 render 函数,自动响应数据变化
  4. 异步处理:用微任务实现 nextTick,保证 DOM 更新后执行

下一章预告:本章我们完成了渲染系统的完整解析。从编译、VNode、Patch、Diff 到响应式驱动,Vue 3 的核心链路已全面展开。下一章,我们将总结整个系统的架构设计和优化思想。


扫描关注微信 - 前端小卒,获取更多 Vue 3 源码解析内容