Vue3源码解读-createApp流程

💡 [本系列Vue3源码解读文章基于3.3.4版本](https://github.com/vuejs/core/tree/v3.3.4)

1、前言

在Vue3中,我们挂载实例采用:createApp(App).mount("#app");请问这个过程发生了什么呢?

javacript 复制代码
import { createApp } from "vue";//引入的runtime-dom
import App from "./App.vue";
// 今天主要分析createApp这个函数
createApp(App).mount("#app");

2、createApp函数

createApp函数源码在core/packages/runtime-dom/src/index.ts目录下:****

createApp函数源码

核心代码如下:

javacript 复制代码
export const createApp = ((...args) => {
  // 创建app对象
  const app = ensureRenderer().createApp(...args)

  const { mount } = app
  // 重写mount方法
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 获取<div id="root"></div>的DOM对象(这个函数处理了字符串和真实DOM两种情况)
    const container = normalizeContainer(containerOrSelector)

    const component = app._component
    // 如果组件对象没有定义 render 函数和 template 模板(首次初始化的时候),则取容器的 innerHTML 作为组件模板内容
    if (!isFunction(component) && !component.render && !component.template) {
      // __UNSAFE__
      // Reason: potential execution of JS expressions in in-DOM template.
      // The user must make sure the in-DOM template is trusted. If it's
      // rendered by the server, the template should not contain any user data.
      // 将根节点下的HTML内容添加到组件的template上
      component.template = container.innerHTML
    }
    // clear content before mounting
    container.innerHTML = ''
    // 真正的挂载(挂载容器,是否是ssr,是否是svg元素)
    const proxy = mount(container, false, container instanceof SVGElement)
    // 返回渲染器的所有方法的集合
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

核心代码做了两件事情:

  1. 调用ensureRenderer函数获取渲染器,然后执行渲染器的createApp方法常见了app实例;
  2. 获取app实例上的mount方法,并对重写了mount方法;

接着,我们看看深入ensureRenderer,看它做了什么事情。

2.1 ensureRender函数

ensureRender函数源码

我们进入ensureRender函数后,它的实现如下:

jsx 复制代码
// https://github.com/vuejs/core/blob/v3.3.4/packages/runtime-dom/src/index.ts#L41-L46
function ensureRenderer() {
  // 最终返回了一个对象 {render,createApp}
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

这个函数的返回值,实际上是返回了一个render渲染器,返回之前,首先判断了渲染器是否存在,如果不存在,则调用createRenderer函数创建了一个渲染器。我们接着深入createRenderer函数看它的实现。

2.2 createRenderer函数

createRenderer函数源码

我们进入createRenderer函数后,它的实现如下:

javacript 复制代码
// https://github.com/vuejs/core/blob/v3.3.4/packages/runtime-core/src/renderer.ts#L296-L301
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  // 创建基础的渲染器
  return baseCreateRenderer<HostNode, HostElement>(options)
}

createRenderer根据传入的options,调用baseCreateRenderer创建了一个基础的渲染器。

2.3 baseCreateRenderer函数

baseCreateRenderer函数源码

baseCreateRenderer函数实现很长,我们先只关注它的返回值:

jsx 复制代码
// https://github.com/vuejs/core/blob/v3.3.4/packages/runtime-core/src/renderer.ts#L2354-L2359
return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
}

从上面的代码中,我们发现它返回了一个对象,里面有render、hydrate、createApp属性,而createApp是根据createAppAPI函数实现的,我们继续进入createAppAPI函数里面去看它的实现。

2.4 createAppAPI函数

createAppAPI函数源码

baseCreateRenderer函数实现很长,我们先只关注它做了哪些事情:

javacript 复制代码
// https://github.com/vuejs/core/blob/v3.3.4/packages/runtime-core/src/apiCreateApp.ts#L199-L416
render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  // 返回开发者使用的app工厂函数
  return function createApp(rootComponent, rootProps = null) {
    if (!isFunction(rootComponent)) {
      rootComponent = extend({}, rootComponent)
    }

    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }
    // 1.创建上下文
    const context = createAppContext()

    // 2.声明一个不可重复的插件容器
    const installedPlugins = new Set()
    // 3.初始化isMounted的状态是false
    let isMounted = false
    // 4.应用程序实例:创建app,并将其添加到context对象的app属性上
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,

      version,

      get config() {
        // ...
      },

      set config(v) {
        // ....
      },

      use(plugin: Plugin, ...options: any[]) {
        // ...
      },

      mixin(mixin: ComponentOptions) {
        // ...
      },

      component(name: string, component?: Component): any {
         // ...
      },

      directive(name: string, directive?: Directive) {
       // ...
      },
      // eg. app.mount('#app'),核心渲染逻辑,将vnode转换成真实的DOM
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
         // ...
      },

      unmount() {
        // ...
      },

      provide(key, value) {
        // ...
      },

      runWithContext(fn) {
       // ...
      }
    })
    // 5.考虑兼容的属性
    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }

    return app
  }
}

baseCreateRenderer实际上返回了createApp工厂函数,该函数其实就是开发者在页面上调用的createApp方法。在该工厂函数中中创建了app实例,并将添加到context对象的app属性上,最后将app实例返回。

2.5 createApp函数调用流程

从createApp函数一路分析,最终的调用链为:createApp() -> ensureRenderer() -> createRenderer() => renderer -> renderer.createApp()。

3、挂载流程

3.1 mount函数

页面上执行完createApp函数后,就会调用mount方法去挂载实例。我们首先来看mount重写的实现

mount重写源码

javacript 复制代码
const { mount } = app
  // 重写mount方法
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 获取<div id="root"></div>的DOM对象(这个函数处理了字符串和真实DOM两种情况)
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    // app是一个包含component\config\directive\mixin\mount\provide等属性的实例对象,该实例对象提供了一个应用上下文,实例对象上的方法可以链式调用
    const component = app._component
    // 如果组件对象没有定义 render 函数和 template 模板(首次初始化的时候),则取容器的 innerHTML 作为组件模板内容
    if (!isFunction(component) && !component.render && !component.template) {
      // 将根节点下的HTML内容添加到组件的template上
      component.template = container.innerHTML
    }
    // clear content before mounting
    container.innerHTML = ''
    // 真正的挂载(挂载容器,是否是ssr,是否是svg元素)
    const proxy = mount(container, false, container instanceof SVGElement)
   
    // 返回渲染器的所有方法的集合
    return proxy
  }

请问为什么这里要重新mount方法呢?这是因为不同平台的mount方法所做的工作不一样,底层mount是通用实现,这里重写是为了在web环境进行dom节点的挂载。我们继续看底层mount方法的实现。

底层mount方法源码实现

javacript 复制代码
// https://github.com/vuejs/core/blob/v3.3.4/packages/runtime-core/src/apiCreateApp.ts#L319-372
// eg. app.mount('#app'),核心渲染逻辑,将vnode转换成真实的DOM
mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  // 判断当前返回的app是否已经调用过mount方法
  if (!isMounted) {
    // #5571
    if (__DEV__ && (rootContainer as any).__vue_app__) {
      warn(
        `There is already an app instance mounted on the host container.\n` +
          ` If you want to mount another app on the same host container,` +
          ` you need to unmount the previous app by calling \`app.unmount()\` first.`
      )
    }
    // 创建根组件对应的vnode,即虚拟DOM(根据编译后的.vue文件生成对应的虚拟节点)
    const vnode = createVNode(rootComponent, rootProps)
    // store app context on the root VNode.
    // this will be set on the root instance on initial mount.
    // 将上下文信息挂载到根组件节点的 appContext 属性上
    vnode.appContext = context
    // HMR root reload
    if (__DEV__) {
      context.reload = () => {
        render(cloneVNode(vnode), rootContainer, isSVG)
      }
    }
    // 转换VNode的处理逻辑
    if (isHydrate && hydrate) {
      // ssr服务端渲染的VNode的逻辑
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      // 利用外部传入的渲染器渲染vnode(将vnode挂载到用户传入的container中),即将VNode转换为真实的DOM
      render(vnode, rootContainer, isSVG)
    }
    isMounted = true
    // 建立app与DOM的关联
    app._container = rootContainer
    // 根容器上设置一个特殊标记 __vue_app__,用于判断一个 DOM 元素上是否已经挂载了 Vue 应用实例
    // for devtools and telemetry
    ;(rootContainer as any).__vue_app__ = app
    // 如果是开发环境或者开启了生产环境下的开发工具支持,还会将应用实例的 _instance 属性设置为根 VNode 对应的组件实例,然后调用 devtoolsInitApp 函数,将应用实例注册到开发工具中。这样,开发者就可以在开发工具中查看应用的状态和行为,方便调试和排查问题
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      app._instance = vnode.component
      devtoolsInitApp(app, version)
    }

    return getExposeProxy(vnode.component!) || vnode.component!.proxy
  } else if (__DEV__) {
    warn(
      `App has already been mounted.\n` +
        `If you want to remount the same app, move your app creation logic ` +
        `into a factory function and create fresh app instances for each ` +
        `mount - e.g. \`const createMyApp = () => createApp(App)\``
    )
  }
},

该函数首先createVNode函数创建根组件对应的虚拟VNode,然后判断是否是服务端渲染:

  • 服务端渲染:调用hydrate方法将VNode转换成真实的DOM;
  • 非服务端渲染:调用外部传入的render方法,将VNode转换成真实的DOM,然后渲染出真正的html;

3.1 render函数

render函数源码实现

javacript 复制代码
// 如果第一个参数是null,则执行销毁组件的逻辑,否则执行patch函数来创建或者更新组件的逻辑
  const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        // 卸载组件
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 执行patch过程,即diff过程
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPreFlushCbs()
    flushPostFlushCbs()
    container._vnode = vnode
  }

该函数实现比较简单,首先判断vnode节点是否存在,如果为null,则执行卸载组件逻辑,否则执行patch更新(即Diff过程)。

4、createApp整体流程图

结合createApp创建实例和mount挂载流程后,Vue3 实例整个挂载流程如下:

5、参考资料

[1] Vue官网

[2] Vuejs设计与实现

相关推荐
秦jh_1 小时前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑2131 小时前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy1 小时前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
涔溪2 小时前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞2 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-2 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与2 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun2 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇2 小时前
ES6进阶知识一
前端·ecmascript·es6
渗透测试老鸟-九青3 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss