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
相关推荐
灿灿121389 分钟前
CSS 文字浮雕效果:巧用 text-shadow 实现 3D 立体文字
前端·css
烛阴27 分钟前
Babel 完全上手指南:从零开始解锁现代 JavaScript 开发的超能力!
前端·javascript
AntBlack1 小时前
拖了五个月 ,不当韭菜体验版算是正式发布了
前端·后端·python
31535669131 小时前
一个简单的脚本,让pdf开启夜间模式
前端·后端
尘心cx1 小时前
前端-CSS-day1
前端·css
知否技术1 小时前
前端常说的 SCSS是个啥玩意?一篇文章给你讲的明明白白!
前端·scss
盛夏绽放1 小时前
Vue3 中 Excel 导出的性能优化与实战指南
vue.js·excel
幼儿园技术家1 小时前
Uniapp简易使用canvas绘制分享海报
前端
开开心心就好2 小时前
免费PDF处理软件,支持多种操作
运维·服务器·前端·spring boot·智能手机·pdf·电脑
全宝2 小时前
🎨前端实现文字渐变的三种方式
前端·javascript·css