大家好,我是你们今天的 Vue 源码导游。今天,我们要扒一扒 Vue 3 源码中两个关键函数的底裤:createComponentInstance
和 setupComponent
。它们俩就像是 Vue 组件的“造人”流水线,一个负责创建组件实例,另一个负责初始化 setup
函数的上下文。准备好了吗?让我们一起深入源码,看看它们是如何“造人”的!
一、createComponentInstance
:组件实例的诞生
createComponentInstance
的主要职责就是创建一个组件实例对象。这个实例对象将会贯穿组件的整个生命周期,存储组件的状态、props、emit 函数等等。
先来看看源码(简化版,省略了一些不常用的属性):
function createComponentInstance(vnode: VNode, parent: ComponentInternalInstance | null): ComponentInternalInstance {
const type = vnode.type as Component;
// 组件实例对象
const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
type,
parent,
appContext: parent ? parent.appContext : vnode.appContext || {}, // 如果存在父组件,则继承父组件的 appContext
root: null!, // 稍后设置
next: null, // 用于更新
subTree: null!, // 组件渲染的内容
effect: null!, // 用于响应式更新
update: null!, // 更新函数
render: null,
provides: parent ? parent.provides : Object.create(null), // 继承父组件的 provides
proxy: null, // 稍后设置
exposed: null, // 暴露给父组件的属性
exposeProxy: null, // 暴露给父组件的 proxy
isMounted: false,
isUnmounted: false,
isDeactivated: false,
emitted: null, // 存储 emit 事件
emitsOptions: normalizeEmitsOptions(type.emits), // 标准化 emits 选项
setupState: {}, // setup 函数返回的状态
slots: {}, // 插槽
attrs: {}, // 属性
props: {}, // props
propsOptions: normalizePropsOptions(type, appContext), // 标准化 props 选项
emit: null! // 稍后设置
};
instance.root = parent ? parent.root : instance; // 设置根组件
// 初始化 emit 函数 (需要 instance 提前创建)
instance.emit = emit.bind(null, instance) as any;
return instance;
}
这个函数接收两个参数:
vnode
: 组件的虚拟节点。parent
: 父组件的实例。
它返回一个 ComponentInternalInstance
对象,这个对象包含了组件的所有信息。
让我们逐行解读一下:
-
const type = vnode.type as Component;
: 从虚拟节点中获取组件的类型(也就是组件的定义)。 -
const instance: ComponentInternalInstance = { ... };
: 创建组件实例对象。这里面包含了大量的属性,我们挑选一些重要的来说:uid
: 组件的唯一 ID,用于调试和性能分析。vnode
: 组件的虚拟节点。type
: 组件的定义。parent
: 父组件的实例。appContext
: 应用上下文,包含了全局配置、插件等等。如果存在父组件,则继承父组件的appContext
,否则使用虚拟节点上的appContext
。root
: 根组件的实例。next
: 用于更新组件,指向新的虚拟节点。subTree
: 组件渲染的内容(虚拟 DOM 树)。effect
: 用于响应式更新的副作用函数。update
: 更新函数,负责触发组件的重新渲染。render
: 渲染函数,将组件的状态转换为虚拟 DOM。provides
: 用于依赖注入,提供给子组件使用的值。如果存在父组件,则继承父组件的provides
。proxy
: 代理对象,用于访问组件的状态、props、emit 函数等等。exposed
: 暴露给父组件的属性。exposeProxy
: 暴露给父组件的 proxy。isMounted
: 组件是否已经挂载。isUnmounted
: 组件是否已经卸载。isDeactivated
: 组件是否已经停用(用于<keep-alive>
)。emitted
: 存储 emit 事件。emitsOptions
: 标准化 emits 选项。setupState
:setup
函数返回的状态。slots
: 插槽。attrs
: 属性(没有在 props 中定义的属性)。props
: props。propsOptions
: 标准化 props 选项。emit
:emit
函数,用于触发事件。
-
instance.root = parent ? parent.root : instance;
: 设置根组件。如果存在父组件,则根组件为父组件的根组件,否则根组件为自身。 -
instance.emit = emit.bind(null, instance) as any;
: 初始化emit
函数。 这里使用bind
预先绑定了组件实例,这样在emit
函数内部就可以访问到组件实例的信息了。 为什么要bind
呢? 因为emit
函数会被传递到子组件或者其他地方使用,如果不bind
,this
指向就会发生错误。 -
return instance;
: 返回组件实例。
用表格总结一下 createComponentInstance
的作用:
步骤 | 描述 |
---|---|
1 | 从虚拟节点中获取组件的类型。 |
2 | 创建组件实例对象,并初始化各种属性,例如 uid 、vnode 、type 、parent 、appContext 、root 、props 、emits 、slots 等等。 |
3 | 设置根组件。 |
4 | 初始化 emit 函数,并使用 bind 预先绑定组件实例。 |
5 | 返回组件实例。 |
总的来说,createComponentInstance
就像是组件的“骨架”,它创建了一个空的组件实例,并初始化了组件的基本信息。接下来,setupComponent
就要给这个“骨架”填充血肉了。
二、setupComponent
:初始化 setup
上下文
setupComponent
的职责是初始化组件的 setup
函数,并处理 setup
函数的返回值。 setup
函数是 Vue 3 中非常重要的一个特性,它允许我们使用 Composition API 来组织组件的逻辑。
还是先看源码(同样是简化版):
function setupComponent(instance: ComponentInternalInstance, isSSR = false) {
const { propsOptions, type: Component } = instance;
// 初始化 props
initProps(instance, instance.vnode.props, isSSR);
// 初始化 slots
initSlots(instance, instance.vnode.children);
const { setup } = Component;
if (setup) {
// 创建 setup 上下文
const setupContext = createSetupContext(instance);
// 调用 setup 函数
const setupResult = callWithErrorHandling(
setup,
instance,
0 /* SETUP_FUNCTION */,
[shallowReadonly(instance.props), setupContext]
);
// 处理 setup 函数的返回值
handleSetupResult(instance, setupResult);
} else {
// 如果没有 setup 函数,则直接完成组件的 setup
finishComponentSetup(instance);
}
}
这个函数接收两个参数:
instance
: 组件实例。isSSR
: 是否是服务器端渲染。
让我们逐行解读一下:
-
const { propsOptions, type: Component } = instance;
: 从组件实例中获取propsOptions
和组件的定义。 -
initProps(instance, instance.vnode.props, isSSR);
: 初始化 props。 这个函数会将虚拟节点上的 props 传递给组件实例,并进行校验和转换。 -
initSlots(instance, instance.vnode.children);
: 初始化 slots。 这个函数会将虚拟节点上的 children 转换为 slots 对象,并存储到组件实例中。 -
const { setup } = Component;
: 从组件定义中获取setup
函数。 -
if (setup) { ... }
: 如果存在setup
函数,则执行以下步骤:-
const setupContext = createSetupContext(instance);
: 创建setup
上下文。setup
上下文是一个对象,包含了attrs
、slots
、emit
和expose
等属性。setup
函数可以通过setupContext
来访问这些属性。createSetupContext
源码大致如下:function createSetupContext(instance: ComponentInternalInstance): SetupContext { return { attrs: instance.attrs, slots: instance.slots, emit: instance.emit, expose: (exposed: Record<string, any> | null) => { instance.exposed = exposed || {}; } }; }
-
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [shallowReadonly(instance.props), setupContext]);
: 调用setup
函数。 这里使用callWithErrorHandling
来捕获setup
函数执行过程中发生的错误。setup
函数接收两个参数:shallowReadonly(instance.props)
: 只读的 props 对象。 为什么要只读呢? 因为 Vue 希望我们不要在setup
函数中直接修改 props,而是通过 emit 事件来通知父组件修改 props。setupContext
:setup
上下文。
-
handleSetupResult(instance, setupResult);
: 处理setup
函数的返回值。setup
函数可以返回一个对象,或者一个渲染函数。handleSetupResult
函数会根据返回值的类型来做不同的处理。
-
-
else { finishComponentSetup(instance); }
: 如果没有setup
函数,则直接调用finishComponentSetup
完成组件的 setup。
handleSetupResult
的源码分析:
function handleSetupResult(instance: ComponentInternalInstance, setupResult: unknown) {
if (isFunction(setupResult)) {
// setup 返回的是 render 函数
instance.render = setupResult;
} else if (isObject(setupResult)) {
// setup 返回的是 state 对象
instance.setupState = proxyRefs(setupResult);
} else if (__DEV__ && setupResult !== undefined) {
warn(
`setup() should return an object. Received: ${String(setupResult)}`
);
}
finishComponentSetup(instance);
}
- 如果
setupResult
是一个函数,那么 Vue 认为它是一个渲染函数,并将其赋值给instance.render
。 - 如果
setupResult
是一个对象,那么 Vue 认为它是一个状态对象,并将其赋值给instance.setupState
。 这里使用proxyRefs
将状态对象转换为响应式对象。 - 如果
setupResult
是undefined
,或者其他类型,那么 Vue 会发出警告。
finishComponentSetup
的源码分析:
function finishComponentSetup(instance: ComponentInternalInstance) {
const Component = instance.type as Component;
// 如果没有 render 函数,则尝试从 template 编译
if (!instance.render) {
if (!Component.template && Component.render) {
// 2.x compat 检查 legacy render option
Component.template = Component.render;
}
if (Component.template) {
// template 编译
instance.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement,
delimiters: Component.delimiters
});
}
}
// 应用 mixins
applyOptions(instance);
}
- 如果组件没有
render
函数,那么 Vue 会尝试从template
编译生成render
函数。 applyOptions
主要用于处理 Vue 2.x 风格的 options API (例如data
,computed
,watch
,methods
等)。 Vue 3 仍然支持 options API,applyOptions
会将其转换为 Composition API 风格的代码, 然后挂载到组件实例上。 这保证了 Vue 2.x 代码可以平滑迁移到 Vue 3。
用表格总结一下 setupComponent
的作用:
步骤 | 描述 |
---|---|
1 | 初始化 props。 |
2 | 初始化 slots。 |
3 | 如果存在 setup 函数,则创建 setup 上下文,并调用 setup 函数。 |
4 | 处理 setup 函数的返回值,如果返回值是一个函数,则将其作为渲染函数;如果返回值是一个对象,则将其作为状态对象。 |
5 | 如果没有 setup 函数,或者 setup 函数执行完毕,则调用 finishComponentSetup 完成组件的 setup。 |
6 | 在 finishComponentSetup 中,如果组件没有 render 函数,则尝试从 template 编译生成 render 函数. 处理 Vue 2.x 风格的 options API. |
总的来说,setupComponent
就像是组件的“灵魂”,它初始化了组件的 setup
函数,并处理了 setup
函数的返回值,最终完成了组件的 setup。
三、createComponentInstance
和 setupComponent
的关系
createComponentInstance
和 setupComponent
是 Vue 组件初始化流程中两个非常重要的函数,它们之间的关系可以用下图来表示:
graph LR
A[createComponentInstance] --> B(setupComponent);
B --> C{has setup?};
C -- Yes --> D[createSetupContext];
D --> E(call setup);
E --> F(handleSetupResult);
C -- No --> G(finishComponentSetup);
F --> G;
G --> H(applyOptions);
createComponentInstance
创建组件实例。setupComponent
初始化组件的setup
函数和上下文。- 如果组件有
setup
函数,则创建setup
上下文,调用setup
函数,并处理setup
函数的返回值。 - 如果组件没有
setup
函数,或者setup
函数执行完毕,则调用finishComponentSetup
完成组件的 setup。
它们就像是“造人”流水线上的两个工位,createComponentInstance
负责创建“骨架”,setupComponent
负责填充“血肉和灵魂”。
四、总结
今天我们深入分析了 Vue 3 源码中 createComponentInstance
和 setupComponent
的详细执行流程。 它们是 Vue 组件初始化流程中两个非常重要的函数, 它们负责创建组件实例和初始化 setup
函数的上下文。 通过理解这两个函数的源码,我们可以更好地理解 Vue 组件的内部机制, 从而更好地使用 Vue 进行开发。
希望今天的讲座能帮助大家更好地理解 Vue 3 的源码。 源码的世界充满了惊喜, 让我们一起探索吧! 感谢大家!