The design of component mechanism allows developers to divide a complex application into functional independent components, reducing the difficulty of development, but also provides excellent reuse and maintainability. In this article, we work together to understand the underlying implementation principles of components from the perspective of source code.

What does the component do when it registers?

The first step to using components in Vue is registration. Vue provides both global registration and local registration.

The global registration mode is as follows:

Vue.component('my-component-name', { / *... * / })
Copy the code

Local registration is as follows:

var ComponentA = { / *... * / }

new Vue({
  el: '#app'.components: {
    'component-a': ComponentA
  }
})
Copy the code

Globally registered components that are used in any Vue instance. A partially registered component can only be used in the Vue instance where the component is registered, or even in the child components of the Vue instance.

Those who have some experience in using Vue know the difference above, but why is there such a difference? We explain this in terms of the code implementation of the component registration.

// Vue.component core code
// ASSET_TYPES = ['component', 'directive', 'filter']
ASSET_TYPES.forEach(type= > {
    Vue[type] = function (id, definition
    ){
      if(! definition) {return this.options[type + 's'][id]
      } else {
        // Component registration
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          // If definition is an object, you need to call vue.extend () to convert it to a function. Vue.extend creates a subclass of Vue (component class) and returns the constructor of that subclass.
          definition = this.options._base.extend(definition)
        }
        
        / /... Omit other code
        // The key here is to add the component to vue.options in the constructor's options object.
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
Copy the code
// The Vue constructor
function Vue(options){
 if(process.env.NODE_ENV ! = ='production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)
    
}

// Merge option objects in Vue initialization
Vue.prototype._init = function (options) {
    const vm = this
    vm._uid = uid++
    vm._isVue = true
    / /... Omit other code
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      // Merge the vUE option object, merge the constructor option object and the option object in the instance
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    / /... Omit other code
  }
Copy the code

The main code for component registration is extracted above. You can see that the option object of the Vue instance consists of two parts: the constructor option object of the Vue instance and the option object of the Vue instance.

Globally registered components are actually added to the Vue.options.components option object of the Vue constructor via Vue.com Ponent.

The option object specified by the Vue at instantiation (new Vue(options)) is merged with the constructor’s option object as the final option object of the Vue instance. Thus, globally registered components are available in all Vue instances, whereas locally registered components in Vue instances affect only the Vue instance itself.

Why do component tags work properly in HTML templates?

We know that components can be used directly in templates just like normal HTML. Such as:

<div id="app">
  <! -- Use button-counter-->
  <button-counter></button-counter>
</div>
Copy the code
// Globally register a component named button-counter
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0}},template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

// Create a Vue instance
new Vue({
    el: '#app'
})
Copy the code

So what happens when Vue parses into a custom component tag?

Vue parses component tags just like normal HTML tags; it does not treat non-HTML standard tags differently. The first difference in processing occurs when the VNode node is created. Vue creates vNodes internally using the _createElement function.

export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {

  / /... Omit other code
  
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // If it is a normal HTML tag
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined.undefined, context
      )
    } else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
      // for component tags, e.g. my-custom-tag
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined.undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
Copy the code

Taking the button-counter component as an example, since the button-counter tag is not a valid HTML tag, you cannot create a VNode directly by new VNode(). Vue uses the resolveAsset function to check whether the label is a custom component label.

export function resolveAsset (
  options: Object, type: string, id: string, warnMissing? : boolean) :any {
  /* istanbul ignore if */
  if (typeofid ! = ='string') {
    return
  }
  const assets = options[type]

  // First check if the vue instance itself has this component
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id)
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  const PascalCaseId = capitalize(camelizedId)
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]

  // If not found on the instance, look for the prototype chain
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  if(process.env.NODE_ENV ! = ='production'&& warnMissing && ! res) { warn('Failed to resolve ' + type.slice(0, -1) + ':' + id,
      options
    )
  }
  return res
}
Copy the code

Button-counter is our globally registered component and can obviously be found in this. Therefore, Vue executes the createComponent function to generate the component’s VNode.

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

  // If Ctor is an option object, use vue.extend to use the option object to create a subclass that converts the component option object to a Vue
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // If Ctor is not yet a constructor or asynchronous component factory function, do not proceed further.
  if (typeofCtor ! = ='function') {
    if(process.env.NODE_ENV ! = ='production') {
      warn(`Invalid Component definition: The ${String(Ctor)}`, context)
    }
    return
  }

  // Asynchronous components
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // Reparse the constructor option object. After the component constructor is created, Vue may use global mixing to cause the constructor option object to change.
  resolveConstructorOptions(Ctor)

  // Handle the component's V-model
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  / / extraction props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // Functional components
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  const listeners = data.on
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // Install component hooks
  installComponentHooks(data)

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

Since Vue allows components to be defined through an option object, Vue needs to convert the component’s option object into a constructor using vue.extend.

/** * Create Vue component subclasses based on the prototype of Vue. Object. Create () is used to implement inheritance. In the internal implementation, a caching mechanism is added to avoid repeated creation of subclasses. * /
  Vue.extend = function (extendOptions: Object) :Function {
    // extendOptions is the option object of the component, as received by vUE
    extendOptions = extendOptions || {}
    // the Super variable holds a reference to the parent class Vue
    const Super = this
    // SuperId saves the CID of the superclass
    const SuperId = Super.cid
    // Cache constructor
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    // Get the name of the component
    const name = extendOptions.name || Super.options.name
    if(process.env.NODE_ENV ! = ='production' && name) {
      validateComponentName(name)
    }

    // Define the component's constructor
    const Sub = function VueComponent (options) {
      this._init(options)
    }

    // The component's prototype object points to Vue's options object
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub

    // Assign a CID to the component
    Sub.cid = cid++

    // Merge the component's options object with the Vue's options
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    // use the super attribute to point to the parent class
    Sub['super'] = Super
    
    // Proxy component instances' props and computed genera to component prototype objects to avoid repeated calls to Object.defineProperty for each instance creation.
    if (Sub.options.props) {
      initProps(Sub)
    }

    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // Copy global methods like extend/mixin/use on parent Vue
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // Copy resource registration methods such as Component, directive and filter from parent Vue
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })

    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // Save the option object of the parent class Vue
    Sub.superOptions = Super.options
    // Save the component's options object
    Sub.extendOptions = extendOptions
    // Save the final option object
    Sub.sealedOptions = extend({}, Sub.options)

    // Cache component constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}
Copy the code

Another important piece of code is installComponentHooks(Data). This method adds component hooks to the component vNode’s data. These hooks are called at different stages of the component, such as when init hooks are called ata component patch.

function installComponentHooks (data: VNodeData) {
 const hooks = data.hook || (data.hook = {})
 for (let i = 0; i < hooksToMerge.length; i++) {
   const key = hooksToMerge[i]
   // Externally defined hooks
   const existing = hooks[key]
   // Built-in component vNode hooks
   const toMerge = componentVNodeHooks[key]
   // Merge hooks
   if(existing ! == toMerge && ! (existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } }// Component vNode hooks.
const componentVNodeHooks = {
 // Instantiate the component
 init (vnode: VNodeWithData, hydrating: boolean): ? boolean {if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {// kept-alive components, treat as a patch
     const mountedNode: any = vnode // work around flow
     componentVNodeHooks.prepatch(mountedNode, mountedNode)
   } else {
     // Generate a component instance
     const child = vnode.componentInstance = createComponentInstanceForVnode(
       vnode,
       activeInstance
     )
     // Mount the component, like vue's $mount
     child.$mount(hydrating ? vnode.elm : undefined, hydrating)
   }
 },

 prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
   const options = vnode.componentOptions
   const child = vnode.componentInstance = oldVnode.componentInstance
   updateChildComponent(
     child,
     options.propsData, // updated props
     options.listeners, // updated listeners
     vnode, // new parent vnode
     options.children // new children
   )
 },

 insert (vnode: MountedComponentVNode) {
   const { context, componentInstance } = vnode
   if(! componentInstance._isMounted) { componentInstance._isMounted =true
     // Triggers the mounted hook of the component
     callHook(componentInstance, 'mounted')}if (vnode.data.keepAlive) {
     if (context._isMounted) {
       queueActivatedComponent(componentInstance)
     } else {
       activateChildComponent(componentInstance, true /* direct */)
     }
   }
 },

 destroy (vnode: MountedComponentVNode) {
   const { componentInstance } = vnode
   if(! componentInstance._isDestroyed) {if(! vnode.data.keepAlive) { componentInstance.$destroy() }else {
       deactivateChildComponent(componentInstance, true /* direct */)}}}}const hooksToMerge = Object.keys(componentVNodeHooks)

Copy the code

Finally, generate the vNode node for the component as normal HTML tags do:

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

The processing of vNode during patch is different from that of common labels.

Vue If the vNode being patched is found to be a component, the createComponent method is called.

  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      // Execute the component hook init hook to create the component instance
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)}After the init hook is executed, if the vNode is a child component, the component should create a vUE subinstance and mount it to the DOM element. The child component vnode.elm is also set up. Then we just need to return that DOM element.
      if (isDef(vnode.componentInstance)) {
        / / set the vnode. Elm
        initComponent(vnode, insertedVnodeQueue)
        // Insert the component's ELM into the parent component's DOM node
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true}}}Copy the code

CreateComponent creates the component instance by calling the init hook method defined on the data object of the component VNode. Now let’s go back to the init hook code:

/ /... Omit other code
  init (vnode: VNodeWithData, hydrating: boolean): ? boolean {if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {// kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      // Generate a component instance
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      // Mount the component, like vue's $mount
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  }
  / /... Omit other code
Copy the code

Because the component is created for the first time, so the init hook will be called createComponentInstanceForVnode create a component instance, and ponentInstance assigned to vnode.com.

export function createComponentInstanceForVnode (vnode: any, parent: any,) :Component {
  // Internal component options
  const options: InternalComponentOptions = {
    // Flag whether it is a component
    _isComponent: true./ / the father Vnode
    _parentVnode: vnode,
    // Parent Vue instance
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  // New a component instance. Component instantiation is the same process as new Vue().
  return new vnode.componentOptions.Ctor(options)
}
Copy the code

CreateComponentInstanceForVnode will perform in the new vnode.com ponentOptions. Ctor (options). Componentoptions is an object {Ctor, propsData, Listeners, tag, children} that contains the component’s constructor, Ctor. So new vnode.com ponentOptions. Ctor (options) is equivalent to the new VueComponent (options).

// Generate a component instance
const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)
// Mount the component, like vue's $mount
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
Copy the code

Is equivalent to:

new VueComponent(options).$mount(hydrating ? vnode.elm : undefined, hydrating)
Copy the code

This code is probably familiar to all of you. It is the process of component initialization and mounting. The initialization and mounting of the component is the same as the Vue initialization and mounting process described in the previous article, so the description is not expanded. The general process is once a component instance is created and mounted. Use initComponent to set the component instance’s $el to the value vNode.elm. Finally, insert is called to insert the component instance’s DOM root node into its parent node. The processing of the components is then complete.

conclusion

By analyzing the underlying implementation of components, we know that each component is an instance of VueComponent, which in turn inherits from Vue. Each component instance maintains its own state, template parsing, DOM creation and update independently. Limited space, this paper only analyzes the registration and resolution process of basic components, not asynchronous components, keep-alive and other analysis. We’ll make it up later.

Pay attention to our