主题
实现组件的初始化流程
本节对标 Vue 3 源码
@vue/runtime-core中的component.ts、componentRenderUtils.ts、renderer.ts
组件系统概览
组件是 Vue 中最核心的抽象单位。一个组件从创建到渲染,经历以下流程:
VNode(type=组件) → createComponentInstance → setupComponent → 创建渲染 effect → render() → subTree → patch组件的本质:将一段模板(或 render 函数)+ 响应式状态封装为可复用的 VNode 生产者。
createComponentInstance —— 创建组件实例
对标
packages/runtime-core/src/component.ts——createComponentInstance
组件实例是整个组件系统的"数据中心",保存了组件运行所需的全部状态:
ts
// 组件内部实例的接口定义,描述了组件运行时需要的所有状态字段
export interface ComponentInternalInstance {
uid: number // 组件唯一标识符,每个实例递增
type: any // 组件的选项对象(即用户定义的组件配置)
vnode: VNode // 组件自身的 VNode(type 为组件选项对象)
next: VNode | null // 更新时新的组件 VNode,用于 updateComponent 流程
subTree: VNode // 组件 render() 返回的子树 VNode,即组件实际渲染的内容
parent: ComponentInternalInstance | null // 父组件实例,用于 provide/inject 链式查找
// 状态相关
props: Record<string, any> // 外部传入的属性,经 shallowReactive 包装后具有响应性
setupState: Record<string, any> // setup() 函数返回的状态对象
slots: Record<string, Function> // 插槽内容,key 为插槽名,value 为渲染函数
attrs: Record<string, any> // 未在 props 中声明的属性会归入 attrs
// 生命周期钩子数组(数组是因为同一个钩子可以注册多次)
isMounted: boolean // 是否已挂载,用于区分 mount 和 update 流程
bc: Function[] | null // beforeCreate
c: Function[] | null // created
bm: Function[] | null // beforeMount
m: Function[] | null // mounted
bu: Function[] | null // beforeUpdate
u: Function[] | null // updated
// 渲染相关
render: Function | null // 组件的渲染函数,来自 setup 返回或组件选项
proxy: any // 组件的代理对象,作为 render 函数中的 this
emit: Function // 触发自定义事件的函数,已绑定当前实例
// provide/inject 机制
provides: Record<string | symbol, any> // 向子组件提供的数据,继承自父组件
}
// 全局自增 id,用于给每个组件实例分配唯一标识
let uid = 0
// 创建组件实例:根据 VNode 和父组件实例生成一个全新的组件内部实例对象
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
): ComponentInternalInstance {
const instance: ComponentInternalInstance = {
uid: uid++, // 分配递增的唯一 id
type: vnode.type, // 保存组件选项对象,后续用于访问 setup、render 等
vnode, // 保存组件自身的 VNode 引用
next: null, // 初始无待更新的 VNode
subTree: null!, // 初始无子树,挂载时才会赋值(null! 表示稍后一定会赋值)
parent, // 记录父组件实例,用于 provide/inject 向上查找
props: {}, // 初始为空,由 initProps 填充
setupState: {}, // 初始为空,由 handleSetupResult 填充
slots: {}, // 初始为空,由 initSlots 填充
attrs: {}, // 初始为空,由 initProps 中分离出未声明的属性填充
isMounted: false, // 初始未挂载,首次渲染后置为 true
bc: null, // 各生命周期钩子初始为 null,注册时才创建数组
c: null,
bm: null,
m: null,
bu: null,
u: null,
render: null, // 初始无渲染函数,由 setup 或组件选项提供
proxy: null, // 初始无代理,在 setupStatefulComponent 中创建
emit: null!, // 初始占位,下方立即绑定
provides: parent ? parent.provides : {}, // 继承父组件的 provides,实现原型链式的 provide/inject
}
// 将 emit 函数绑定当前实例,这样用户调用 emit 时不需要手动传 instance
instance.emit = emit.bind(null, instance)
return instance
}字段解析
| 字段 | 用途 |
|---|---|
vnode | 组件自身的 VNode(type 为组件选项对象) |
subTree | 组件 render() 返回的 VNode 子树 |
next | 更新时,新的组件 VNode(用于 updateComponent) |
props | 外部传入的属性,shallowReactive 包装 |
setupState | setup() 返回的状态对象 |
slots | 插槽内容 |
proxy | 组件的代理对象,render 函数中的 this |
provides | provide/inject 数据,继承自 parent |
isMounted | 是否已挂载,区分 mount 和 update |
setupComponent —— 初始化流程
对标
packages/runtime-core/src/component.ts——setupComponent
ts
// 组件初始化的入口函数,依次完成 props、slots、setup 的初始化
export function setupComponent(instance: ComponentInternalInstance) {
// 从组件 VNode 中解构出 props(外部传入的属性)和 children(子节点/插槽内容)
const { props, children } = instance.vnode
// 1. 初始化 props:将 VNode 上的 props 分离为 instance.props 和 instance.attrs
initProps(instance, props)
// 2. 初始化 slots:将 children 规范化并挂载到 instance.slots 上
initSlots(instance, children)
// 3. 执行 setup 函数(针对有状态组件),创建 proxy 并处理 setup 返回值
setupStatefulComponent(instance)
}这三步是组件初始化的核心:
setupComponent
├── initProps → 处理 props 和 attrs
├── initSlots → 处理插槽
└── setupStatefulComponent → 执行 setup()setupStatefulComponent —— 执行 setup() 函数
ts
// 处理有状态组件的 setup 逻辑
function setupStatefulComponent(instance: ComponentInternalInstance) {
// 取出组件选项对象(即用户定义的 { setup, render, ... })
const Component = instance.type
// 创建代理对象,作为 render 函数中的 this
// PublicInstanceProxyHandlers 会将属性访问代理到 setupState、props 等位置
instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers)
// 从组件选项中取出 setup 函数
const { setup } = Component
if (setup) {
// 在执行 setup 之前,将 currentInstance 设置为当前实例
// 这样 setup 内部调用 onMounted、provide 等 API 时能找到当前组件
setCurrentInstance(instance)
// 调用 setup,传入只读的 props 和上下文对象
// shallowReadonly 确保用户不能在 setup 中直接修改 props
const setupResult = setup(
shallowReadonly(instance.props),
{
emit: instance.emit, // 事件触发函数
slots: instance.slots, // 插槽内容
attrs: instance.attrs, // 非 props 属性
expose: () => {}, // 暴露公共方法(简化实现)
},
)
// setup 执行完毕后清除 currentInstance,防止在 setup 外部误用
setCurrentInstance(null)
// 根据 setup 的返回值类型(函数或对象)进行不同处理
handleSetupResult(instance, setupResult)
} else {
// 如果组件没有定义 setup 函数,直接完成组件设置(使用选项中的 render)
finishComponentSetup(instance)
}
}PublicInstanceProxyHandlers
组件的 proxy 是一个 Proxy,统一代理对 setupState、props、$ 开头的公共属性的访问:
ts
// 组件公共实例的 Proxy 处理器,统一拦截对组件属性的读取和设置操作
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
// 拦截属性读取:按优先级从 setupState、props、公共属性中查找
get({ _: instance }, key: string) {
const { setupState, props } = instance
// 非 $ 开头的属性:先查 setupState,再查 props
if (key[0] !== '$') {
// setupState 优先级高于 props,这意味着如果两者都有同名属性,setupState 胜出
if (hasOwn(setupState, key)) {
return setupState[key]
} else if (hasOwn(props, key)) {
return props[key]
}
}
// $ 开头的公共属性(如 $el、$slots 等),从预定义的映射表中查找
const publicGetter = publicPropertiesMap[key]
if (publicGetter) {
return publicGetter(instance)
}
},
// 拦截属性设置:只允许设置 setupState 中的属性(props 是只读的)
set({ _: instance }, key: string, value: any) {
const { setupState } = instance
if (hasOwn(setupState, key)) {
setupState[key] = value // 修改 setupState 中的属性,会触发响应式更新
return true
}
return true // 即使属性不存在也返回 true,避免抛出错误
},
}
// $ 开头的公共属性映射表,将 $el、$slots 等映射到实例的对应字段
const publicPropertiesMap: Record<string, (i: ComponentInternalInstance) => any> = {
$el: (i) => i.vnode.el, // 组件根 DOM 元素
$slots: (i) => i.slots, // 插槽对象
$props: (i) => i.props, // props 对象
$emit: (i) => i.emit, // 事件触发函数
}这样在 render 函数或模板中:
this.count→ 先查setupState.count,再查props.countthis.$slots→ 返回instance.slotsthis.$emit→ 返回instance.emit
handleSetupResult —— 处理 setup 返回值
ts
// 处理 setup() 函数的返回值,支持两种返回类型
function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: any,
) {
if (typeof setupResult === 'function') {
// setup 返回函数 → 将其作为组件的 render 函数
instance.render = setupResult
} else if (typeof setupResult === 'object' && setupResult !== null) {
// setup 返回对象 → 存入 setupState,作为模板/render 中可访问的状态
// proxyRefs 包装后,模板中使用 ref 无需写 .value(自动解包)
instance.setupState = proxyRefs(setupResult)
}
// 最终确保 instance.render 有值(如果 setup 没返回 render 函数,则从组件选项中取)
finishComponentSetup(instance)
}两种返回值
ts
// 返回对象 —— 暴露状态给模板使用
setup() {
const count = ref(0)
return { count } // 模板中可以直接 {{ count }} 而非 {{ count.value }}
}
// 返回函数 —— 直接作为 render 函数
setup() {
const count = ref(0)
return () => h('div', count.value)
}proxyRefs 的作用:在模板中使用 ref 时不需要写 .value。它会自动解包 ref:
ts
const state = proxyRefs({ count: ref(0) })
state.count // 0(自动解包,不需要 .value)
state.count = 1 // 自动设置 ref.valuefinishComponentSetup —— 设置 render 函数
ts
// 完成组件设置:确保 instance.render 有值
function finishComponentSetup(instance: ComponentInternalInstance) {
const Component = instance.type // 取出组件选项对象
if (!instance.render) {
// 如果 setup 没有返回 render 函数,则使用组件选项中定义的 render 方法
instance.render = Component.render
}
}在完整的 Vue 3 中,这里还会处理 template 编译:
ts
// 完整版 Vue 3 中的 finishComponentSetup,额外处理 template 编译
function finishComponentSetup(instance) {
const Component = instance.type
if (!instance.render) {
// 如果组件定义了 template 但没有预编译的 render 函数
if (Component.template && !Component.render) {
// 在运行时调用编译器将 template 字符串编译为 render 函数
Component.render = compile(Component.template)
}
// 将编译后(或用户定义)的 render 赋值给实例
instance.render = Component.render
}
}currentInstance 机制
对标
packages/runtime-core/src/component.ts——currentInstance
ts
// 全局变量,记录当前正在执行 setup 的组件实例
// Composition API(如 onMounted、provide)通过它访问当前组件
export let currentInstance: ComponentInternalInstance | null = null
// 设置当前实例:在 setup 执行前调用传入 instance,执行后传入 null 清除
export function setCurrentInstance(instance: ComponentInternalInstance | null) {
currentInstance = instance
}
// 获取当前实例:供用户在 setup 中调用,获取当前组件的内部实例
export function getCurrentInstance(): ComponentInternalInstance | null {
return currentInstance
}为什么需要 currentInstance?
Composition API 中的 onMounted、provide、inject 等函数需要知道"当前正在初始化的是哪个组件实例"。通过全局变量 currentInstance,这些函数可以在 setup() 执行期间访问到当前组件实例:
ts
// 用户代码
setup() {
// 此时 currentInstance 指向当前组件实例
onMounted(() => {
console.log('mounted!')
})
// getCurrentInstance() 可以获取当前实例
const instance = getCurrentInstance()
}setCurrentInstance 的调用时机:
ts
setCurrentInstance(instance) // setup 执行前设置
const setupResult = setup(...) // setup 执行中可以访问
setCurrentInstance(null) // setup 执行后清除mountComponent —— 挂载组件
对标
packages/runtime-core/src/renderer.ts——mountComponent
ts
// 挂载组件:从创建实例到首次渲染的完整流程
function mountComponent(
initialVNode: VNode, // 组件的初始 VNode
container: any, // 挂载的目标容器 DOM 元素
anchor: any, // 插入锚点(用于精确定位 DOM 插入位置)
parentComponent: ComponentInternalInstance | null, // 父组件实例,用于建立组件树关系
) {
// 1. 创建组件实例,并将实例挂到 VNode 的 component 属性上
// 这样后续可以通过 VNode 访问到组件实例
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
))
// 2. 初始化组件:依次处理 props、slots、执行 setup
setupComponent(instance)
// 3. 创建渲染 effect:执行 render 函数并 patch 子树到 DOM
// 同时建立响应式依赖追踪,后续数据变化会自动触发更新
setupRenderEffect(instance, initialVNode, container, anchor)
}setupRenderEffect —— 创建渲染 effect
ts
// 创建渲染 effect:建立响应式依赖追踪,实现数据变化自动触发重新渲染
function setupRenderEffect(
instance: ComponentInternalInstance,
initialVNode: VNode,
container: any,
anchor: any,
) {
// 组件更新函数,根据 isMounted 状态区分首次挂载和后续更新
const componentUpdateFn = () => {
if (!instance.isMounted) {
// === MOUNT 首次挂载分支 ===
const { bm, m } = instance // 取出 beforeMount 和 mounted 钩子
// 执行 beforeMount 生命周期钩子(同步执行)
if (bm) {
invokeArrayFns(bm)
}
// 执行组件的 render 函数,生成子树 VNode
const subTree = (instance.subTree = renderComponentRoot(instance))
// 递归 patch 子树:将子树 VNode 渲染为真实 DOM 并挂载到容器中
// 第一个参数 null 表示这是首次挂载(无旧 VNode)
patch(null, subTree, container, anchor, instance)
// 将子树根节点的真实 DOM 元素赋值给组件 VNode 的 el
// 这样外部可以通过 vnode.el 访问组件的根 DOM 元素
initialVNode.el = subTree.el
// 执行 mounted 生命周期钩子
if (m) {
// 放到 post 队列中执行,确保此时 DOM 已经完成更新
queuePostFlushCb(m)
}
// 标记组件已挂载,后续更新将走 update 分支
instance.isMounted = true
} else {
// === UPDATE 更新分支 ===(第 16 节详细实现)
const { bu, u, next, vnode } = instance
// 如果有 next,说明是父组件触发的更新(props/slots 变化)
if (next) {
next.el = vnode.el // 复用旧的 DOM 元素引用
updateComponentPreRender(instance, next) // 更新 props、slots 等
}
// 执行 beforeUpdate 生命周期钩子
if (bu) {
invokeArrayFns(bu)
}
// 执行 render 生成新的子树 VNode
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree // 保存旧子树用于 diff
instance.subTree = nextTree // 更新为新子树
// 对比新旧子树,执行最小化 DOM 更新(diff + patch)
patch(prevTree, nextTree, container, anchor, instance)
// 执行 updated 生命周期钩子(异步,确保 DOM 已更新)
if (u) {
queuePostFlushCb(u)
}
}
}
// 创建 ReactiveEffect 实例
// 第一个参数是副作用函数,第二个参数是 scheduler(调度器)
// scheduler 的作用:响应式数据变化时不直接执行副作用,而是将更新任务推入异步队列
const effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update), // 调度器:将 update 推入微任务队列,实现异步批量更新
)
// 将 effect.run 封装为 update 函数,并挂到实例上供外部手动触发更新
const update = (instance.update = () => effect.run())
update() // 立即执行一次,触发首次挂载(走 mount 分支)
}renderComponentRoot
ts
// 执行组件的 render 函数,生成子树 VNode
function renderComponentRoot(
instance: ComponentInternalInstance,
): VNode {
const { render, proxy } = instance
// 调用 render 并将 this 绑定为 proxy,这样 render 中可以通过 this.xxx 访问状态和 props
// 同时将 proxy 作为第一个参数传入,支持 render(ctx) 风格的写法
const result = render!.call(proxy, proxy)
// 规范化返回值:确保返回的一定是 VNode 对象
return normalizeVNode(result)
}
// 规范化 VNode:将字符串/数字等简单类型转为 Text VNode
function normalizeVNode(child: any): VNode {
if (typeof child === 'string' || typeof child === 'number') {
// 字符串或数字会被包装为 Text 类型的 VNode
return createVNode(Text, null, String(child))
}
// 如果已经是 VNode 对象则直接返回
return child
}render.call(proxy, proxy) —— this 绑定为 proxy,这样 render 函数中可以通过 this.xxx 访问状态和 props。
effect + scheduler 的协作
ts
// 创建响应式副作用:将组件更新函数与调度器关联
const effect = new ReactiveEffect(
componentUpdateFn, // 副作用函数:执行 render + patch
() => queueJob(update), // scheduler 调度器:数据变化时不立即执行,而是将更新推入异步队列
)- 首次执行
update()→effect.run()→ 执行componentUpdateFn→ mount 分支 - 响应式数据变化 →
trigger→ 调用scheduler(不直接执行副作用) →queueJob(update)→ 微任务中批量执行更新 - 更新执行时 →
effect.run()→ 执行componentUpdateFn→ update 分支
这种设计实现了异步批量更新:多次数据变化只触发一次组件更新。
完整流程图
h(MyComponent, props)
│
▼
createVNode(MyComponent, props) → VNode { type: MyComponent, ... }
│
▼
patch(null, vnode, container)
│ shapeFlag & COMPONENT
▼
processComponent(null, vnode, ...)
│ n1 === null
▼
mountComponent(vnode, container, ...)
│
├── 1. createComponentInstance(vnode, parent)
│ → instance { type, vnode, props: {}, setupState: {}, ... }
│
├── 2. setupComponent(instance)
│ ├── initProps(instance, vnode.props)
│ ├── initSlots(instance, vnode.children)
│ └── setupStatefulComponent(instance)
│ ├── instance.proxy = new Proxy(...)
│ ├── setCurrentInstance(instance)
│ ├── setup(props, { emit, slots, attrs })
│ ├── setCurrentInstance(null)
│ ├── handleSetupResult → instance.setupState / instance.render
│ └── finishComponentSetup → instance.render
│
└── 3. setupRenderEffect(instance, vnode, container, ...)
│
├── componentUpdateFn (mount 分支)
│ ├── render.call(proxy) → subTree VNode
│ ├── patch(null, subTree, container)
│ └── instance.isMounted = true
│
└── new ReactiveEffect(fn, scheduler)
└── effect.run() → 首次渲染对比 React
| 维度 | Vue 3 | React |
|---|---|---|
| 组件实例 | ComponentInternalInstance 显式对象 | Fiber 节点 |
| 初始化入口 | setupComponent → setup() | renderWithHooks → 函数体 |
| 状态存储 | instance.setupState | Fiber 上的 hook 链表 |
| 当前实例 | currentInstance 全局变量 | currentlyRenderingFiber |
| 渲染触发 | ReactiveEffect + scheduler | scheduleUpdateOnFiber |
| 异步更新 | queueJob + 微任务 | MessageChannel / setTimeout |
| this 访问 | Proxy 代理 | 函数组件无 this |
Vue 3 的组件实例是一个显式的、可操控的对象,而 React Fiber 是一个内部的、不对外暴露的树节点。Vue 的 getCurrentInstance() 允许用户在 setup 中获取实例,React 则通过 hooks 隐式访问 Fiber 状态。
测试用例
ts
describe('component initialization', () => {
// 测试组件实例创建和 setup 执行:验证 setup 返回的状态能在 render 中通过 this 访问
it('should create component instance and run setup', () => {
const Comp = {
setup() {
const count = ref(0) // 创建响应式数据
return { count } // 返回对象,暴露给模板使用
},
render() {
return h('div', {}, this.count) // 通过 this.count 访问 setupState(proxyRefs 自动解包 ref)
},
}
const root = document.createElement('div')
render(h(Comp), root)
// 验证组件正确渲染:count 初始值为 0
expect(root.innerHTML).toBe('<div>0</div>')
})
// 测试 setup 返回 render 函数的场景:setup 可以直接返回一个函数作为 render
it('should support setup returning render function', () => {
const Comp = {
setup() {
const msg = ref('hello')
// 返回函数而非对象,该函数直接作为组件的 render 函数
return () => h('p', {}, msg.value)
},
}
const root = document.createElement('div')
render(h(Comp), root)
// 验证 setup 返回的 render 函数正确执行
expect(root.innerHTML).toBe('<p>hello</p>')
})
// 测试 props 通过 this 访问:render 中可以通过 this 访问外部传入的 props
it('should access props via this in render', () => {
const Comp = {
props: ['msg'], // 声明接收的 props
setup(props: any) {
return {} // 返回空对象,不提供额外状态
},
render() {
return h('span', {}, this.msg) // 通过 this.msg 访问 props(proxy 会自动查找 props)
},
}
const root = document.createElement('div')
render(h(Comp, { msg: 'world' }), root) // 传入 props
// 验证 props 能正确渲染到 DOM
expect(root.innerHTML).toBe('<span>world</span>')
})
// 测试响应式更新:数据变化后组件应自动重新渲染
it('should trigger update when reactive data changes', async () => {
const Comp = {
setup() {
const count = ref(0)
const increment = () => count.value++ // 修改响应式数据的方法
return { count, increment }
},
render() {
return h(
'button',
{ onClick: this.increment }, // 绑定点击事件
String(this.count), // 显示 count 值
)
},
}
const root = document.createElement('div')
render(h(Comp), root)
// 验证初始渲染
expect(root.innerHTML).toBe('<button>0</button>')
const btn = root.querySelector('button')!
btn.click() // 模拟点击,触发 count.value++
await nextTick() // 等待异步更新完成(微任务队列执行)
// 验证更新后的渲染结果
expect(root.innerHTML).toBe('<button>1</button>')
})
// 测试 getCurrentInstance:验证在 setup 中可以获取当前组件实例
it('should support getCurrentInstance in setup', () => {
let inst: any = null
const Comp = {
setup() {
inst = getCurrentInstance() // 在 setup 中获取当前实例
return () => h('div')
},
}
render(h(Comp), document.createElement('div'))
// 验证获取到的实例不为 null,且 type 指向组件选项对象
expect(inst).not.toBeNull()
expect(inst.type).toBe(Comp)
})
// 测试 getCurrentInstance 在 setup 外部应返回 null
it('getCurrentInstance should be null outside setup', () => {
// setup 执行完毕后 currentInstance 会被清除,所以在外部调用应返回 null
expect(getCurrentInstance()).toBeNull()
})
})本节小结
- createComponentInstance — 创建组件实例对象,包含 props/setupState/slots/emit/render 等全部字段
- setupComponent — 三步初始化:initProps → initSlots → setupStatefulComponent
- setupStatefulComponent — 创建 proxy、执行 setup()、处理返回值
- handleSetupResult — setup 返回对象则存入 setupState(proxyRefs 包装),返回函数则作为 render
- currentInstance — 全局变量机制,让 Composition API 在 setup 执行期间访问当前实例
- setupRenderEffect — 创建 ReactiveEffect,componentUpdateFn 区分 mount/update 两个分支
- 异步批量更新 — effect.scheduler + queueJob 实现多次数据变化只触发一次渲染
下一节实现 Props 与 Emit 机制。