Hello, here’s Link 😋. Having seen the native node mount, let’s see how Vue instantiates a component. The flow charts in this article are available in vue-core-analyse, my source project, Star✨

The flow chart

Demo

Let’s write two demos based on the usual usage of components

Vue.com Ponent global registration

Register a HelloWorld component globally

Vue.component('HelloWorld', {
    name: 'HelloWorld'.template: '<div>{{ msg }}</div>'.data() {
      return {
        msg: 'Hello-world'}}})Copy the code

In-component use

This is actually a file I generated with scaffolding and added a HelloWorld component to app.vue

// mian.js
import Vue from '.. /.. /.. /vue'
import App from './App.vue'

new Vue({
  el: '#app'.render: h= > h(App)
})
Copy the code
// App.vue
<template>
  <div id="app">
    <h1>The common node</h1>
    <HelloWorld />
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
  name: 'App'.components: {
    HelloWorld
  },
}
</script>
Copy the code
  • This tree diagram shows their relationship

The principle of Vue.com ponent

First let’s look at the component function definition, which is created and assigned to the Vue constructor by the initAssetRegisters(Vue) function when executed through the Vuejs file

const ASSET_TYPES = [
  'component'.'directive'.'filter'
]
export function initAssetRegisters (Vue) {
  /** * where the component function is initialized */
  ASSET_TYPES.forEach(type= > {
    /** * Vue.component */
    Vue[type] = function (id, definition) {
      if (type === 'component' && isPlainObject(definition)) {
        definition.name = definition.name || id The name / / assignment
        // Prototype chain inheritance method
        definition = this.options._base.extend(definition)
      }
      this.options[type + 's'][id] = definition
      return definition
    }
  })
}
Copy the code

Here we skip over the logic of directive and filter to see that when type === ‘component’ a function extend is executed and attributes such as data are passed to the component.

We can pull out the component function, which looks something like this:

Vue.component = function (id, definition) {
    definition.name = definition.name || id The name / / assignment
    definition = this.options._base.extend(definition) // Prototype chain inheritance method
    
    this.options[type + 's'][id] = definition 
    return definition
}
Copy the code

The _base function is the Vue constructor itself, which I mentioned in the first article. We use the vue. extend method to convert the Definition object into a constructor, so that we can instantiate it as new when we use the component later.

Since the extend function is also used for non-global component registrations, we’ll see how it works later.

Principles of use within components

Let’s take a look under the scaffolding, how is the first component App generated

import Vue from '.. /.. /.. /vue'
import App from './App.vue'
new Vue({
  el: '#app'.render: h= > h(App)
})
Copy the code

You can see here that we gaveRender functionthehThe enteredAppComponent, because our whole project is made up ofwebpackBuilt, after the project runs, all of us*.vueThe page will bevue-loaderTo an object that describes the current component

How do I initialize the first tag in Vue? This article reviews the new Vue instantiation process, and the principle of h function. If you feel that the following is a bit of a jump, I recommend reading this article

Execution of new Vue()

When the first Vue is instantiated, the _init function is executed according to the initialization logic, and finally the $mount function is executed. And Vue will see that we already have a render function and use it directly.

During Vue instantiation, an instance of Watcher is created and a function in Watcher controls the generation and implementation of components and labels. This updateComponent is executed by default the first time, and it is executed later when the data changes, but this is the content of a responsive system and will be examined in more detail in a future article

updateComponent = () = > {
  vm._update(vm._render(), hydrating)
}
Copy the code

This function can be divided into two parts: vm._render(), vm._update(). The former is responsible for generating tags and components, while the latter is responsible for mounting them to the real DOM tree


Component Vnode generation

Let’s start with the vm._render() function, which takes our custom render function and executes it

  // vm._render()
  Vue.prototype._render = function () :VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    // render self
    let vnode  = render.call(vm._renderProxy, vm.$createElement)
    
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
Copy the code
  • Pay attention to thisvm.$createElementIs ourRender functionThe receivedParameter h

It does two things:

  1. Execute ourmain.jsThe incomingRender function
  2. returnRender functionA generated virtual nodeVNode

The core is the vm.$createElement function that receives the App component we passed in

The vm $createElement method function

Between h and _createElement, there are several other functions that do the same thing with parameters, but we don’t need to worry about that. All we need to know is that the tag parameter is the component App that we passed in to h

export function _createElement (
  context: Component, // The current component instancetag? : string | Class<Component> |Function | Object.// Function component labeldata? : VNodeData, children? : any,) :VNode | Array<VNode> {
  let vnode, ns
  // Tag is a string
  if (typeof tag === 'string') {
    let Ctor
    // Check if the tag is a reserved tag
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        tag, data, children,
        undefined.undefined, context
      )
    } else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
      // component
      // When the project is initialized
      vnode = createComponent(Ctor, data, context, children, tag)
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
}
Copy the code

Through graph combing, its logic is as follows:

Now obviously tag is a component object, so it goes to the left. What does createComponent do

createComponent

export function createComponent (
  Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
  // baseCtor Vue constructor
  const baseCtor = context.$options._base

  // Prototype chain inheritance
  // The component comes in as an object and needs to be inherited again
  // Globally registered components are not required. They are inherited when initialized
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // Register some component management hooks on placeholders
  installComponentHooks(data)

  // Return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
    data, undefined.undefined.undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}
Copy the code

As you can see from the code, this function does three things altogether:

  1. Through the functionextendwillCtor component objectTransformed intoCtor component constructor
  2. installComponentHooks(data)Mount inline hooks
  3. The currentComponent constructortoVnodeAnd return

Implementation of the extend function

The extend function actually completes the generation of component functions through the form of prototype chain inheritance

The important code is in 3 steps:

  1. To define aSub function, internal executionthis._initThe method, which is usnew Vue()Executed by default_init method
  2. Sub.prototype = Object.create(Super.prototype)
  3. Sub.prototype.constructor = Sub

These three steps complete the core of prototype chain inheritance

  Vue.cid = 0
  let cid = 1
  Vue.extend = function (extendOptions: Object) :Function {
    // Options for the current component
    extendOptions = extendOptions || {}
    const Super = this // Vue = _base = this Super is usually Vue
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    / / check the cache
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }
    // define Sub to execute _init internally
    const Sub = function VueComponent (options) {
      this._init(options) / / ✨
    }
    // Prototype chain inheritance
    // The constructor prototype of the current component points to the prototype of Vue (indicating that the component constructor is instantiated through Vue)
    Sub.prototype = Object.create(Super.prototype) / / ✨
    // The prototype of the current constructor points to the constructor
    Sub.prototype.constructor = Sub / / ✨
    Sub.cid = cid++
    // Merge the configuration of Vue and the current instance.
    // Globally registered components, global mixins, etc. are merged into child components through this function
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super
    // component and other functions that create components
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // Allow the component to reference itself
    if (name) {
      Sub.options.components[name] = Sub
    }

    / / to join the cache
    cachedCtors[SuperId] = Sub
    return Sub
  }
}
Copy the code

However, there is much to learn from this function, such as the cachedCtors cache notation, which can cache Ctor effectively so that the same component doesn’t have to go through the same steps in the future. For example, mergeOptions is also a highly reusable function in the global context of Vue. The internal configuration of parameter 1 can be merged into parameter 2 through a policy mode, such as global Mixins, where the globally introduced components are merged. We’ll look at the implementation of this function later when we talk about lifecycle functions.

installComponentHooks

What this function does is simply assign four hooks (init, prepatch, Insert, destroy) to a Vnode, which will be used when a component is converted to a real node. We’ll see what they do later


Another case we didn’t analyze in the figure above is the possibility that the tag we pass in is a string, which is also a component.

Example: a HelloWorld component that we write in an HTML tag (Demo above). That’s going to go to the right hand side of the picture.

As we all know, template compilation will eventually convert the HTML structure to a render function, even inside an App component, which will eventually be converted to something like this:

  • There’s one in the picture_c functionActually, this is it$createElement method function, justVueThere will be two versions implemented, one for user use, and_cVue is used internally. The difference is that the child nodes are treated in a way that, for space reasons, I won’t expand.
    vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };

    vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
Copy the code

As you can see, we pass $createElement a string “HelloWorld”, which goes to the resolveAsset function, which changes the component name to camel case, uppercase, Then go to the options of the parent component to find the definition of the component, and return the constructor if it has one.

The globally registered component uses the MergeOptions function to assign a reference to the child component, so inside the child component is the constructor of the globally registered component. This eliminates the need to pass extend inside the creatComponent function and uses the constructor directly. Eventually, of course, a VNode is returned.

Ok, so here we are looking at the generation of component virtual nodes. Let’s sum it up:

  1. The createElement function executes the creatComponent function inside the createElement function. This function extends the prototype chain and converts the current component to a component constructor, just like the Vue constructor

  2. Components, like native tags, are also generated as virtual nodes, commonly called placeholder virtual nodes


Component mount

Back to this function, that’s all vm.render() does, and it finally returns a Vnode that will be mounted to the DOM tree via vm._update(). Now let’s look at what this function does differently for components

updateComponent = () = > { 
    vm._update(vm._render(), hydrating) 
}
Copy the code

The _update function has only two core operations to focus on in the component mount process:

  1. Convert components and native virtual nodes to real nodes
  2. Insert to its parent node

Creation of a real node

When _update is executed, a creatElm function is executed

It does four things:

  1. Try to combine the currentVNodeAs acomponentcreate
  2. If not, willVnodeThrough the nativecreateElementCreate a real node
  3. Create child nodes recursively
  4. Insert to the parent node

  function createElm (
    vnode,              // The current virtual node
    insertedVnodeQueue,
    parentElm,          // Parent real node
    refElm,             // this is used for node insertion
    nested,             // The child is created with true to determine whether it is the root node
    ownerArray,
    index
  ) {
    // Try to create the current VNode as a component
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    // Create a real node
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    // Create child nodes recursively
    createChildren(vnode, children, insertedVnodeQueue)
      / / insert
    insert(parentElm, vnode.elm, refElm)
  }
Copy the code
  • createChildrenThe implementation of the
  function createChildren (vnode, children, insertedVnodeQueue) {
    for (let i = 0; i < children.length; ++i) {
      // Create child nodes recursively
      createElm(children[i], insertedVnodeQueue, vnode.elm, null.true, children, i)
    }
  }
Copy the code

Creation of child components

The next step is to focus on createComponent. At this point, you are entering the creation of child components within the component.

Remember when our component Vnode was created, one step was to install four inline hooks into vNode.data? Oh, that’s where it’s used. Let’s look at the first one that uses hook init

  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
      let i = vnode.data

      if (isDef(i = i.hook) && isDef(i = i.init)) { // assign init here
        i(vnode, false /* hydrating */)}/** * 1. The native node in the component is inserted during the creatElm phase. This is the parent root node */ inserted into the current component
      insert(parentElm, vnode.elm, refElm)

      return true
  }
Copy the code

The init function executes the component Vnode’s Ctor constructor

init (vnode: VNodeWithData, hydrating: boolean): ? boolean {const child = vnode.componentInstance = new vnode.componentOptions.Ctor(options)
    // Implement mount here, _init globally does not go to $mount
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
},
Copy the code

Remember the definition of a constructor?

const Sub = function VueComponent (options) {
  this._init(options)
}
Copy the code

The component performs _init in Vue once more and then goes through processes like new Vue(), initializing method, data, and the vm.render() vm._update() mentioned above to generate its own internal native nodes, components, etc.

Finally, when the component is instantiated, it is inserted into the parent node via insert.

function insert (parent, elm, ref) {
    if (isDef(parent)) {
      if (isDef(ref)) {
        // If the ref element exists, insert it before the ref element
        // This element is the next real sibling of the real node
        // I guess the insertion position is more accurate this way?
        if (nodeOps.parentNode(ref) === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        // Add it directly to the parent element
        nodeOps.appendChild(parent, elm)
      }
    }
  }
Copy the code

It might be a little tricky to look at in code, but let’s look at the diagram

conclusion

Each component is essentially a subclass of Vue, and the component instance is the procedure of new Vue(), a recursive procedure. This may be a little confusing when you first see it, but I highly recommend you step through it and get a feel for it.

Source code here we can see Vue’s first design concept.

For Vue, every component instantiation that executes the _init method creates a new Watcher instance that is used to subscribe to data changes.

What’s the good of that? The diff process can be limited to components rather than done as a whole. There is no need to propose Fiber architecture due to the massive computing footprint of React

Thank 😘


If you find the content helpful:

  • ❤️ welcome to focus on praise oh! I will do my best to produce high-quality articles

Contact author: Linkcyd 😁 Previous:

  • Vue is how to initialize the first tag?

  • # Vue source code analysis series ①: Data attributes can be directly through this. Access?

  • React Get started with 6 brain maps

  • Interviewer: Come on, hand write a promise

  • Prototype and Prototype Chain: How to implement Call, Bind, New yourself