Skip to content

Vue 3 组件实例与属性访问优化:从 VNode 到 this 的完整链路

本章深入解析 Vue 3 组件实例的创建过程与属性访问的优化机制,揭示 this 背后的代理魔法。

前言

在前面的章节中,我们学习了 VNode 结构、Patch 路由和 Diff 算法。但当 patch 函数遇到一个组件类型的 VNode 时,它是如何将其转换成一个可运行的组件实例的?当你在模板中访问 this.count 时,Vue 内部经历了怎样的查找过程?

本章将解答四个关键问题:

  1. 组件实例是如何诞生的? 它包含哪些核心属性?
  2. 三步走战略是什么? 每步的职责边界在哪里?
  3. this 的本质是什么? 为什么能同时访问 setup、data、props?
  4. 属性访问如何优化? accessCache 的性能意义何在?

组件挂载流程:从 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 不创建新实例,而是激活缓存的实例。这体现了 Vue 的标记驱动设计——通过 ShapeFlags 标记不同的处理路径。

mountComponent:三步走的挂载策略

组件挂载分解为三个清晰的步骤,每步职责明确:

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

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

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

职责分离

Step 1 → 分配内存,建立父子关系
Step 2 → 初始化状态,执行用户代码
Step 3 → 建立驱动机制,启动自动更新

createComponentInstance:实例的数据结构

一个组件实例本质上是一个包含所有必需信息的对象。让我们按功能分类理解这些属性:

【核心身份】
  uid: 唯一 ID(用于调度排序)
  vnode: 组件的 VNode(父中的表示)
  type: 组件定义对象
  parent: 父组件实例引用

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

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

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

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

vnode vs subTree:关键区分

这是一个容易混淆的概念:

vnode = 包装盒子(组件在父中的表示)
  例:<MyComponent :msg="hello" />

subTree = 盒子里的内容(组件 render 的结果)
  例:<div>{{ msg }}</div>

时机不同:
  vnode:创建组件实例时就有
  subTree:挂载时才由 render 生成

为什么要分离?

  1. 组件有完整的生命周期(创建 → 挂载 → 更新 → 卸载)
  2. 父组件更新时,先比较 vnode,再决定是否生成新 subTree
  3. 便于管理元数据与渲染产物

setupComponent:状态初始化的四大操作

初始化的两个主要任务

ts
// 伪代码
function setupComponent(instance) {
  // 任务 1:标准化 props 和 slots
  initProps(instance, instance.vnode.props)
  initSlots(instance, instance.vnode.children)

  // 任务 2:执行 setup() 函数
  if (isStatefulComponent(instance)) {
    setupStatefulComponent(instance)
  }
}

initProps 的作用

  • 验证父传入的属性是否符合组件声明
  • 设置默认值
  • 转换为响应式对象

initSlots 的作用

  • 将 VNode 中的插槽函数提取出来
  • 便于模板中 <slot> 标签使用

setupStatefulComponent:四个关键操作

ts
// 伪代码
function setupStatefulComponent(instance) {
  // 操作 1:创建属性查找缓存
  instance.accessCache = Object.create(null)

  // 操作 2:创建公开代理对象(this)
  instance.proxy = new Proxy(instance.ctx, ProxyHandlers)

  // 操作 3:执行 setup() 函数
  const setupResult = instance.type.setup(
    instance.props,      // 第一参数:响应式 props
    { emit, slots, ... }  // 第二参数:上下文
  )

  // 操作 4:处理 setup() 的返回值
  handleSetupResult(instance, setupResult)
}

实例代理:this 的本质

属性访问的五层优先级

当你访问 this.count 时,Vue 通过代理拦截器执行一个优先级链:

优先级 1️⃣  setupState
    ├─ 来源:setup() 返回值
    └─ 用途:Composition API 的状态

优先级 2️⃣  data
    ├─ 来源:data() 返回值
    └─ 用途:Options API 的状态

优先级 3️⃣  props
    ├─ 来源:父组件传入
    └─ 用途:单向数据流

优先级 4️⃣  ctx
    ├─ 来源:内部上下文
    └─ 用途:$attrs、$slots 等

优先级 5️⃣  globalProperties
    ├─ 来源:全局注册
    └─ 用途:全局属性、插件注入

为什么 setupState 优先级最高?

原因 1:推荐 Composition API
  Vue 3 官方推荐使用 setup,应该优先满足

原因 2:避免意外覆盖
  setup 是新代码,data 是旧代码
  新代码不应被旧代码覆盖

原因 3:执行顺序一致
  setup 先执行,优先级也应先处理

代理 get 拦截的完整流程

ts
// 伪代码
proxy.get = function (key) {
  // 第一步:检查缓存
  const cached = accessCache[key]
  if (cached !== undefined) {
    return getValue(cached) // O(1) 直接返回
  }

  // 第二步:按优先级查找
  if (hasOwn(setupState, key)) {
    accessCache[key] = 'SETUP' // 记录来源
    return setupState[key]
  }

  if (hasOwn(data, key)) {
    accessCache[key] = 'DATA'
    return data[key]
  }

  if (hasOwn(props, key)) {
    accessCache[key] = 'PROPS'
    return props[key]
  }

  if (hasOwn(ctx, key)) {
    accessCache[key] = 'CONTEXT'
    return ctx[key]
  }

  // 第三步:特殊属性处理
  if (key === '$el') return vnode.el
  if (key === '$root') return rootInstance
  // ... 其他 $ 开头的公共属性
}

代理 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 是只读的,不能直接修改')
    return false
  }

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

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

三大保护原则

【单向数据流】
  ❌ 禁止修改 props
  因为 props 由父组件控制,直接修改会破坏数据流向追踪

【保护内置属性】
  ❌ 禁止修改 $ 开头的属性
  因为这些是 Vue 内部使用,修改可能导致系统崩溃

【修改触发更新】
  ✅ 修改 setupState/data 会触发响应式更新
  因为它们是 reactive() 或 ref() 对象

accessCache:性能优化的妙手

为什么需要 accessCache?

在模板中频繁访问 this.xxx。如果每次都进行优先级查找:

访问 this.count(无缓存):
  1. hasOwn(setupState, 'count')?
  2. hasOwn(data, 'count')?
  3. hasOwn(props, 'count')?
  4. ... 检查其他来源

1000 次访问 this.count:
  无缓存 = 1000 × 4 次判断 = 4000 次 hasOwn 调用
  有缓存 = 4 次判断 + 996 × O(1) 查找 ≈ 只需 1% 的判断

性能提升:约 100 倍

accessCache 的工作流程

【首次访问】
  this.count → 检查 setupState → 找到!
  记录:accessCache['count'] = SETUP_SOURCE
  返回值

【第 2-1000 次访问】
  this.count → 检查 accessCache['count']
  命中 SETUP_SOURCE → 直接返回 setupState['count']

【性能差异】
  首次:5 次 hasOwn() 调用,~0.5ms
  后续:1 次 Map 查找,~0.01ms(50 倍快)

实现细节

ts
// accessCache 是一个纯对象 Object.create(null)
// 结构示例:
{
  'count': SETUP_SOURCE,      // 记录属性来源
  'message': DATA_SOURCE,
  'title': PROPS_SOURCE,
  '$el': PUBLIC_PROPERTY,
}

// 当代理拦截访问时:
const source = accessCache[key]
if (source === SETUP_SOURCE) return setupState[key]
if (source === DATA_SOURCE) return data[key]
// ... 极速路径

完整流程演示

从创建组件到第一次访问 this.count

【第 1 步】patch 遇到组件 VNode
  n1 = null(首次挂载)
  调用 mountComponent()

【第 2 步】createComponentInstance
  创建实例对象
  分配 uid = 1
  instance = {
    uid: 1,
    vnode,
    type: MyComponent,
    ...
  }

【第 3 步】setupComponent
  initProps() → 解析父传入的属性
  initSlots() → 提取插槽函数
  setupStatefulComponent():
    ├─ accessCache = {}(空)
    ├─ proxy = new Proxy(ctx, handlers)
    ├─ 执行 setup() 函数
    │   const count = ref(0)
    │   return { count }
    └─ instance.setupState = { count }

【第 4 步】模板首次访问 {{ count }}
  代理拦截 get('count')

  检查 accessCache['count'] → 不存在

  按优先级查找:
    1. hasOwn(setupState, 'count')?✅ YES

  记录缓存:
    accessCache['count'] = SETUP_SOURCE

  返回 setupState['count'] = 0

【第 5 步】后续访问 {{ count }}
  代理拦截 get('count')

  检查 accessCache['count'] → SETUP_SOURCE

  直接返回 setupState['count'](O(1))

Composition + Options 混用场景

ts
// 组件实例中同时有两种状态源
{
  setupState: {
    count: Ref(0),      // setup() 返回值
  },
  data: {
    message: 'hello',   // data() 返回值
  },
  props: {
    title: 'App',       // 父传入
  }
}

// 访问优先级演示:
this.count       → setupState(优先级 1
this.message     → data(优先级 2
this.title       → props(优先级 3

// 同名时的覆盖规则:
{
  setupState: { value: 1 },
  data: { value: 2 },
  props: { value: 3 }
}

this.value       → 1(setupState 优先级最高)

架构流程图

属性访问的决策树


思考

accessCache 真的必要吗?

问题:现代 JavaScript 引擎有 inline cache,为什么还需要 accessCache?

分析

  • Inline cache 优化属性访问,但 Vue 的问题是属性来源判断
  • Vue 需要在 hasOwn() 调用中判断属性属于哪个来源
  • Inline cache 无法优化这个判断过程
  • accessCache 是主动的、可控的、可预测的优化

结论:accessCache 解决了 inline cache 无法解决的问题,是必要的。

setupState 优先级为什么最高?

问题:这是为了推荐 Composition API,还是有技术原因?

分析

  1. API 推荐:Vue 3 明确推荐 Composition API,优先级应该体现这一点
  2. 执行顺序:setup() 在 data() 之前执行,优先级应该保持一致
  3. 避免困惑:后执行的代码不应被先执行的代码覆盖

结论:优先级设计既符合执行顺序,也符合框架的发展方向。

Props 为什么强制只读?

问题:为什么不让开发者自己决定是否修改 props?

分析

  • 数据流追踪:单向数据流使代码易于理解和调试
  • 框架共识:React 也禁止直接修改 props
  • 绕过机制:v-model 提供了双向绑定的语法糖

结论:单向数据流是正确的约束,增强了代码的可维护性。


总结:从 VNode 到 this 的完整链路

VNode(虚拟描述)

processComponent(路由分发)

createComponentInstance(内存分配)

setupComponent(初始化)
    ├─ initProps
    ├─ initSlots
    └─ setupStatefulComponent
        ├─ accessCache = {}(空缓存)
        ├─ proxy = new Proxy(ctx, handlers)
        └─ 执行 setup()

instance.proxy(代理对象 = this)

访问 this.count
    ├─ 代理 get 拦截
    ├─ 检查 accessCache(命中则 O(1))
    ├─ 未命中则按优先级查找(setupState → data → props → ctx)
    └─ 记录查找结果到 accessCache

三个核心优化

  1. accessCache:属性查找缓存,性能提升 100 倍
  2. 优先级机制:清晰的访问顺序,避免混淆
  3. 代理保护:enforcing 单向数据流,增强可维护性

下一章预告:实例创建完成后,如何驱动重新渲染?我们将深入 setupRenderEffect,探索响应式系统与渲染的连接,以及异步调度系统的奥秘。


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