[vue3.0源码解读]二.创建App和挂载过程

592 阅读6分钟

创建App和挂载过程

这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」。

导语

在了解到创建App的入口函数为 createApp 之后,下面要做的就是继续追踪该函数的内容以及该函数调用的子函数有哪些,分别都做了什么。尝试在阅读完源码之后回答下面几个问题:

  1. 应用实例的创建过程是怎样的(createApp的实现过程)?创建后的实例长什么样?

  2. 应用实例的过载过程是怎样的(app.mount()的实现过程)?

这两个问题也是vue3相关面试题经常被提及的几个问题。

一. 应用实例的创建过程

1. createApp

通过第一阶段的环境搭建和初步探究,知道程序入口 createApp被定义在 packages/runtime-dom/src/index.ts 的第 66 行开始:

image.png

核心代码为:

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

createApp 中返回的变量在方法中被定义为 app,是通过 ensureRender().createApp()的工厂方法进行创建的。想要了解创建过程和实例是什么样的,还需要继续了解渲染器 ensureRender,实际上 ensureRender创建出的实例对象才是创建 app 的核心,是这个对象提供的 createApp方法。

2. ensureRender 和它的 createApp()

在当前 ts 文件中定义了 ensureRender的构造函数:

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

ensureRenderer 通过 createRenderer 构造去创建 renderer实例,并且返回

继续追踪 createRenderer函数,

image.png

runtimetime-core/src/renderer.ts中定义并实现了 baseCreateRenderer,走到这里才是实现创建app的核心。重点即解读 // implementation 下的实现。

在函数的最后,终于看到了最终返回的结果代码:

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

在同文件下,定义的 createAppApi函数:

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(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 (__FEATURE_OPTIONS_API__) {
          if (!context.mixins.includes(mixin)) {
            context.mixins.push(mixin)
          } else if (__DEV__) {
            warn(
              'Mixin has already been applied to target app' +
                (mixin.name ? `: ${mixin.name}` : '')
            )
          }
        } else if (__DEV__) {
          warn('Mixins are only available in builds supporting Options API')
        }
        return app
      },

      component(name: string, component?: Component): any {
        if (__DEV__) {
          validateComponentName(name, context.config)
        }
        if (!component) {
          return context.components[name]
        }
        if (__DEV__ && context.components[name]) {
          warn(`Component "${name}" has already been registered in target app.`)
        }
        context.components[name] = component
        return app
      },

      directive(name: string, directive?: Directive) {
        if (__DEV__) {
          validateDirectiveName(name)
        }

        if (!directive) {
          return context.directives[name] as any
        }
        if (__DEV__ && context.directives[name]) {
          warn(`Directive "${name}" has already been registered in target app.`)
        }
        context.directives[name] = directive
        return app
      },

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        if (!isMounted) {
          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

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = vnode.component
            devtoolsInitApp(app, version)
          }

          return getExposeProxy(vnode.component!) || vnode.component!.proxy
        } else if (__DEV__) {
          warn(
            `App has already been mounted.\n` +
              `If you want to remount the same app, move your app creation logic ` +
              `into a factory function and create fresh app instances for each ` +
              `mount - e.g. \`const createMyApp = () => createApp(App)\``
          )
        }
      },

      unmount() {
        if (isMounted) {
          render(null, app._container)
          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = null
            devtoolsUnmountApp(app)
          }
          delete app._container.__vue_app__
        } else if (__DEV__) {
          warn(`Cannot unmount an app that is not mounted.`)
        }
      },

      provide(key, value) {
        if (__DEV__ && (key as string | symbol) in context.provides) {
          warn(
            `App already provides property with key "${String(key)}". ` +
              `It will be overwritten with the new value.`
          )
        }
        // TypeScript doesn't allow symbols as index type
        // https://github.com/Microsoft/TypeScript/issues/24587
        context.provides[key as string] = value

        return app
      }

其中里面有我们熟知的: use(插件) , mixin(混入),conponent(组件),directive(指令),mount(挂载dom)等等。各个模块都是由模块单独去实现,最终组成了 app 对象进行返回。

暂不考虑每一个模块的实现细节的话,我们大致可以将 createApp函数的实现总结为以下几个步骤:

createAppAPI -> baseCreateRenderer -> createRenderer -> ensureRenderer -> createApp -> app(use,mixin,component,directive,mount)

最终返回的 app 对象是包含 use,mixin,component,directive,mount 等属性的一个对象。

二. 实例的挂载过程

1. mount 函数

在实例创建过程中以及最后创建的实例对象中包含了一个 mount方法,这也是我们在创建实例后即调用的 mount函数:

createApp({
    xxx
}).mount('#app')

这个函数是 createApp函数返回的对象中的方法,入参是 domId。通过阅读 mount函数,可以尝试去了解实例挂载的实现过程。

mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        if (!isMounted) {
          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

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = vnode.component
            devtoolsInitApp(app, version)
          }

          return getExposeProxy(vnode.component!) || vnode.component!.proxy
        } else if (__DEV__) {
          warn(
            `App has already been mounted.\n` +
              `If you want to remount the same app, move your app creation logic ` +
              `into a factory function and create fresh app instances for each ` +
              `mount - e.g. \`const createMyApp = () => createApp(App)\``
          )
        }
      }

mount函数在最外层做了一个已加载的判断,防止开发者重复调用 mount 方法。其内部通过

render(vnode, rootContainer, isSVG) 即通过 render 函数进行实现挂载和渲染。 其中 vnode是通过

const vnode = createVNode(
                rootComponent as ConcreteComponent,
                rootProps
              )

创建出来的虚拟节点

2. render 函数

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
  }

render函数中,又再次通过 patch(container._vnode || null, vnode, container, null, null, null, isSVG),patch函数进行核心渲染,那么要了解 mount函数实现挂载的过程,实际上就是了解 patch函数的实现过程。

3. patch 函数

在不了解 patch函数之前,我们也可以发现,他至少有两个入参,即 创建出来的 虚拟dom (vnode),和挂载的 dom 对象(container)。那么我们就可以猜测,patch函数调用的目的是将虚拟 dom 转化为 真实 dom,并且追加到宿主 container 上去。

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    if (n1 === n2) {
      return
    }

    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    }
  }

在 patch 函数中,对节点做了比较和类型的分类处理,根据类型的不同分别做不同的渲染处理。 实际上patch主要就是用来对比两次虚拟dom的方法,做的主要就是diff的操作。

text类型为例: processText

const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateText(n2.children as string)),
        container,
        anchor
      )
    } else {
      const el = (n2.el = n1.el!)
      if (n2.children !== n1.children) {
        hostSetText(el, n2.children as string)
      }
    }
  }

实际上会调用 hostCreateTexthostSetText,点进去看里面的实现就会发现其目的就是对 dom 进行操作和渲染。

总结

  1. createApp 的实现过程:

createAppAPI -> baseCreateRenderer -> createRenderer -> ensureRenderer -> createApp -> app(use,mixin,component,directive,mount)

最终返回的 app 对象是包含 use,mixin,component,directive,mount 等属性的一个对象。

  1. 应用实例挂载的过程:

创建根节点虚拟dom(vNode),然后执行 render函数,通过 patch函数和 diff 算法将传入的虚拟 dom 和 宿主进行渲染,即将 dom 追加到 宿主元素上。

App.png

以上是app实例创建和挂载的主要流程,后面应当详细解读各个流程节点的实现过程是怎样的。