2. vue3源码:createApp

84 阅读4分钟

前言

这里我们最后定义了自己的例子,核心代码非常简单,只有一行如下:

const demo = Vue.createApp({
            data() {
                return {
                    message: 'Hello Vue!'
                }
            }
        }).mount('#app')

那么我们就来看看这一行代码的前半部分createApp做了什么事情。

createApp源码解析

该方法的源码如下:

export const createApp = ((...args) => {
  console.log('createApp', args)
  const app = ensureRenderer().createApp(...args)

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

  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) {
      // __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.
      component.template = container.innerHTML
      // 2.x compat check
      if (__COMPAT__ && __DEV__) {
        for (let i = 0; i < container.attributes.length; i++) {
          const attr = container.attributes[i]
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null
            )
            break
          }
        }
      }
    }

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

以上代码主要做了以下几件事情

  1. 创建应用实例app
  2. 开发环境做处理
  3. 重写app实例的mount方法
  4. 返回app实例

因此简单来说createApp就是创建一个app实例,这跟vue2创建应用实例的方法new Vue()不同,可以将它看成vue2vue3的第一个区别之处。

那么接下来我们就对创建app实例的过程做一个详细的了解。

1. 创建应用实例app

创建实例的代码就是下面这么短短一行:

const app = ensureRenderer().createApp(...args)

于是我们按照方法调用分2步展开。(心里默念创建app实例分2步展开)

1). ensureRenderer()

我们顺藤摸瓜ensureRenderer -> createRenderer -> baseCreateRenderer,关键方法正是baseCreateRenderer,然后我们发现这个方法的代码有2000行左右,嗯,要打退堂鼓了有没有?挺住!它其实就是定义了一系列处理节点或者组件的方法,我们先把这些方法列出来而不必知道里面具体的逻辑, patch processText processCommentNode mountStaticNode patchStaticNode moveStaticNode removeStaticNode processElement mountElement setScopeId mountChildren patchElement patchBlockChildren patchProps processFragment processComponent mountComponent updateComponent setupRenderEffect updateComponentPreRender patchChildren patchUnkeyedChildren patchKeyedChildren move unmount remove removeFragment unmountComponent unmountChildren getNextHostNode render hydrate

重点关注最后的返回。

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  // compile-time feature flags check
  if (__ESM_BUNDLER__ && !__TEST__) {
    initFeatureFlags()
  }
  // 获取全局对象:为了满足跨平台做了判断
  const target = getGlobalThis()
  // 给全局对象添加_VUE_属性,暂时不知这个属性有什么作用
  target.__VUE__ = true
  // 判断环境
  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__, target)
  }
  ...... // 一系列方法
    return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate)
      }
  }

所以创建app实例的第一步是返回含renderhydrate, createApp3个方法的对象, 而createAppAPI执行返回真正的createApp,作为闭包,它可访问入参renderhydrate

image.png 因此我们做一个小总结:ensureRenderer() = {render, hydrate, createApp}, ensureRenderer()返回的对象调用createApp方法就得到了app实例,于是我们就来到了第2步,这个createApp做了什么?

2). createApp(...args)

放删减的代码:

function createApp(rootComponent, rootProps = null) {
    if (!isFunction(rootComponent)) {
      rootComponent = { ...rootComponent }
    }

    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
    // 创建app就在这里啦,定义了实例方法use, mixin, component, directive, mount, unmount, provide等方法 并把app挂载在上下文context
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _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, mixin, component, directive, mount, unmount, provide等方法
        
        context.provides[key as string | symbol] = value

        return app
      }
    })

    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }

    return app
  }

我们惊喜地发现,平时我们使用的实例方法use, mixin, component, directive, mount, unmount, provide正是在这里定义的,所以一个app实例很显然包含以下属性,在以上代码中都有迹可循。 image.png 回过头来,我们上面所有种种操作,最终就是为了拿到以上截图打印的对象(哭唧唧,就是为了拿一个对象)。

补充:app上下文

{
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      errorHandler: undefined,
      warnHandler: undefined,
      compilerOptions: {}
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null),
    optionsCache: new WeakMap(),
    propsCache: new WeakMap(),
    emitsCache: new WeakMap()
  }

2. 开发环境做处理

  1. 注入 isNativeTag, 挂载在app.config,用于判断是否为原生标签,校验组件名称。
  2. 编译选项校验

3. 重写app实例的mount方法

这一步重中之重,就放在下一篇文章详解啦

说明

vue version": "3.2.45

以上代码仓库

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情