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
相关推荐
剪刀石头布啊19 分钟前
设计一个多计时任务的任务管理功能
前端
ModyQyW1 小时前
vite-plugin-uni-manifest 更新了什么
前端·typescript·uni-app
Asize2 小时前
AI 协作开发新范式:我用 SDD 做了个排版 npm 包
前端·人工智能
kyriewen3 小时前
我把今年流传的前端 AI 面试题整理了一遍——4 类场景题+回答框架(附速查表)
前端·面试·程序员
计算机魔术师4 小时前
国产多模态模型正面硬刚Opus旗舰:差距从30%缩到3%
前端
风骏时光牛马4 小时前
程序员进阶:深度思考,解锁职场成长的底层逻辑
前端
IT_陈寒4 小时前
用了Proxy才发现以前的JavaScript白写了
前端·人工智能·后端
爱丶不疚5 小时前
Eval: Agent 说的 Eval 是什么?从单测、TDD 到 Sentry 聊起
前端·ai编程·vibecoding
求道於盲5 小时前
python中的类型标注
前端
计算机魔术师5 小时前
从硅谷测试到全球铺开,ChatGPT广告的10亿美元秘密
前端