Vue3 源码解析系列之初始化流程(三)

654 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

前言

上一篇我们找到了入口文件,并分析到了 createApp 方法,在createApp 中,首先运行了ensureRenderer方法,然后再调用ensureRenderer里面的createApp方法,来生成 app, 然后再为 app重构了 mount 方法。

createRenderer

ensureRenderer 只做了一件事,就是调用了createRenderer生成了渲染器,并赋值给renderer

// packages/runtime-dom/src/index.ts
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer

function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

ensureRenderer 位于 /packages/runtime-core/src/renderer.ts 中。

// packages/runtime-core/src/renderer.ts
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

里面只有一行代码,返回了baseCreateRenderer的结果,我们来看看baseCreateRenderer的实现

function baseCreateRenderer(options, createHydrationFns) {
  // 代码过长省略N行 ...
   const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }

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

这个方法的代码非常长,足有2000行,我们先跳过,直接看返回值,结果返回了一个render方法、hydrate属性,这里还返回了真正的createApp方法,由 createAppAPI 生成,并返回到了ensureRenderer进行执行,生成 app,也就是我们之前分析过的这句话 const app = ensureRenderer().createApp(...args)

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    const context = createAppContext()

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

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

      mixin(mixin: ComponentOptions) {},

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

      directive(name: string, directive?: Directive) { },

      mount(): any {},

      unmount() { /** 忽略 */ },

      provide(key, value) { /** 忽略 */ }
    })
    return app
  }
}

由这个方法我们就能很清楚的知道所创建的 app 的所有属性和方法。

mount

我们回到 packages/runtime-dom/src/index.ts 里面的 createApp 方法里。我们之前说过,在这里面对 mount 进行了重构

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
  }
})

首先他把原本 app 里面的 mount 方法抽出来。然后重新为 app.mount 赋值一个方法,这个方法做了这几件事情。

  • 调用 normalizeContainer 获取元素容器;
  • 判断template,获取需要渲染的模板;
  • 把容器的innerHTML置空;
  • 调用旧的mount方法,并返回 proxy
  • 删除v-cloak属性,添加data-v-app属性;
  • 返回 proxy

我们再回去 createAppAPI 方法里,看看 mount是怎么实现的。

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    mount(
      rootContainer: HostElement,
      isHydrate?: boolean,
      isSVG?: boolean
    ): any {
      // 是否初始化挂载,只执行一次
      if (!isMounted) {
        const vnode = createVNode(
          rootComponent as ConcreteComponent,
          rootProps
        )
        vnode.appContext = context

        render(vnode, rootContainer, isSVG)

        isMounted = true
        app._container = rootContainer
        return getExposeProxy(vnode.component!) || vnode.component!.proxy
      },
    }
  }
}
  • 首先判断是否挂载过,说明这个挂载方法只在初识化时执行一次。
  • 通过 createVNode 创建根节点的 vnode
  • 然后在执行 render 方法进行渲染,render 方法是通过参数传进来的。

小结

这篇主要讲我们顺着createApp 这条线下去找,找到了 app 的属性,然后知道了在 mount 里面创建了根节点的 vnode,最后调用了render方法,对这个vnode 进行了渲染。 下一篇我们主要会围绕这个 render 方法,了解是怎么渲染到页面上的。下面是这部分的一个简单流程图。

image.png