分析 Vue 3 源码中 `createApp` 方法如何创建应用实例,并初始化渲染过程。

各位靓仔靓女们,晚上好!欢迎来到 Vue 3 源码剖析小课堂。今天咱们的主题是:createApp 凭啥能创建应用实例,又是怎么开始渲染的?别慌,我会用最接地气的方式,带你们抽丝剥茧,扒光它的底裤(误)。

一、开场白:createApp 是个啥?

在 Vue 3 中,createApp 就像一个造物主,它负责创建一个 Vue 应用实例,这个实例就是咱们整个应用的核心。有了它,才能挂载组件、注册全局组件/指令/混入等等。

简单来说,没了 createApp,Vue 应用就是一堆散装零件,根本跑不起来。

二、源码初探:createApp 的真面目

咱们先看看 createApp 的源码,别怕,我会把关键部分拎出来:

// packages/vue/src/apiCreateApp.ts

import {
  createAppAPI,
  CreateAppFunction,
} from './apiCreateAppInner'

export const createApp = ((...args) => {
  const app = createAppAPI(...args)

  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }

  return app
}) as CreateAppFunction

别被 TypeScript 类型定义吓到,实际上 createApp 只是一个包装函数,它调用了 createAppAPI 函数,并返回一个应用实例。__DEV__ 是一个编译时常量,表示是否是开发环境,如果是开发环境,会做一些额外的检查。

所以,真正的核心逻辑都在 createAppAPI 函数里。

三、createAppAPI:应用实例的制造工厂

// packages/vue/src/apiCreateAppInner.ts

import {
  Component,
  ComponentPublicInstance,
  createVNode,
  VNode,
} from './vnode'
import {
  render,
  RootRenderFunction,
  createRenderer,
  RendererOptions,
} from './renderer'
import {
  isString,
  isFunction,
  extend,
  isObject,
} from '@vue/shared'
import {
  ComponentOptions,
  ConcreteComponent,
  validateComponentName,
} from './component'
import {
  AppContext,
  createAppContext,
} from './apiCreateAppContext'
import {
  warn,
  DeprecationTypes,
  deprecationContext,
} from './warning'
import {
  Directive,
  validateDirectiveName,
} from './directives'
import {
  isRuntimeOnly,
  isCompatEnabled,
  compatUtils,
} from './compat/compatConfig'

export interface CreateAppFunction<HostElement> {
  <T>(rootComponent: Component, rootProps?: Record<string, any>): App<T>
}

export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootRenderFunction<HostElement>
): CreateAppFunction<HostElement> {
  return function createApp(...args) {
    if (__DEV__ && !__TEST__) {
      deprecationContext.set(DeprecationTypes.GLOBAL_MOUNT)
    }

    const [rootComponent, rootProps] = args
    // 省略类型检查部分

    const context = createAppContext() // 创建应用上下文
    const installedPlugins = new Set()

    let isMounted = false

    const app: App = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps || null,
      _container: null,
      _context: context,
      _instance: null,

      version,

      get config() {
        return context.config
      },

      set config(v) {
        if (__DEV__) {
          warn(
            `app.config cannot be replaced. Modify individual options instead.`
          )
        }
      },

      use(plugin: Plugin, ...options: any[]) {
        if (installedPlugins.has(plugin)) {
          __DEV__ && warn(`Plugin has already been applied to target app.`)
        } else if (plugin && isFunction(plugin.install)) {
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          installedPlugins.add(plugin)
          plugin(app, ...options)
        } else if (__DEV__) {
          warn(
            `A plugin must either be a function ` +
              `or an object with an "install" function.`
          )
        }
        return app
      },

      mixin(mixin: ComponentOptions) {
        if (__DEV__ && !isObject(mixin)) {
          warn(
            `Mixin argument must be an object. Received ${String(mixin)}`
          )
        }
        context.mixins.push(mixin)
        return app
      },

      component(name: string, component?: Component): any {
        if (!component) {
          return context.components[name]
        }
        if (__DEV__ && !isString(name)) {
          warn(`Component name must be a string. Received ${String(name)}`)
        }
        if (__DEV__ && !validateComponentName(name, context.components)) {
          return app
        }
        context.components[name] = component
        return app
      },

      directive(name: string, directive?: Directive): any {
        if (!directive) {
          return context.directives[name] as any
        }
        if (__DEV__ && !isString(name)) {
          warn(`Directive name must be a string. Received ${String(name)}.`)
        }
        if (__DEV__ && !validateDirectiveName(name)) {
          return app
        }
        context.directives[name] = directive
        return app
      },

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        if (!isMounted) {
          // 创建 VNode
          const vnode = createVNode(rootComponent as ConcreteComponent, rootProps)

          // 将应用实例注入到 VNode 的 appContext 中,供子组件使用
          vnode.appContext = context

          // 如果是服务端渲染,则进行激活
          if (isHydrate && hydrate) {
            hydrate(vnode as any, rootContainer)
          } else {
            // 调用 renderer.render 方法进行渲染
            render(vnode as any, rootContainer, isSVG)
          }
          isMounted = true
          app._container = rootContainer
          // for devtools and vue-router
          ;(rootContainer as any).__vue_app__ = app

          return getExposeProxy(vnode.component!) || vnode.component!.proxy
        } else if (__DEV__) {
          warn(
            `App has already been mounted. Create a new app instance instead.`
          )
        }
      },

      unmount() {
        if (isMounted) {
          render(null, app._container)
          if (__DEV__) {
            ;(app._container as any).__vue_app__ = null
          }
          app._component = null
          app._container = null
          app._instance = null
          isMounted = false
        } else if (__DEV__) {
          warn(`Cannot unmount an app that is not mounted.`)
        }
      },

      provide(key: any, value: any) {
        if (__DEV__ && key in context.provides) {
          warn(
            `App already provides property with key "${String(key)}". ` +
              `It will be overwritten with the new value.`
          )
        }

        context.provides[key] = value

        return app
      }
    }

    if (__DEV__) {
      injectRegisterHook(app)
    }

    return app
  }
}

这段代码略长,但别担心,我来给大家划重点:

  1. createAppContext() 创建一个应用上下文对象 context,这个对象存储了应用级别的配置、组件、指令、混入等信息。相当于应用的全局状态仓库。

  2. app 对象: createAppAPI 返回一个 app 对象,这个对象就是咱们的应用实例。它包含了以下关键属性和方法:

    • _component:根组件。
    • _props:传递给根组件的 props。
    • _container:挂载的 DOM 元素。
    • _context:应用上下文。
    • use():注册插件。
    • mixin():注册全局混入。
    • component():注册全局组件。
    • directive():注册全局指令。
    • mount() 将应用挂载到 DOM 元素上,这是启动渲染的关键方法。
    • unmount():卸载应用。
    • provide():提供可以在应用中的所有组件中访问的依赖项。
  3. mount() 方法: mount 方法是整个渲染过程的入口,它做了以下几件事:

    • createVNode() 创建一个 VNode (虚拟节点),VNode 是对真实 DOM 的抽象,Vue 3 使用 VNode 来进行高效的更新。
    • vnode.appContext = context 将应用上下文 context 注入到 VNode 中,这样子组件就可以访问到全局配置等信息了。
    • render() 调用 renderer.render() 方法,将 VNode 渲染到指定的 DOM 容器中。这就是真正的渲染过程。

四、createRenderer:渲染器的制造工厂

咱们接着往下挖,看看 render 函数是怎么来的。在 createAppAPI 函数中,我们可以看到:

export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootRenderFunction<HostElement>
): CreateAppFunction<HostElement> {
  // ...
}

createAppAPI 接收一个 render 函数作为参数。这个 render 函数实际上是由 createRenderer 函数创建的。

// packages/vue/src/renderer.ts

import {
  // ...省略一堆导入
} from '@vue/shared'

export interface RendererOptions<
  HostNode = any,
  HostElement = any
> {
  patchProp: (
    el: HostElement,
    key: string,
    prevValue: any,
    nextValue: any,
    isSVG?: boolean,
    prevChildren?: VNode[],
    parentComponent?: ComponentInternalInstance | null,
    parentSuspense?: SuspenseBoundary | null,
    unmountChildren?: UnmountChildrenFn
  ) => void
  insert: (el: HostNode, parent: HostElement, anchor?: HostNode | null) => void
  remove: (el: HostNode) => void
  createElement: (
    type: string,
    isSVG?: boolean,
    vnode?: VNode,
    props?: any
  ) => HostElement
  createText: (text: string) => HostNode
  createComment: (text: string) => HostNode
  setText: (node: HostNode, text: string) => void
  setElementText: (el: HostElement, text: string) => void
  parentNode: (node: HostNode) => HostElement | null
  nextSibling: (node: HostNode) => HostNode | null
  querySelector?: (selector: string) => HostElement | undefined
  setScopeId?: (el: HostElement, id: string) => void
  cloneNode?: (node: HostNode) => HostNode
  insertStaticContent?: (
    content: string,
    container: HostElement,
    anchor: HostNode | null,
    isSVG: boolean
  ) => [HostNode, HostNode]
}

export function createRenderer<HostNode, HostElement>(
  options: RendererOptions<HostNode, HostElement>
) {
  // ... 省略一堆内部函数定义,比如 patch、mountChildren 等

  const render: RootRenderFunction<HostElement> = (
    vnode: VNode | null,
    container: HostElement,
    isSVG?: boolean
  ) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(null, vnode, container, null, null, null, isSVG)
    }
    container._vnode = vnode
  }

  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

createRenderer 接收一个 options 对象作为参数,这个 options 对象包含了各种 DOM 操作方法,比如 createElementpatchPropinsert 等等。Vue 3 会根据这些方法,创建一个针对特定平台的渲染器。

比如,在浏览器环境中,options 对象会包含浏览器提供的 DOM API;在 NativeScript 环境中,options 对象会包含 NativeScript 提供的 API。

createRenderer 返回一个对象,包含了 render 函数、hydrate 函数 和 createApp 函数。这里的 createApp 函数,就是我们最开始看到的 createAppAPI 函数,它被注入了当前渲染器的 renderhydrate 方法。

重点来了! render 函数内部调用了 patch 函数,patch 函数才是真正负责将 VNode 渲染成真实 DOM 的核心函数。

五、patch 函数:新旧 VNode 的终极对决

patch 函数是 Vue 3 渲染器的核心,它负责比较新旧 VNode,并根据差异更新 DOM。由于 patch 函数的代码非常复杂,咱们这里只简单介绍一下它的工作原理:

  1. 判断 VNode 类型: patch 函数首先会判断 VNode 的类型,比如是组件、元素、文本等等。

  2. 处理不同类型的 VNode: 针对不同类型的 VNode,patch 函数会采取不同的处理方式。

    • 组件: patch 函数会创建或更新组件实例,并递归地 patch 组件的根 VNode。
    • 元素: patch 函数会创建或更新 DOM 元素,并 patch 元素的子节点。
    • 文本: patch 函数会创建或更新文本节点。
  3. Diff 算法: patch 函数使用 Diff 算法来比较新旧 VNode 的子节点,找出需要更新的 DOM 节点。Vue 3 使用了更高效的 Diff 算法,可以更快地更新 DOM。

  4. 更新 DOM: 根据 Diff 算法的结果,patch 函数会更新 DOM,比如添加、删除、移动、修改 DOM 节点。

六、流程总结:createApp 到渲染的完整路径

为了更好地理解整个过程,咱们用一张表格来总结一下:

步骤 涉及函数 描述
1. 创建应用实例 createApp -> createAppAPI createApp 函数调用 createAppAPI 函数,createAppAPI 函数创建一个应用实例 app,并初始化应用上下文 context
2. 获取渲染器 createRenderer createRenderer 函数接收一个 options 对象,包含了各种 DOM 操作方法,并返回一个渲染器对象,包含了 render 函数。
3. 挂载应用 app.mount app.mount 方法接收一个 DOM 元素作为参数,创建一个根组件的 VNode,并将应用上下文 context 注入到 VNode 中。
4. 渲染 VNode render -> patch render 函数接收一个 VNode 和一个 DOM 元素作为参数,调用 patch 函数将 VNode 渲染到 DOM 元素中。patch 函数比较新旧 VNode,并根据差异更新 DOM。

七、举个栗子:从代码到页面

// App.vue
<template>
  <h1>{{ message }}</h1>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const message = ref('Hello, Vue 3!')
    return {
      message
    }
  }
}
</script>
// main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

这段代码的执行流程如下:

  1. createApp(App):创建一个应用实例 app,根组件是 App.vue
  2. app.mount('#app'):将应用挂载到 ID 为 app 的 DOM 元素上。
  3. mount 函数内部:
    • createVNode(App):创建一个 App.vue 组件的 VNode。
    • render(vnode, document.querySelector('#app')):调用渲染器的 render 函数,将 App.vue 组件的 VNode 渲染到 #app 元素中。
  4. render 函数内部:
    • patch(null, vnode, document.querySelector('#app')):调用 patch 函数,由于是首次渲染,所以第一个参数是 null
    • patch 函数会创建 <h1> 元素,并将 message 的值 "Hello, Vue 3!" 渲染到 <h1> 元素中。
    • 最终,<h1>Hello, Vue 3!</h1> 会出现在页面上。

八、总结与思考

今天咱们一起扒了扒 Vue 3 createApp 的底裤,从 createAppcreateRenderer,再到 patch 函数,整个渲染流程就清晰多了。

当然,Vue 3 的源码远不止这些,还有很多细节值得我们深入研究。比如,组件的生命周期、响应式系统、编译器等等。

希望今天的分享能帮助大家更好地理解 Vue 3 的原理,在实际开发中更加得心应手。

下次再见! 别忘了点赞收藏哦!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注