深入分析 Vue 3 源码中 `createComponentInstance` 和 `setupComponent` 的详细执行流程,它们如何构建组件实例和初始化 `setup` 上下文。

大家好,我是你们今天的 Vue 源码导游。今天,我们要扒一扒 Vue 3 源码中两个关键函数的底裤:createComponentInstancesetupComponent。它们俩就像是 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 对象,这个对象包含了组件的所有信息。

让我们逐行解读一下:

  1. const type = vnode.type as Component;: 从虚拟节点中获取组件的类型(也就是组件的定义)。

  2. 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 函数,用于触发事件。
  3. instance.root = parent ? parent.root : instance;: 设置根组件。如果存在父组件,则根组件为父组件的根组件,否则根组件为自身。

  4. instance.emit = emit.bind(null, instance) as any;: 初始化 emit 函数。 这里使用 bind 预先绑定了组件实例,这样在 emit 函数内部就可以访问到组件实例的信息了。 为什么要 bind 呢? 因为 emit 函数会被传递到子组件或者其他地方使用,如果不 bindthis 指向就会发生错误。

  5. return instance;: 返回组件实例。

用表格总结一下 createComponentInstance 的作用:

步骤 描述
1 从虚拟节点中获取组件的类型。
2 创建组件实例对象,并初始化各种属性,例如 uidvnodetypeparentappContextrootpropsemitsslots 等等。
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: 是否是服务器端渲染。

让我们逐行解读一下:

  1. const { propsOptions, type: Component } = instance;: 从组件实例中获取 propsOptions 和组件的定义。

  2. initProps(instance, instance.vnode.props, isSSR);: 初始化 props。 这个函数会将虚拟节点上的 props 传递给组件实例,并进行校验和转换。

  3. initSlots(instance, instance.vnode.children);: 初始化 slots。 这个函数会将虚拟节点上的 children 转换为 slots 对象,并存储到组件实例中。

  4. const { setup } = Component;: 从组件定义中获取 setup 函数。

  5. if (setup) { ... }: 如果存在 setup 函数,则执行以下步骤:

    • const setupContext = createSetupContext(instance);: 创建 setup 上下文。 setup 上下文是一个对象,包含了 attrsslotsemitexpose 等属性。 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 函数会根据返回值的类型来做不同的处理。

  6. 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 将状态对象转换为响应式对象。
  • 如果 setupResultundefined,或者其他类型,那么 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。

三、createComponentInstancesetupComponent 的关系

createComponentInstancesetupComponent 是 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);
  1. createComponentInstance 创建组件实例。
  2. setupComponent 初始化组件的 setup 函数和上下文。
  3. 如果组件有 setup 函数,则创建 setup 上下文,调用 setup 函数,并处理 setup 函数的返回值。
  4. 如果组件没有 setup 函数,或者 setup 函数执行完毕,则调用 finishComponentSetup 完成组件的 setup。

它们就像是“造人”流水线上的两个工位,createComponentInstance 负责创建“骨架”,setupComponent 负责填充“血肉和灵魂”。

四、总结

今天我们深入分析了 Vue 3 源码中 createComponentInstancesetupComponent 的详细执行流程。 它们是 Vue 组件初始化流程中两个非常重要的函数, 它们负责创建组件实例和初始化 setup 函数的上下文。 通过理解这两个函数的源码,我们可以更好地理解 Vue 组件的内部机制, 从而更好地使用 Vue 进行开发。

希望今天的讲座能帮助大家更好地理解 Vue 3 的源码。 源码的世界充满了惊喜, 让我们一起探索吧! 感谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注