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
相关推荐
Old Uncle Tom17 分钟前
银行用户画像 -- 金融目标与需求意图
前端·人工智能·金融
IT_陈寒22 分钟前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端
东方小月23 分钟前
从0开发一个 Coding Agent(一):前言
前端·人工智能·typescript
恋猫de小郭23 分钟前
Flutter 全新真 3D 实现,用 flutter_scene 能开发一个「我的世界」
android·前端·flutter
ji_shuke1 小时前
远程排查 Web 系统问题:如何导出 HAR 文件协助定位
前端·问题排查
程序员爱钓鱼1 小时前
Rust String 与 &str 详解:字符串所有权、借用与转换
前端·后端·rust
码农学院1 小时前
GEO与SEO协同:从传统搜索到生成式搜索的平滑迁移路径
服务器·前端·python
咩咩啃树皮10 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
阳光是sunny10 小时前
LangGraph中的Reducer是什么
前端·人工智能·后端