SoFunction
Updated on 2025-04-11

Detailed explanation of the implementation principle of vue component

The design of component mechanism allows developers to divide a complex application into functionally independent components, reducing the difficulty of development, while also providing excellent reusability and maintainability. In this article, let’s learn about the underlying implementation principles of components from the perspective of source code.

What did you do when component registration?

When using components in Vue, the first step is to register. Vue provides two ways to register globally and locally.

The global registration method is as follows:

('my-component-name', { /* ... */ })

The local registration method is as follows:

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

new Vue({
 el: '#app',
 components: {
  'component-a': ComponentA
 }
})

Globally registered components will be used in any Vue instance. A partially registered component can only be used in the registration place of the component, that is, in the Vue instance where the component is registered, and it cannot even be used in the child components of the Vue instance.

Friends who have some experience in using Vue understand the above differences, but why are there such differences? We explain it from the code implementation of component registration.

// Core code// ASSET_TYPES = ['component', 'directive', 'filter']
ASSET_TYPES.forEach(type => {
  Vue[type] = function (id, definition
  ){
   if (!definition) {
    return [type + 's'][id]
   } else {
    // Component registration    if (type === 'component' && isPlainObject(definition)) {
      =  || id
     // If definition is an object, you need to call () to convert it into a function.  A subclass of Vue (component class) will be created and the constructor of the subclass will be returned.     definition = ._base.extend(definition)
    }
    
    // ...Omit other codes    // It's very important here, add the component to the constructor's option object.    [type + 's'][id] = definition
    return definition
   }
  }
 })
// Vue's constructorfunction Vue(options){
 if (.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._init = function (options) {
  const vm = this
  vm._uid = uid++
  vm._isVue = true
  // ...Omit other codes  if (options && options._isComponent) {
   initInternalComponent(vm, options)
  } else {
   // Merge vue option objects, merge constructor option objects and option objects in instances   vm.$options = mergeOptions(
    resolveConstructorOptions(),
    options || {},
    vm
   )
  }
  // ...Omit other codes }

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

The globally registered component is actually added to the option object of the Vue constructor.

The option object specified by Vue when instantiated (new Vue(options)) will be merged with the option object of the constructor as the final option object of the Vue instance. Therefore, globally registered components can be used in all Vue instances, while locally registered components in Vue instances will only affect the Vue instance itself.

Why can component tags be used normally in HTML templates?

We know that components can be used directly in templates like ordinary HTML. For example:

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

// Create a Vue instancenew Vue({
  el: '#app'
})

So, how is it handled when Vue parses to custom component tags?

Vue's parsing component tags is the same as that of ordinary HTML tags, and will not be specially processed because of non-HTML standard tags. The first different place during the processing occurs when the vnode node is created. vue internally realizes the creation of vnode through 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 codes 
 let vnode, ns
 if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$) || (tag)
  // If it is a normal HTML tag  if ((tag)) {
   vnode = new VNode(
    (tag), data, children,
    undefined, undefined, context
   )
  } else if ((!data || !) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
   // If it is a component tag, . 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 ((vnode)) {
  return vnode
 } else if (isDef(vnode)) {
  if (isDef(ns)) applyNS(vnode, ns)
  if (isDef(data)) registerDeepBindings(data)
  return vnode
 } else {
  return createEmptyVNode()
 }
}

Taking the button-counter component in the article as an example, since the button-counter tag is not a legal HTML tag, you cannot directly create vnode by new VNode(). Vue will check whether the tag is a tag of a custom component through the resolveAsset function.

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

 // First check whether the vue instance itself has the 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 the instance is not found, go to find the prototype chain const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
 if (.NODE_ENV !== 'production' && warnMissing && !res) {
  warn(
   'Failed to resolve ' + (0, -1) + ': ' + id,
   options
  )
 }
 return res
}

button-counter is a component we register globally, and it is obviously a definition that can be found in this.$. Therefore, Vue will execute 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 constructor of Vue const baseCtor = context.$options._base

 // If Ctor is an option object, you need to use the option object to create a subclass that converts the component option object into a Vue if (isObject(Ctor)) {
  Ctor = (Ctor)
 }

 // If Ctor is not a constructor or asynchronous component factory function, it will not be executed further. if (typeof Ctor !== 'function') {
  if (.NODE_ENV !== 'production') {
   warn(`Invalid Component definition: ${String(Ctor)}`, context)
  }
  return
 }

 // Asynchronous components let asyncFactory
 if (isUndef()) {
  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 || {}

 // Re-parse the constructor's option object. After the component constructor is created, Vue may use global inclusion to cause the constructor option object to change. resolveConstructorOptions(Ctor)

 // Handle component v-model if (isDef()) {
  transformModel(, data)
 }

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

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

 const listeners = 
  = 

 if (isTrue()) {
  const slot = 
  data = {}
  if (slot) {
    = slot
  }
 }

 // Install component hooks installComponentHooks(data)

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

 return vnode
}
 

Since Vue allows the definition of components through an option object, Vue needs to use the option object of the component to convert the component into a constructor.

/**
   * Vue class inherits, and creates Vue component subclasses based on Vue's prototype.  The inheritance implementation method is to use (). In the internal implementation, a caching mechanism is added to avoid repeated creation of subclasses.
   */
  = function (extendOptions: Object): Function {
  // extendOptions is the component's option object, the same as what vue receives  extendOptions = extendOptions || {}
  // Super variable saves reference to parent class Vue  const Super = this
  // SuperId saves the cid of the parent class  const SuperId = 
  // Cache constructor  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
   return cachedCtors[SuperId]
  }

  // Get the component's name  const name =  || 
  if (.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 the option object of Vue   = ()
   = Sub

  // Assign a cid to the component   = cid++

  // Merge the component's option object with Vue's option   = mergeOptions(
   ,
   extendOptions
  )
  // Point to the parent class through the super attribute  Sub['super'] = Super
  
  // Proxy the props and computed belongings of the component instance to the component prototype object to avoid repeated calls when each instance is created.  if () {
   initProps(Sub)
  }

  if () {
   initComputed(Sub)
  }

  // Copy global methods such as extend/mixin/use on the parent class Vue   = 
   = 
   = 

  // Copy the resource registration methods such as component, directive, filter on the parent class Vue  ASSET_TYPES.forEach(function (type) {
   Sub[type] = Super[type]
  })

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

  // Save the option object of the parent class Vue   = 
  // Save component's option object   = extendOptions
  // Save the final option object   = extend({}, )

  // The constructor of the cache component  cachedCtors[SuperId] = Sub
  return Sub
 }
}

Another important code is installComponentHooks(data). This method adds component hooks to the data of the component vnode, which are called at different stages of the component, such as the init hook will be called when the component is patched.

function installComponentHooks (data: VNodeData) {
 const hooks =  || ( = {})
 for (let i = 0; i < ; i++) {
  const key = hooksToMerge[i]
  // Externally defined hook  const existing = hooks[key]
  // Built-in component vnode hook  const toMerge = componentVNodeHooks[key]
  // Merge hooks  if (existing !== toMerge && !(existing && existing._merged)) {
   hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
  }
 }
}

// Hook for component vnode.const componentVNodeHooks = {
 // Instantiate the component init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  if (
    &&
   !._isDestroyed &&
   
  ) {
   // kept-alive components, treat as a patch
   const mountedNode: any = vnode // work around flow
   (mountedNode, mountedNode)
  } else {
   // Generate component instance   const child =  = createComponentInstanceForVnode(
    vnode,
    activeInstance
   )
   // Mount the component, the same as vue's $mount   child.$mount(hydrating ?  : undefined, hydrating)
  }
 },

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

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

 destroy (vnode: MountedComponentVNode) {
  const { componentInstance } = vnode
  if (!componentInstance._isDestroyed) {
   if (!) {
    componentInstance.$destroy()
   } else {
    deactivateChildComponent(componentInstance, true /* direct */)
   }
  }
 }
}

const hooksToMerge = (componentVNodeHooks)

Finally, like normal HTML tags, a vnode node is generated for the component:

// Create vnode const vnode = new VNode(
  `vue-component-${}${name ? `-${name}` : ''}`,
  data, undefined, undefined, undefined, context,
  { Ctor, propsData, listeners, tag, children },
  asyncFactory
 )

Components handle vnodes differently when patching than ordinary tags.

Vue If you find that the vnode being patched is a component, then call the createComponent method.

 function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = 
  if (isDef(i)) {
   const isReactivated = isDef() && 
   // Execute the init hook in the component hook and create a component instance   if (isDef(i = ) && isDef(i = )) {
    i(vnode, false /* hydrating */)
   }
   
   // After the init hook is executed, if vnode is a child component, the component should create a vue child instance and mount it on the DOM element.  The subcomponents are also set up.  Then we just need to return that DOM element.   if (isDef()) {
    // set up    initComponent(vnode, insertedVnodeQueue)
    // Insert the elm of the component into the dom node of the parent component    insert(parentElm, , refElm)
    if (isTrue(isReactivated)) {
     reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
    }
    return true
   }
  }
 }

createComponent will call the init hook method defined on the data object of the component vnode to create a component instance. Now let's look back at the code of the init hook:

// ...Other codes are omitted init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  if (
    &&
   !._isDestroyed &&
   
  ) {
   // kept-alive components, treat as a patch
   const mountedNode: any = vnode // work around flow
   (mountedNode, mountedNode)
  } else {
   // Generate component instance   const child =  = createComponentInstanceForVnode(
    vnode,
    activeInstance
   )
   // Mount the component, the same as vue's $mount   child.$mount(hydrating ?  : undefined, hydrating)
  }
 }
 // ...Omit other codes

Since the component is created for the first time, the init hook will call createComponentInstanceForVnode to create a component instance and assign it to.

export function createComponentInstanceForVnode (
 vnode: any, 
 parent: any,
): Component {
 // Internal component options const options: InternalComponentOptions = {
  // Whether the tag is a component  _isComponent: true,
  // Parent Vnode  _parentVnode: vnode,
  // Parent Vue instance  parent
 }
 // check inline-template render functions
 const inlineTemplate = 
 if (isDef(inlineTemplate)) {
   = 
   = 
 }
 // new A component instance.  Component instantiation is the same as new Vue() execution. return new (options)
}

new (options) is executed in createComponentInstanceForVnode. From the previous example, we can see that when we created the component vnode, the value is an object: { Ctor, propsData, listeners, tag, children }, which contains the component's constructor Ctor. Therefore new (options) is equivalent to new VueComponent(options).

// Generate component instanceconst child =  = createComponentInstanceForVnode(vnode, activeInstance)
// Mount the component, the same as vue's $mountchild.$mount(hydrating ?  : undefined, hydrating)

Equivalent to:

new VueComponent(options).$mount(hydrating ?  : undefined, hydrating)

I believe everyone is familiar with this code, it is the process of component initialization and mounting. The initialization and mount of components are the same as the Vue initialization and mount process described in the previous article, so I will not expand the description. The general process is to create a component instance and mount it. Use initComponent to set the $el of the component instance to the value. Finally, insert is called to insert the DOM root node of the component instance into its parent node. Then the component processing is completed.

Summarize

Through analysis of the underlying implementation of components, we can know that each component is a VueComponent instance, and VueComponent inherits from Vue. Each component instance independently maintains its own state, template parsing, DOM creation and update. The article is limited in space. Only the basic component registration and analysis process is analyzed, and no analysis is done for asynchronous components, keep-alive, etc. Wait until later to make up for it.

The above is a detailed explanation of the implementation principle of vue components. For more information about vue components, please pay attention to my other related articles!