Vue3源码解读(一)-createApp

14,780 阅读4分钟

前言

距离上一篇文章源码解读预热过去了两周多的时间了,度过了国庆和中秋节,迟到的中秋国庆快乐送给大家,祝大家no bug,线上不报警,天天早下班。

Vue3系列文章将会围绕文件夹来进行讲解,主线流程走通后,再进行部分的讲解,会按照这个原则进行Vue3源码的逐一阅读和分析。

正文

正文从这里开始,Vue3源码分析之路从这里开始,

createApp

Vue3中创建应用的方式是通过createApp来进行的,较之前new Vue的方式在使用上基本无差别。

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

  if (__DEV__) {
    injectNativeTagCheck(app)
  }

  const { mount } = app
  app.mount = (containerOrSelector: Element | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    container.innerHTML = ''
    const proxy = mount(container)
    container.removeAttribute('v-cloak')
    container.setAttribute('data-v-app', '')
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

上面代码createApp就是咱们在创建应用时调用的方法

  • ensureRenderer是一个单例模式的函数,会返回一个renderer,如果无renderer则会调用createRenderer进行获取renderer,获得了一个app实例;
  • dev环境下注册一个方法:isNativeTag,挂载到app.config下面;
  • 获取到实例的mount方法,并保存下来;
  • 重写实例的mount方法;
    • 调用normalizeContainer获取根元素容器;
    • 判断template,获取需要渲染的模板;
    • 把容器的innerHTML置空;
    • 调用上面实例的mount方法;
    • 删除v-cloak属性,添加data-v-app属性;
    • 返回mount后的代理;
  • 返回实例;
// 上面代码中所使用到的两个简单的函数源码
// @file packages/runtime-dom/src/index.ts
// 获取renderer
let renderer: Renderer<Element> | HydrationRenderer
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
// normalizeContainer,获取元素
function normalizeContainer(container: Element | string): Element | null {
  if (isString(container)) {
    return document.querySelector(container)
  }
  return container
}

至此,createAPP方法讲解完成。其中的核心包括两部分,第一部分就是调用createRenderer的实现;第二部分就是实例的mount方法的实现。

createRenderer

上面说到createAPP里面调用此方法获取renderer对象,来看下这个的实现。createRenderer接受一个对象参数rendererOptions, const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps)

nodeOps和Vue2的时候的node-ops是差不多的功能,不过在Vue3中进行了优化,把patch和clone增加到了nodeOps里面,去掉了部分功能;

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

baseCreateRenderer的实现如下:

// @file packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
    options: RendererOptions,
    createHydrationFns?: typeof createHydrationFunctions
): any {
	
    // balabala,一堆函数的声明

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

可以看到上面的代码,最后返回的是render函数,hydrate函数和createApp函数,createApp函数是通过调用createAppAPI来获取到的。

// @file packages/runtime-core/src/apiCreateApp.ts
export function createAppContext(): AppContext {
  return {
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      isCustomElement: NO,
      errorHandler: undefined,
      warnHandler: undefined
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null)
  }
}

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,

      version,

      get config() {
        return context.config
      },

      set config(v) {
        if (__DEV__) {
          warn(
            `app.config cannot be replaced. Modify individual options instead.`
          )
        }
      },
      use() {},
      mixin() {},
      component() {},
      directive() {},
      mount() {},
      unmount() {},
      provide() {}
    })
    return app
  }
}

如上,就是createAppAPI的源码,直接返回了一个函数createApp。createApp:

  • 对传递进来的第二个参数,也就是root props进行校验;
  • 调用createAppContext创建appContext对象,赋值给context;
  • 创建变量installedPlugins,Set类型,存储已经安装过的插件;
  • isMounted设为false;
  • 创建app,挂载属性和函数;
  • 返回app;

此时的app属于Vue的一个准备阶段,为后面的mount等操作准备好了所需要使用到的函数。

mount

最核心(繁琐)的操作都在mount里面,这里面包括Vnode,render,patch等等所有的核心功能。咱们先理一下mount的调用流程。

  • 第一步:调用runtime-dom下面index.ts里面的mount函数;
  • 第二步:调用runtime-core下面apiCreateApp里面的mount函数;

前面这两步骤和Vue2中的调用是相似的,先调用平台相关的mount,再调用核心相关的mount;

  • 第三步:直接调用createVNode;与Vue2不同的是,2并不会直接调用,而是建立一个watcher,使用watcher调用get的时候来调用,详情可见Vue2源码解读(七)-mount
  • 第四步:调用render,判断是进行卸载还是进行渲染;
  • 第五步:调用patch,进行dom diff,渲染页面。

结语

本篇文章算是简单的开了个小头,接下来会对mount部分进行详细的分析和解读。

路漫漫其修远兮,吾将上下而求索

听说最近react出了个recoil的官方状态管理库,更加简单的管理state了,期待Vue后续的跟进。