Vue3源码解析之 createApp

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue3 源码解析系列第 22 篇,关注专栏

前言

前面我们分别对 Vue响应式render编译器 等内容的分析,至此我们还剩下最后一块内容,即 createAPP 函数的讲解。本篇也是 Vue3 源码解析系列 的最后一篇,下面我们依旧通过案例的形式来一探究竟。

案例一

首先引入 createApph 两个函数,声明一个包含 render 方法的 APP 对象,通过 createApp 创建 app 对象,之后调用 mount 方法来挂载到对应的节点上。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../dist/vue.global.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { createApp, h } = Vue

      const APP = {
        render() {
          return h('div', 'hello world')
        }
      }

      const app = createApp(APP)

      app.mount('#app')
    </script>
  </body>
</html>

CreateApp 函数

根据案例一我们得知,APP 对象类似于之前的 component,而 createAPP 类似返回了一个 vnode 节点,并通过 mount 方法来进行 render 挂载。

理解完上述分析,我们再来看下 createApp 函数,它被定义在 packages/runtime-core/src/renderer.ts 文件中:

ts 复制代码
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
    // 省略
    
    return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate)
      }
}

可见 createAPP 是由 createAppAPI 函数所返回的,我们再看下 createAppAPI 方法,它被定义在 packages/runtime-core/apiCreateApp.ts 文件中:

ts 复制代码
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // 省略

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

      // 省略

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        if (!isMounted) {
          // 省略
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          // 省略

          if (isHydrate && hydrate) {
            // 省略
          } else {
            render(vnode, rootContainer, isSVG)
          }
          
         // 省略

          return getExposeProxy(vnode.component!) || vnode.component!.proxy
        } else if (__DEV__) {
          // 省略
        }
      },

      // 省略
    })

    // 省略

    return app
  }
}

该方法会返回一个 createApp 函数,接收 组件 作为参数,然后创建一个 app 对象且返回,该对象包含了 mount 方法。这也就是为什么我们执行完 createApp,可以直接调用 app.mount

我们再看下 mount 方法:

ts 复制代码
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
    if (!isMounted) {
      // 省略
      const vnode = createVNode(
        rootComponent as ConcreteComponent,
        rootProps
      )
      // 省略

      if (isHydrate && hydrate) {
        // 省略
      } else {
        render(vnode, rootContainer, isSVG)
      }

     // 省略

      return getExposeProxy(vnode.component!) || vnode.component!.proxy
    } else if (__DEV__) {
      // 省略
    }
},

该方法将 组件 通过 createVNode 生成 vnode 节点,然后执行 render 函数,将节点挂载到指定的容器上。但是我们发现我们传入的容器是一个字符串 '#app',可是 render 函数接收的 container 是一个元素,那 Vue 又是如何处理的呢?

我们知道 render 函数是通过 ensureRenderer().render(...args) 来执行的,那么 createAPP 也是一样,它被定义在 packages/runtime-dom/src/index.ts 文件中:

ts 复制代码
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }

  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component
    // 省略

    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

可以看到 container 是通过 normalizeContainer 来获取的:

ts 复制代码
function normalizeContainer(
  container: Element | ShadowRoot | string
): Element | null {
  if (isString(container)) {
    const res = document.querySelector(container)
    if (__DEV__ && !res) {
      warn(
        `Failed to mount app: mount target selector "${container}" returned null.`
      )
    }
    return res
  }
  // 省略
  return container as any
}

该方法根据传入的 container 如果是字符串 (例如:'#app') 就重新获取元素且返回,这也就是为什么我们执行 app.mount('#app') 时可以直接渲染了。

另外还有种模板场景,Vue 是如何进行挂载的呢?我们再来看个例子。

案例二

首先引入 createApph 两个函数,声明一个包含 template 模板的 APP 对象,通过 createApp 创建 app 对象,之后调用 mount 方法来挂载到对应的节点上。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../dist/vue.global.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { createApp, h } = Vue

      const APP = {
        template: '<div>hello world</div>'
      }

      const app = createApp(APP)

      app.mount('#app')
    </script>
  </body>
</html>

模板渲染

要将模板进行渲染,首先要将 template 转换成 render 函数,这里就要调用 compiler 编译器。

根据案例执行 mount 方法触发 render 函数,接着执行 patch 方法触发 finishComponentSetup 方法:

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

  // 省略

  // template / render function normalization
  // could be already set when returned from setup()
  if (!instance.render) {
    // only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
    // is done by server-renderer
    if (!isSSR && compile && !Component.render) {
      const template =
        (__COMPAT__ &&
          instance.vnode.props &&
          instance.vnode.props['inline-template']) ||
        Component.template
      if (template) {
        // 省略
        Component.render = compile(template, finalCompilerOptions)
        // 省略
      }
    }

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

    // 省略
  }

  // 省略
}

当前 vnode 节点的 type 类型为:

根据判断,当前组件不存在 render 函数,则执行 Component.render = compile(template, finalCompilerOptions) ,调用 compile 函数对模板进行编译返回 render 函数。此时 vnode 节点就具备了 render 函数,之后再将节点插入到指定容器中,执行完页面呈现:

至此,createApp 两种挂载场景都分析完毕。

总结

  1. createApp 函数实际返回的是一个 app 对象,里面包含了 mount 方法。
  2. mount 方法接收的参数会通过 normalizeContainer 函数进行处理,如果是字符串类型则会获取对应的元素。
  3. 如果传入的是 template 模板类型,则会调用 compile 进行编译生成 render 函数。

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列

  1. Vue3源码解析之 源码调试
  2. Vue3源码解析之 reactive
  3. Vue3源码解析之 ref
  4. Vue3源码解析之 computed
  5. Vue3源码解析之 watch
  6. Vue3源码解析之 runtime
  7. Vue3源码解析之 h
  8. Vue3源码解析之 render(一)
  9. Vue3源码解析之 render(二)
  10. Vue3源码解析之 render(三)
  11. Vue3源码解析之 render(四)
  12. Vue3源码解析之 render component(一)
  13. Vue3源码解析之 render component(二)
  14. Vue3源码解析之 render component(三)
  15. Vue3源码解析之 render component(四)
  16. Vue3源码解析之 render component(五)
  17. Vue3源码解析之 diff(一)
  18. Vue3源码解析之 diff(二)
  19. Vue3源码解析之 compiler(一)
  20. Vue3源码解析之 compiler(二)
  21. Vue3源码解析之 compiler(三)
  22. Vue3源码解析之 createApp
相关推荐
计算机程序设计小李同学10 小时前
幼儿园信息管理系统的设计与实现
前端·bootstrap·html·毕业设计
雨季66610 小时前
Flutter 三端应用实战:OpenHarmony “专注时光盒”——在碎片洪流中守护心流的数字容器
开发语言·前端·安全·flutter·交互
tao35566711 小时前
【用AI学前端】HTML-02-HTML 常用标签(基础)
前端·html
2601_9495328411 小时前
Psello HTML Template: A Developer‘s Deep-Dive Review and Guide - Download Free
前端·windows·html·seo·wordpress·gpl
CappuccinoRose11 小时前
CSS前端布局总指南
前端·css·学习·布局·flex布局·grid布局·float布局
穿过锁扣的风11 小时前
如何操作HTML网页
前端·javascript·html
San30.11 小时前
从零构建坚固的前端堡垒:TypeScript 与 React 实战深度指南
前端·react.js·typescript
yunhuibin11 小时前
VideoPipe环境搭建及编译ubuntu240403
前端·人工智能
CHANG_THE_WORLD12 小时前
PDF文档结构分析 一
前端·pdf
晚霞的不甘12 小时前
Flutter for OpenHarmony从基础到专业:深度解析新版番茄钟的倒计时优化
android·flutter·ui·正则表达式·前端框架·鸿蒙