From creation to mount

Look at the demo:

<div id="app">
  <input :value="input" @input="update" />
  <div>{{output}}</div>
</div>

<script>
const { ref, computed, effect } = Vue
const APP = {
  setup() {
    const input = ref(0)
    const output = computed(function computedEffect() { return input.value + 5})
    
    // computedEffect & effect below is reexecuted
    const update = _.debounce(e= > { input.value = e.target.value*1 }, 50)
    
	  effect(function callback() {
        // Rely on collection
        console.log(input.value)
    })
    return {
      input,
      output,
      update
    }
  }
}
constapp = Vue.createApp(APP) app.. mount('#app')
</script>
Copy the code

The above code is a simple process for creating a component.

Create an app instance by calling createAPP API and mount the app instance to complete the component mount.

Let’s take a look at Vue3’s internal implementation from create App to mount.

CreateApp API

export const createApp = ((. args) = > {
  // 👉 Call the ensureRenderer function to create an app instance
  constapp = ensureRenderer().createApp(... args)if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }
  // 👉 constructs the original mount method
  const { mount } = app
  // 👉 override the mount function
  app.mount = (containerOrSelector: Element | ShadowRoot | string) :any= > {
    / / check containerOrSelector
    const container = normalizeContainer(containerOrSelector)
    if(! container)return
      
    The // _component property stores the parent component, so when we call createApp, the component passed in gets the component
    const component = app._component
    // Determine whether the component meets the conditions
    // If component is not functional and there is no render function,
    // Without template, we mount the innerHTML of the container as template
    if(! isFunction(component) && ! component.render && ! component.template) { component.template = container.innerHTML// Vue2 compatibility processing
      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}}}}// Empty the contents of the mount container before performing mount
    container.innerHTML = ' '
    / / mounted container
    // Execute the mount function, which renders and mounts the component
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      // Remove the V-cloak command
      container.removeAttribute('v-cloak')
      // Set the data-v-app command
      container.setAttribute('data-v-app'.' ')}return proxy
  }
  // Return the app instance
  return app
}) as CreateAppFunction<Element>
Copy the code

The ensureRenderer function creates an app instance, overwrites the mount method, and returns the app instance when one is called with the createApp ensureRenderer function.

For students with weak foundation, they may not understand the operation of structure mount and then rewrite mount. This is actually related to the way in which the JS traversal is stored, in JS functions are also objects. When overwriting a mount, you are actually pointing it to a new memory address.

The rewritten mount method does a few things:

  • callnormalizeContainerFunction to verify the mount container or selector
  • To obtainappComponent on the instance, to determine the component, if the condition does not meet, will containerinnerHTMLAs atemplate.
  • Empty the containerinnerHTML
  • The implementation structure comes outmountMethod, hang on the component,mountMethod is responsible for rendering the mount component

Let’s look at the ensureRenderer function that creates app:

// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
// Create the renderer inertly - this makes the core renderer logic tree tree-shakable
// In some cases, users will only use Vue's responsive system
let renderer: Renderer<Element> | HydrationRenderer

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

Copy the code

The main reason Vue3 chose to layer the renderer was for tree-shakable, because users would probably only use Vue3’s responsive system.

As you can see from the code, inside is the renderer function created through the createRenderer API.

RendererOptions is a renderer configuration item that contains:

  • rightDOMOf nodes: Insert, move, create, set properties, clone
  • For node attributes,class,stylethepatch
export const render = ((. args) = >{ ensureRenderer().render(... args) })as RootRenderFunction<Element>

Copy the code

The Render API is also created with ensureRenderer.

Now let’s dive into the createRenderer API,

CreateRenderer functions; The renderer is created by calling baseCreateRenderer, and we know from the previous code that baseCreateRenderer returns an object with createAPP, Render,

export function createRenderer<
  HostNode = RendererNode.HostElement = RendererElement> (options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}
Copy the code

Let’s take a look at the shortened version of the baseCreateRenderer function, where we’ll look directly at the return result.

BaseCreateRenderer function:

function baseCreateRenderer(options: RendererOptions, createHydrationFns? :typeof createHydrationFunctions
) {
    /** * omit some code... * /
    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, // Server render related
      createApp: createAppAPI(render, hydrate)
    }
}
Copy the code

Render API (createAPP, createAppAPI, createAppAPI, createAppAPI, createAppAPI)

EnsureRenderer indicates that createAppAPI returns a function that is the app instance created by that function. Now comes the key part. Let’s analyze the createAppAPI function.

In fact, I had planned to analyze it directly from the baseCreateRenderer function, but I realized that this analysis might not make sense to people. So with this paper, this paper kind of foreshadores the introduction of baseCreateRenderer’s analysis. The baseCreateRenderer function contains too much information

CreateAppAPI function:

export function createAppAPI<HostElement> (render: RootRenderFunction, hydrate? : RootHydrateFunction) :CreateAppFunction<HostElement> {

  // createApp API
  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
    }
    
    // Create App execution context, which is actually a JS object
    const context = createAppContext()
    // Plug-in collection
    const installedPlugins = new Set(a)let isMounted = false
    
    // App context instance
    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.`)}},// How to configure the plug-in
      // During configuration, the system will check whether there is a duplicate. If there is already one, a prompt will be given
      // Our plug-in can be a function or an instance of a class with the install method
      // If the install type is used, the app instance and options are passed in
      // If it is a function, this is passed to the app instance and the options configuration item
      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
      },
      / / with
      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
      },
      
      // Configure global components
      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
      },
      // Configure the global directive
      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 methodmount( rootContainer: HostElement, isHydrate? :boolean, isSVG? :boolean) :any {
        if(! isMounted) {/ / create a Vnode
          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
            render(vnode, rootContainer, isSVG)
          }
          / / finish mounted
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry; (rootContaineras any).__vue_app__ = app

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = vnode.component
            devtoolsInitApp(app, version)
          }
          // Return component Proxy
          returnvnode.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)\``)}},// Uninstall the component
      unmount() {
        if (isMounted) {
          // Vnode === null during uninstallation
          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.`)}},// Assign data to the provides property of the context
      provide(key, value) {
        if (__DEV__ && (key as string | symbol) in context.provides) {
          warn(
            `App already provides property with key "The ${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
      }
    })
    // Compatible processing
    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }

    return app
  }
}
Copy the code

The app object contains functions such as use, mixin, Component, directive, mount, unmount, and provide, all of which eventually return the app object, so we can implement chain calls:

app.use().component().mixin().mount()

As you can see from the above code, createAPP ultimately returns an App configuration object. The createAPP API creates a Vnode by calling the createVnode method and the Render method in the baseCreateRenderer function. To complete the Vnode rendering.

When app.mount(‘#app’) is executed in the code, the Vnode will be created and rendered.

The createVnode function returns a Vnode that is the JS description object of a Vnode, which can be found at juejin.cn/post/704248…

CreateAppContext, instance context object:

export function createAppContext() :AppContext {
  return {
    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()}}Copy the code

conclusion

In the process of analyzing the whole link here, I also wondered why Utah made the link so deep. There are four or five layers of functions between createApp and createApp.

When I looked at some of the source code for the baseCreateRenderer function, it was mainly the separation of the code responsibilities. Let baseCreateRenderer copy Vnode’s patch and render tasks and createApp create the instance.

Use closures in the createApp function to access all methods defined in baseCreateRenderer.

The end of this paper is baseCreateRenderer’s learning analysis, which can fill in a lot of holes.