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!