Explain the implementation principle of vue component in detail

  • 2021-09-16 06:16:40
  • OfStack

The design of component mechanism allows developers to divide a complex application into functional independent components, which reduces the difficulty of development and provides excellent reusability and maintainability. In this paper, from the perspective of source code, we understand the underlying implementation principle of components under 1.

What did you do when you registered the component?

To use components in Vue, the first step is to register. Vue provides global registration and local registration.

The global registration method is as follows:


Vue.component('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 locally registered component can only be used in the Vue instance where the component is registered, or even in a subcomponent of the Vue instance.

Small partners who have 1 experience in using Vue know the above differences, but why are there such differences? We explain it from the code implementation of component registration.


// Vue.component Core code of 
// 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 Yes 1 Objects that need to be called Vue.extend() Convert to a function. Vue.extend Will create 1 A Vue And returns the constructor of the subclass (component class). 
     definition = this.options._base.extend(definition)
    }
    
    // ... Omit other codes 
    //  The key here is to add the component to the option object of the constructor Vue.options Go. 
    this.options[type + 's'][id] = definition
    return definition
   }
  }
 })

// Vue Constructor of 
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)
  
}

// Vue Merge option objects in initialization of 
Vue.prototype._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, option objects of merge constructors, and option objects in instances 
   vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
   )
  }
  // ... Omit other codes 
 }

The main code of component registration is extracted above. It can be seen that the option object of Vue instance consists of the constructor option object of Vue and the option object of Vue instance.

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

The option object specified by Vue at instantiation time (new Vue (options) is 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 only affect Vue instances themselves.

Why can component tags be used normally in HTML templates?

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


<div id="app">
 <!-- Working with components button-counter-->
 <button-counter></button-counter>
</div>

//  Global registration 1 A person named  button-counter  Components of 
Vue.component('button-counter', {
 data: function () {
  return {
   count: 0
  }
 },
 template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

//  Create Vue Instances 
new Vue({
  el: '#app'
})

So, what does Vue do when it resolves to a custom component label?

Vue's resolution of component tags is similar to that of ordinary HTML tags, and will not be specially treated because of non-HTML standard tags. The first difference in processing occurs when the vnode node is created. vnode is created internally by 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.$vnode.ns) || config.getTagNamespace(tag)
  //  If it is ordinary HTML Label 
  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))) {
   //  If it is a component label, 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()
 }
}

Taking the button-counter component in this paper as an example, because button-counter tag is not a legal HTML tag, new VNode () cannot directly create vnode. Vue checks through the resolveAsset function whether the label is the label of the custom component.


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

 //  Check first vue Does the instance itself have 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
}

button-counter is our globally registered component, and its definition can obviously be found at this. $options. components. Therefore, the Vue executes the createComponent function to generate the vnode of the component.


// 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 Vue Constructor of 
 const baseCtor = context.$options._base

 //  If Ctor Yes 1 Options object, which requires the use of Vue.extend Using the option object, create a conversion of the component option object to the 1 A Vue Subclass of 
 if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
 }

 //  If Ctor Not yet 1 Constructor or asynchronous component factory function, which is no longer executed. 
 if (typeof Ctor !== 'function') {
  if (process.env.NODE_ENV !== 'production') {
   warn(`Invalid Component definition: ${String(Ctor)}`, context)
  }
  return
 }

 //  Asynchronous component 
 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 || {}

 //  The option object of the constructor is reparsed. After the component constructor is created, Vue Global blending may be used to cause the constructor option object to change. 
 resolveConstructorOptions(Ctor)

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

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

 //  Functional component 
 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
  }
 }

 //  Installing components hooks
 installComponentHooks(data)

 //  Create  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
}
 

Since Vue allows a component to be defined from one option object, Vue requires Vue. extend to convert the option object of the component to one constructor.


/**
  * Vue Class inheritance, with Vue Prototype for prototype creation Vue Component subclass. Inheritance is implemented by using Object.create() In the internal implementation, the cache mechanism is added to avoid creating subclasses repeatedly. 
  */
 Vue.extend = function (extendOptions: Object): Function {
  // extendOptions  Is the option object of the component, which is the same as the vue Received 1 Sample 
  extendOptions = extendOptions || {}
  // Super Variable holds a reference to the parent class Vue Reference to 
  const Super = this
  // SuperId  Save the parent class's cid
  const SuperId = Super.cid
  //  Cache constructor 
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
   return cachedCtors[SuperId]
  }

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

  //  Defining a constructor for a component 
  const Sub = function VueComponent (options) {
   this._init(options)
  }

  //  The prototype object of the component points to the Vue Option object of 
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub

  //  Assign a component 1 A cid
  Sub.cid = cid++

  //  Match the option object of a component with the Vue Combine options for 
  Sub.options = mergeOptions(
   Super.options,
   extendOptions
  )
  //  Pass super Property points to the parent class 
  Sub['super'] = Super
  
  //  Object of the component instance props And computed Proxy to the component prototype object to avoid repeated calls when each instance is created Object.defineProperty . 
  if (Sub.options.props) {
   initProps(Sub)
  }

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

  //  Copy parent class Vue Above extend/mixin/use Isoglobal method 
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  //  Copy parent class Vue Above component , directive , filter Equal resource registration method 
  ASSET_TYPES.forEach(function (type) {
   Sub[type] = Super[type]
  })

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

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

  //  Constructor of 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 when the component patch is called.


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

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

Finally, the vnode node is generated for the component, like the normal HTML tag 1:


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

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

Components treat vnode differently when they are on patch than normal tags.

Vue If it is found that vnode in patch is a component, the createComponent method is called.


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

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

createComponent calls the init hook method defined on the data object of the component vnode to create the component instance. Now let's go back to the code for the init hook:


// ...  Omit other codes 
 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 component instances 
   const child = vnode.componentInstance = createComponentInstanceForVnode(
    vnode,
    activeInstance
   )
   //  Mounts the assembly, which is the same as the vue Adj. $mount1 Sample 
   child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
 }
 // ... Omit other codes 

Since the component is first created, the init hook calls createComponentInstanceForVnode to create a component instance and assigns a value to vnode. componentInstance.


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

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

new vnode. componentOptions. Ctor (options) is executed in createComponentInstanceForVnode. As we saw earlier when we created the component vnode, the value of vnode. componentOptions is an object: {Ctor, propsData, listeners, tag, children}, which contains the component constructor Ctor. Therefore, new vnode. componentOptions. Ctor (options) is equivalent to new VueComponent (options).


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

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

Equivalent to:


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

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

This code must be familiar to everyone, and it is the process of component initialization and mounting. Component initialization and mounting is the same as the Vue initialization and mounting process described earlier, so the description will not be expanded. 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 of vnode. elm. Finally, call insert to insert the DOM root node of the component instance into its parent node. Then, the processing of components is completed.

Summarize

By analyzing the underlying implementation of components, we can know that each component is an instance of VueComponent, and VueComponent inherits from Vue. Each component instance independently maintains its own state, template parsing, DOM creation and update. Limited space, this paper only analyzes the basic component registration parsing process, not asynchronous components, keep-alive and so on. Wait until later to make up slowly.

The above is the detailed explanation of vue component implementation principle details, more information about vue component please pay attention to other related articles on this site!


Related articles: