Vue3之setup初始化工作流程

2,114 阅读3分钟

一个组件选项,在创建组件之前执行,一旦props被解析,并作为组合式API的入口点

参数

使用setup函数时候,接受两个参数:

  • propssetup函数中的第一个参数,响应式的,当传入新的props时,它将被更新
  • content:即上下文,setup函数中的第二个参数,是一个普通的JavaScript对象,它暴露三个组件的property:attrsslotsemit

WARNINGS

  • 因为props是响应式的,不能使用ES6结构,因为他会消除props的响应性,如果需要结构props,可以通过使用setup函数中的toRefs来安全地完成此操作
  • content是一个普通的JavaScript对象,不是响应式的,意味着可以安全的对content使用ES6结构 attrsslots是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行结构,并始终以attrs.xslots.x的方式应用property。请注意,与props不同,attrsslots是非相应式的。如果你打算根据attrsslots更改应用副作用,那么应该在onUpdated生命周期钩子中执行此操作。

从一个栗子开始

const { createApp, setup } = Vue;

createApp({
    setup() {
        return {
            msg: 'hello, vue3~'
        }
    }
}).mount('#demo');

CAPTURE_202175_194337.jpg

可以看到触发setupComponent方法的流程是:createApp -> createApp.mount -> mount -> render -> patch -> processComponent -> mountComponent -> setupComponent

Vue3初始化过程中有提到过一个非常庞大的方法baseCreateRenderer,中间具体实现的一些方法给跳过了,现在我们详细了解下部分流程和方法:

// packages/runtime-core/src/renderer.ts
// overload 1: no hydration
function baseCreateRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>): Renderer<HostElement>

// overload 2: with hydration
function baseCreateRenderer(
  options: RendererOptions<Node, Element>,
  createHydrationFns: typeof createHydrationFunctions
): HydrationRenderer

// implementation
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
    // ...

    // Note: functions inside this closure should use `const xxx = () => {}`
    // style in order to prevent being inlined by minifiers.
    const patch: PatchFn = (
        n1,
        n2,
        container,
        anchor = null,
        parentComponent = null,
        parentSuspense = null,
        isSVG = false,
        slotScopeIds = null,
        optimized = false
    ) => {
        // patching & not same type, unmount old tree
        if (n1 && !isSameVNodeType(n1, n2)) {
            anchor = getNextHostNode(n1)
            unmount(n1, parentComponent, parentSuspense, true)
            n1 = null
        }

        if (n2.patchFlag === PatchFlags.BAIL) {
            optimized = false
            n2.dynamicChildren = null
        }
        
        const { type, ref, shapeFlag } = n2
        switch (type) {
            case Text:
                processText(n1, n2, container, anchor)
                break
            case Comment:
                processCommentNode(n1, n2, container, anchor)
                break
            case Static:
                if (n1 == null) {
                    mountStaticNode(n2, container, anchor, isSVG)
                } else if (__DEV__) {
                    patchStaticNode(n1, n2, container, isSVG)
                }
                break
        case Fragment:
            processFragment(
                n1,
                n2,
                container,
                anchor,
                parentComponent,
                parentSuspense,
                isSVG,
                slotScopeIds,
                optimized
            )
            break
        default:
            if (shapeFlag & ShapeFlags.ELEMENT) {
                processElement(
                    n1,
                    n2,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    slotScopeIds,
                    optimized
                )
            } else if (shapeFlag & ShapeFlags.COMPONENT) {
                // processComponent
                processComponent(
                    n1,
                    n2,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    slotScopeIds,
                    optimized
                )
            } else if (shapeFlag & ShapeFlags.TELEPORT) {
                ;(type as typeof TeleportImpl).process(
                    n1 as TeleportVNode,
                    n2 as TeleportVNode,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    slotScopeIds,
                    optimized,
                    internals
                )
            } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
                ;(type as typeof SuspenseImpl).process(
                    n1,
                    n2,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    slotScopeIds,
                    optimized,
                    internals
                )
            } else if (__DEV__) {
                warn('Invalid VNode type:', type, `(${typeof type})`)
            }
        }

        // set ref
        if (ref != null && parentComponent) {
            setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
        }
    }
    
    // ...

    const processComponent = (
        n1: VNode | null,
        n2: VNode,
        container: RendererElement,
        anchor: RendererNode | null,
        parentComponent: ComponentInternalInstance | null,
        parentSuspense: SuspenseBoundary | null,
        isSVG: boolean,
        slotScopeIds: string[] | null,
        optimized: boolean
    ) => {
        n2.slotScopeIds = slotScopeIds
        if (n1 == null) {
        if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
            ;(parentComponent!.ctx as KeepAliveContext).activate(
            n2,
            container,
            anchor,
            isSVG,
            optimized
            )
        } else {
            mountComponent(
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
            )
        }
        } else {
        updateComponent(n1, n2, optimized)
        }
    }
    
    const mountComponent: MountComponentFn = (
        initialVNode,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
    ) => {
        // 2.x compat may pre-creaate the component instance before actually
        // mounting
        const compatMountInstance = __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
        const instance: ComponentInternalInstance =
            compatMountInstance ||
            (initialVNode.component = createComponentInstance(
                initialVNode,
                parentComponent,
                parentSuspense
            ))

        if (__DEV__ && instance.type.__hmrId) {
            registerHMR(instance)
        }

        if (__DEV__) {
            pushWarningContext(initialVNode)
            startMeasure(instance, `mount`)
        }

        // inject renderer internals for keepAlive
        if (isKeepAlive(initialVNode)) {
            ;(instance.ctx as KeepAliveContext).renderer = internals
        }

        // resolve props and slots for setup context
        if (!(__COMPAT__ && compatMountInstance)) {
            if (__DEV__) {
                startMeasure(instance, `init`)
            }

            // setup
            setupComponent(instance)
            if (__DEV__) {
                endMeasure(instance, `init`)
            }
        }

        // setup() is async. This component relies on async logic to be resolved
        // before proceeding
        if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
            parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)

            // Give it a placeholder if this is not hydration
            // TODO handle self-defined fallback
            if (!initialVNode.el) {
                const placeholder = (instance.subTree = createVNode(Comment))
                processCommentNode(null, placeholder, container!, anchor)
            }
            return
        }

        setupRenderEffect(
        instance,
        initialVNode,
        container,
        anchor,
        parentSuspense,
        isSVG,
        optimized
        )

        if (__DEV__) {
            popWarningContext()
            endMeasure(instance, `mount`)
        }
    }

    //...  
    
    return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate)
    }
}

mountComponent方法中的setupComponent方法,即开始setup一系列功能,由于此时还没有真正创建Vue实例对象,所以setup中的this并不会指向Vue

// packages/runtime-core/src/component.ts
export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children } = instance.vnode
  // 判断组件状态
  const isStateful = isStatefulComponent(instance)
  // 初始化props
  initProps(instance, props, isStateful, isSSR)
  // 初始化slots
  initSlots(instance, children)

  // 根据 isStateful 状态创建状态组件 或 undefined
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}
// packages/runtime-core/src/component.ts
function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions

  if (__DEV__) {
    if (Component.name) {
      validateComponentName(Component.name, instance.appContext.config)
    }
    if (Component.components) {
      const names = Object.keys(Component.components)
      for (let i = 0; i < names.length; i++) {
        validateComponentName(names[i], instance.appContext.config)
      }
    }
    if (Component.directives) {
      const names = Object.keys(Component.directives)
      for (let i = 0; i < names.length; i++) {
        validateDirectiveName(names[i])
      }
    }
    if (Component.compilerOptions && isRuntimeOnly()) {
      warn(
        `"compilerOptions" is only supported when using a build of Vue that ` +
          `includes the runtime compiler. Since you are using a runtime-only ` +
          `build, the options should be passed via your build tool config instead.`
      )
    }
  }
  // 0. create render proxy property access cache
  instance.accessCache = Object.create(null)
  // 1. create public instance / render proxy
  // also mark it raw so it's never observed
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
  if (__DEV__) {
    exposePropsOnRenderContext(instance)
  }
  // 2. call setup()
  const { setup } = Component
  if (setup) {
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    currentInstance = instance
    pauseTracking()
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
    resetTracking()
    currentInstance = null

    if (isPromise(setupResult)) {
      if (isSSR) {
        // return the promise so server-renderer can wait on it
        return setupResult
          .then((resolvedResult: unknown) => {
            handleSetupResult(instance, resolvedResult, isSSR)
          })
          .catch(e => {
            handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
          })
      } else if (__FEATURE_SUSPENSE__) {
        // async setup returned Promise.
        // bail here and wait for re-entry.
        instance.asyncDep = setupResult
      } else if (__DEV__) {
        warn(
          `setup() returned a Promise, but the version of Vue you are using ` +
            `does not support it yet.`
        )
      }
    } else {
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    finishComponentSetup(instance, isSSR)
  }
}

setupStatefulComponent方法中创建实例和setupResult,初始化时并调用handleSetupResult

// packages/runtime-core/src/component.ts
export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  if (isFunction(setupResult)) {
    // setup returned an inline render function
    if (__NODE_JS__ && (instance.type as ComponentOptions).__ssrInlineRender) {
      // when the function's name is `ssrRender` (compiled by SFC inline mode),
      // set it as ssrRender instead.
      instance.ssrRender = setupResult
    } else {
      instance.render = setupResult as InternalRenderFunction
    }
  } else if (isObject(setupResult)) {
    if (__DEV__ && isVNode(setupResult)) {
      warn(
        `setup() should not return VNodes directly - ` +
          `return a render function instead.`
      )
    }
    // setup returned bindings.
    // assuming a render function compiled from template is present.
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      instance.devtoolsRawSetupState = setupResult
    }
    instance.setupState = proxyRefs(setupResult)
    if (__DEV__) {
      exposeSetupStateOnRenderContext(instance)
    }
  } else if (__DEV__ && setupResult !== undefined) {
    warn(
      `setup() should return an object. Received: ${
        setupResult === null ? 'null' : typeof setupResult
      }`
    )
  }
  finishComponentSetup(instance, isSSR)
}

handleSetupResult中调用finishConponentSetup

export function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean,
  skipOptions?: boolean
) {
  const Component = instance.type as ComponentOptions

  if (__COMPAT__) {
    convertLegacyRenderFn(instance)

    if (__DEV__ && Component.compatConfig) {
      validateCompatConfig(Component.compatConfig)
    }
  }

  // template / render function normalization
  if (__NODE_JS__ && isSSR) {
    // 1. the render function may already exist, returned by `setup`
    // 2. otherwise try to use the `Component.render`
    // 3. if the component doesn't have a render function,
    //    set `instance.render` to NOOP so that it can inherit the render
    //    function from mixins/extend
    instance.render = (instance.render ||
      Component.render ||
      NOOP) as InternalRenderFunction
  } else if (!instance.render) {
    // could be set from setup()
    if (compile && !Component.render) {
      const template =
        (__COMPAT__ &&
          instance.vnode.props &&
          instance.vnode.props['inline-template']) ||
        Component.template
      if (template) {
        if (__DEV__) {
          startMeasure(instance, `compile`)
        }
        const { isCustomElement, compilerOptions } = instance.appContext.config
        const {
          delimiters,
          compilerOptions: componentCompilerOptions
        } = Component
        const finalCompilerOptions: CompilerOptions = extend(
          extend(
            {
              isCustomElement,
              delimiters
            },
            compilerOptions
          ),
          componentCompilerOptions
        )
        if (__COMPAT__) {
          // pass runtime compat config into the compiler
          finalCompilerOptions.compatConfig = Object.create(globalCompatConfig)
          if (Component.compatConfig) {
            extend(finalCompilerOptions.compatConfig, Component.compatConfig)
          }
        }
        // 对template模板进行编译
        Component.render = compile(template, finalCompilerOptions)
        if (__DEV__) {
          endMeasure(instance, `compile`)
        }
      }
    }

    instance.render = (Component.render || NOOP) as InternalRenderFunction

    // for runtime-compiled render functions using `with` blocks, the render
    // proxy used needs a different `has` handler which is more performant and
    // also only allows a whitelist of globals to fallthrough.
    if (instance.render._rc) {
      instance.withProxy = new Proxy(
        instance.ctx,
        RuntimeCompiledPublicInstanceProxyHandlers
      )
    }
  }

  // support for 2.x options
  if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
    currentInstance = instance
    pauseTracking()
    applyOptions(instance)
    resetTracking()
    currentInstance = null
  }

  // warn missing template/render
  // the runtime compilation of template in SSR is done by server-render
  if (__DEV__ && !Component.render && instance.render === NOOP && !isSSR) {
    /* istanbul ignore if */
    if (!compile && Component.template) {
      warn(
        `Component provided template option but ` +
          `runtime compilation is not supported in this build of Vue.` +
          (__ESM_BUNDLER__
            ? ` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
            : __ESM_BROWSER__
              ? ` Use "vue.esm-browser.js" instead.`
              : __GLOBAL__
                ? ` Use "vue.global.js" instead.`
                : ``) /* should not happen */
      )
    } else {
      warn(`Component is missing template or render function.`)
    }
  }
}

finishComponentSetup方法中,待处理完模板字符串和编译相关配置后即开始compiler编译,从源码中可看到在是有对vue2的写法对兼容的,所以在setup中支持vue2的写法,待finishComponentSetup方法结束即整个setup的创建过程完成。

总结:

  • setup在所有组件创建之前被执行,并且被当做是所有API的总入口
  • setup之所以在其他组件之前创建,而此时的vue实例对象并没有成功被创建,所以同vue2中的this是有区别的
  • setup作为一个语法糖包含可以传递其他钩子函数