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.