带你看Vue3源码: Vue.createApp究竟做了什么

1,569 阅读4分钟

笔者在上一篇文章《debug一下,你就学会高效阅读开源项目代码~》中以 Vue3 源码调试配置为案例,给大家介绍了调试的基本配置以及强调了其在查看源码过程中的重要性。本文将使用调试的技巧,在不需要详细了解完整代码实现的前提下,快速了解 Vue3 的执行流程。

最简单的代码

众所周知,调用 Vue.createApp 方法便创建了一个 Vue3 应用,本文从这个方法入手,一步一步剖析该方法在源码中的实现,从而使得大家能够了解 Vue3 整体的执行流程。

首先,在源码目录创建 packages/vue/examples/hello.html 文件:

<script src="../dist/vue.global.js"></script>

<div id="demo">{{text}}</div>

<script>
  debugger
  Vue.createApp({
    data: () => ({
      text: 'hello world'
    })
  }).mount('#demo')
</script>

代码功能非常简单:在页面中打印 hello world 字符串。这里在执行 Vue.createApp 前插入 debugger 代码,使得代码执行在 debugger 处暂停下来。

调试开始

这里还是贴一下调试的配置,如需更详细的了解调试原理和流程,请访问笔者上一篇文章:《debug一下,你就学会高效阅读开源项目代码~》。

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Launch hello",
      "url": "http://localhost:8080",
      "webRoot": "${workspaceFolder}",
      "file": "${workspaceFolder}/packages/vue/examples/hello.html"
    }
  ]
}

Untitled.gif

点击调试按钮,程序在 debugger 处暂停,然后执行到 Vue.createApp 处单步进入,断点进入到 packages/runtime-dom/src/index.ts 中的 createApp 方法。

createApp

我们直接来看 createApp 方法的源码,这里有部分代码删减,主要是针对 dev 环境的一些方法实现,不影响主体流程,下同。

// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }

    // 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>

暂且不看 const app = ensureRenderer().createApp(..args) 这行代码的内部实现,我们直接单步跳过,在左侧的调试面板可以看到 app 的值如下所示:

image.png

那么我们合理猜测,ensureRenderer().createApp(...args) 这行代码使用传进来的参数进行属性和方法初始化,并且挂载在 app 变量中返回

返回 app 变量后,取出原本的 mount 方法,然后将一个新的方法实现挂载到 appmount 属性中,也就是 html 文件中 .mount('#demo') 代码块的具体实现,最后返回 app 变量。

ensureRenderer().createApp(...args)

单步进入到 const app = ensureRenderer().createApp(...args) 这行代码如下所示:

create.gif

这里涉及到两个方法:ensureRenderercreateApp,我们逐个来看。

ensureRenderer

// packages/runtime-dom/src/index.ts
function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

这个方法的核心是 createRenderer,这里不展开这个方法的具体源码,我们看这个方法返回值的定义:

// packages/runtime-core/src/renderer.ts
export interface Renderer<HostElement = RendererElement> {
  render: RootRenderFunction<HostElement>
  createApp: CreateAppFunction<HostElement>
}

createRenderer 返回一个对象,具有 rendercreateApp 两个属性,render 是下面 mount 提到的渲染函数,这里不展开讲。createApp 方法是 createAppAPI 的返回值,源码如下所示:

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }
    const context = createAppContext()
    const installedPlugins = new Set()
    let isMounted = false
    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) {
      },

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        
      },

      unmount() {

      },

      provide(key, value) {

      }
    })
    return app
  }
}

源码验证了我们的猜测:createApp 将若干属性和方法挂载在 app 这个变量中,最后并返回 app

mount

const proxy = mount(container, false, container instanceof SVGElement) 这句代码中打一个断点并且单步进入,可以得到 mount 方法的代码实现:

// packages/runtime-core/src/apiCreateApp.ts
mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  // 用一个变量来控制 mount 只执行一次
  if (!isMounted) {
    // 创建一个虚拟node节点
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
    // store app context on the root VNode.
    // this will be set on the root instance on initial mount.
    vnode.appContext = context

    // HMR root reload
    if (__DEV__) {
      context.reload = () => {
        render(cloneVNode(vnode), rootContainer, isSVG)
      }
    }

    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      render(vnode, rootContainer, isSVG)
    }
    isMounted = true
    app._container = rootContainer
    // for devtools and telemetry
    ;(rootContainer as any).__vue_app__ = app

    return vnode.component!.proxy
  }
}

mount.gif

可以看到,当我们单步跳过 render(vnode, rootContainer, isSvg) 这行代码时,hello world 字符串显示在浏览器上了,也就是说,mount 方法将 Vue 组件挂载到浏览器上,而 render 则是关键的渲染方法。

这里有一个亮点是 mount 调用 createVNode 方法来创建虚拟节点,render 接收虚拟节点参数进行渲染,本文不再展开讲这一部分,有兴趣可以关注笔者后面的文章。

总结

本文为笔者连载 Vue3 源码阅读的第一篇,不涉及各种原理的解读,主要目的是在不需要阅读详细源码的前提下,快速了解 Vue3 组件的执行流程。

希望读者在通读本文之后,能够通过调试的方式自行高效地阅读 Vue3 源码,如果各位觉得本文能给大家带来帮助,点个赞再走呗~~~