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.