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
相关推荐
小白小白从不日白8 分钟前
react 组件通讯
前端·react.js
罗_三金19 分钟前
前端框架对比和选择?
javascript·前端框架·vue·react·angular
Redstone Monstrosity25 分钟前
字节二面
前端·面试
东方翱翔33 分钟前
CSS的三种基本选择器
前端·css
Fan_web1 小时前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
yanglamei19621 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
千穹凌帝1 小时前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
冯宝宝^1 小时前
基于mongodb+flask(Python)+vue的实验室器材管理系统
vue.js·python·flask
dot.Net安全矩阵1 小时前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
程序员小羊!1 小时前
前端框架对比和选择
前端框架