createApp

当我们引入createApp的时候,它实际上是执行了下面这段代码,代码在packages/runtime-dom/src/index.ts

// args是你调用createApp里面传的参数
export const createApp = ((...args) => {
  // 渲染核心,重点
  const app = ensureRenderer().createApp(...args)

  if (__DEV__) {
    // 检查是否原生标签
    injectNativeTagCheck(app)
    // 检查是否自定义标签
    injectCompilerOptionsCheck(app)
  }

  // ...
  return app
}) as CreateAppFunction<Element>

我们可以看到该函数调用了ensureRenderer,该函数返回了一个createRender,该函数接受一个rendererOptions,这个我们暂时先不考虑,我们只用知道该函数调用了createRenderer这个函数,并返回了出去。

我们找到这个函数,这块代码在packages/runtime-core/src/renderer/ts,发现createRenderer内部又调用了一次baseCreateRenderer

function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
) {
  // 以上省略近2k行,该函数主要负责渲染部分相关,后续再说。
  return {
    render, // 渲染相关
    hydrate, // SSR
    createApp: createAppAPI(render, hydrate) // creteApp本体
  }
}

createAppAPI

这时候我们去看下createAppAPI,该代码位于packages/runtime-core/src/apiCreateApp.ts

// 这是createApp的类型定义
export interface App<HostElement = any> {
  version: string
  config: AppConfig
  use(plugin: Plugin, ...options: any[]): this
  mixin(mixin: ComponentOptions): this
  component(name: string): Component | undefined
  component(name: string, component: Component): this
  directive(name: string): Directive | undefined
  directive(name: string, directive: Directive): this
  mount(
    rootContainer: HostElement | string,
    isHydrate?: boolean,
    isSVG?: boolean
  ): ComponentPublicInstance
  unmount(): void
  provide<T>(key: InjectionKey<T> | string, value: T): this

  // internal, but we need to expose these for the server-renderer and devtools
  _uid: number
  _component: ConcreteComponent
  _props: Data | null
  _container: HostElement | null
  _context: AppContext

  /**
   * v2 compat only
   */
  filter?(name: string): Function | undefined
  filter?(name: string, filter: Function): this

  /**
   * @internal v3 compat only
   */
  _createRoot?(options: ComponentOptions): ComponentPublicInstance
}

// 具体实现
let uid = 0

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }
    // 创建AppContext
    const context = createAppContext()
    // 已安装的插件
    const installedPlugins = new Set()

    // 是否渲染
    let isMounted = false

    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,

      version,
      // ...
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        if (!isMounted) {
          // 创建Vnode
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          vnode.appContext = context

          // HMR root reload
          if (__DEV__) {
            context.reload = () => {
              render(cloneVNode(vnode), rootContainer, isSVG)
            }
          }

          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            render(vnode, rootContainer, isSVG)
          }
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            devtoolsInitApp(app, version)
          }

          return vnode.component!.proxy
        } else if (__DEV__) {
          warn(
            `App has already been mounted.\n` +
              `If you want to remount the same app, move your app creation logic ` +
              `into a factory function and create fresh app instances for each ` +
              `mount - e.g. \`const createMyApp = () => createApp(App)\``
          )
        }
      },
    })

    // ...
    return app
  }
}

normalizeContainer用于判断el是否存在

function normalizeContainer(
  container: Element | ShadowRoot | string
): Element | null {
  if (isString(container)) {
    const res = document.querySelector(container)

    if (__DEV__ && !res) {
      // 确保el元素存在
      warn(
        `Failed to mount app: mount target selector "${container}" returned null.`
      )
    }
    return res
  }

  if (
    __DEV__ &&
    container instanceof window.ShadowRoot &&
    container.mode === 'closed'
  ) {
    // 如果是ShadowRoot并且mode为mode可能会导致未知的bug
    warn(
      `mounting on a ShadowRoot with \`{mode: "closed"}\` may lead to unpredictable bugs`
    )
  }

  return container as any
}

mount 挂载

export const createApp = ((...args) => {
  // ...
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 获取mount所挂载的节点
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component

    if (!isFunction(component) && !component.render && !component.template) {
      // __UNSAFE__
      // Reason: potential execution of JS expressions in in-DOM template.
      // The user must make sure the in-DOM template is trusted. If it's
      // rendered by the server, the template should not contain any user data.
      component.template = container.innerHTML
      // 2.x compat check
      console.log('兼容', __COMPAT__)
      /**
       * __COMPAT__ 是启动的时候通过rollup去注入进去的
       * 用来判断是否向下兼容
       */
      if (__COMPAT__ && __DEV__) {
        for (let i = 0; i < container.attributes.length; i++) {
          const attr = container.attributes[i]
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null
            )
            break
          }
        }
      }
    }
    // 渲染前清空html
    container.innerHTML = ''
    // 挂载元素进行渲染
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      // vue2的话,会给#app设置一个v-cloak属性,在render的时候清空掉
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

这部分是vue的初次渲染逻辑,首先官方解构了mount方法, 然后又重写了app.mount,并调用normalizeContainer校验挂载元素,临时保存了需要渲染的内容。接下来并对vue2的写法做了兼容处理。

总结

至此,createApp的流程大概到此结束,下一部分来分析mount渲染部分,由于这部分比较复杂,可能会更新的比较慢。