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!