Componentalization is a core idea of Vue, React and other frameworks. By splitting pages into high-cohesion and low-coupling components, we can greatly improve code reuse and make projects easier to maintain. Therefore, this article will analyze the component rendering process. Let’s analyze it through the following example:
<div id="demo">
<comp></comp>
</div>
<script>
Vue.component('comp', {
template: '<div>I am comp</div>',})const app = new Vue({
el: '#demo',})</script>
Copy the code
Here we break down the analysis into two steps: component declaration, component creation, and rendering
Component declarations
First, let’s take a look at what Vue.component is. Its declaration is in core/global-api/assets.js:
export function initAssetRegisters(Vue: GlobalAPI) {
// ASSET_TYPES are arrays: [' Component ','directive','filter']
ASSET_TYPES.forEach((type) = > {
Vue[type] = function (id: string, definition: Function | Object) :Function | Object | void {
if(! definition) {return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && type === 'component') {
validateComponentName(id)
}
// Component declaration code
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
/ / _base Vue
// vue.extend ({}) returns the component constructor
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = {bind: definition, update: definition}
}
// Register with the components option
// Add the component configuration to the original Vue option, which will be inherited by other components that have these component registrations
this.options[type + 's'][id] = definition
return definition
}
}
})
}
Copy the code
This.options._base. Extend (definition) calls Vue. Extend (definition) :
Vue.extend = function (extendOptions: Object) :Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if(process.env.NODE_ENV ! = ='production' && name) {
validateComponentName(name)
}
const Sub = function VueComponent(options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(Super.options, extendOptions)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
Copy the code
It returns a constructor named VueComponent that inherits from Vue. So, after the component definition is complete, the Vue looks like this:
{... options: {components: {
comp: function VueComponent() {}}}.. }Copy the code
Component creation and mounting
We know that the template in Vue will eventually compile into the render function, as in the above example, the final render function will look like this:
render() {
with (this) {return _c('div', {attrs: {"id":"demo"}},[_c('comp')].1)}}Copy the code
The _c definition here can be found in core/instance/render.js:
vm._c = (a, b, c, d) = > createElement(vm, a, b, c, d, false)
Copy the code
So _c(‘comp’) finally calls createElement (core/vdom/create-element.js) :
export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {... return _createElement(context, tag, data, children, normalizationType) }export function _createElement (context: Component, tag? : string | Class
| Function | Object, data? : VNodeData, children? : any, normalizationType? : number
) :VNode | Array<VNode> {... }else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
// Custom components
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined.undefined, context
)
}
...
}
Copy the code
CreateComponent (core/vdom/create-component.js)
export function createComponent (Ctor: Class
| Function | Object | void, data: ? VNodeData, context: Component, children: ? Array
, tag? : string
) :VNode | Array<VNode> | void {...// install component management hooks onto the placeholder node
// Install component management hooks: component initialization (instance creation, mount) will be done in the future
installComponentHooks(data)
// return a placeholder 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
Here we skip the rest of the code and look at installComponentHooks:
function installComponentHooks(data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if(existing ! == toMerge && ! (existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } }Copy the code
Hooks are mounted on data.hook and merged if the user also passes the same hooks. Which are hooks:
const componentVNodeHooks = {
// instantiate and mount
init(vnode: VNodeWithData, hydrating: boolean): ? boolean {if (
vnode.componentInstance && // The instance already exists! vnode.componentInstance._isDestroyed &&// Not destroyed
vnode.data.keepAlive // Is marked as keepAlive
) {
// kept-alive components, treat as a patch
// For cache components, just patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// Create a component instance
const child = (vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
))
// The child component is mounted
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
callHook(componentInstance, 'mounted')}if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
destroy(vnode: MountedComponentVNode) {
const {componentInstance} = vnode
if(! componentInstance._isDestroyed) {// The cache component is not destroyed directly
if(! vnode.data.keepAlive) { componentInstance.$destroy() }else {
deactivateChildComponent(componentInstance, true /* direct */)}}},}Copy the code
There are four hooks here, read their names and they will be executed in the appropriate action. For example, init is executed when the component is initialized, but we’ll talk about that later. Moving on to createComponent:
// return a placeholder 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
export default class VNode {... constructor( tag? : string, data? : VNodeData, children? :?Array<VNode>, text? : string, elm? : Node, context? : Component, componentOptions? : VNodeComponentOptions, asyncFactory? :Function) {... this.componentOptions = componentOptions }// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child(): Component | void {
return this.componentInstance
}
}
Copy the code
Here a VNode is initialized and returned, and _c(‘comp’) is done. You can see that the constructor for our custom component is not executed at this step, just mounted on the componentOptions property. When will he do it? Don’t worry. Let’s keep going down.
When the render of the root component is finished, vm._update is executed to update the component, and __patch__ is called, which brings us to core/vdom/patch.js:
return function patch(oldVnode, vnode, hydrating, removeOnly) {...// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
...
return vnode.elm
}
Copy the code
Then it goes to createElm:
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {... }else {
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
// Initializes events, attributes, and so on
invokeCreateHooks(vnode, insertedVnodeQueue);
}
// Insert the nodeinsert(parentElm, vnode.elm, refElm); }...Copy the code
Note that the vnode is the
element, so it goes to createChildren:
function createChildren(vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if(process.env.NODE_ENV ! = ='production') {
checkDuplicateKeys(children)
}
for (let i = 0; i < children.length; ++i) {
createElm(
children[i],
insertedVnodeQueue,
vnode.elm,
null.true,
children,
i
)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
Copy the code
Here we end up back at createElm, but the VNode is now a custom component and will go here:
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {...// Custom component creation
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
Copy the code
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// Cache
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// The hook installed earlier is used here, init is performed, custom component instantiation
if (isDef((i = i.hook)) && isDef((i = i.init))) {
i(vnode, false /* hydrating */)}// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
// If the previous creation process is complete, the component instance already exists
if (isDef(vnode.componentInstance)) {
// Initialize the component: events, properties, etc
initComponent(vnode, insertedVnodeQueue)
/ / insert the dom
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true}}}Copy the code
Notice that the i.init method is executed, which, as mentioned above, instantiates the component object and then $mount. Executing $mount eventually leads to the patch method and createElm:
function patch(oldVnode, vnode, hydrating, removeOnly) {... if (isUndef(oldVnode)) {// empty mount (likely as component), create new root element
isInitialPatch = true; createElm(vnode, insertedVnodeQueue); }... }Copy the code
This method will recursively render the vNodes in the custom component into the actual DOM, and finally insert the entire DOM tree into the parent element using the insert method. This is the end of the custom component rendering process.