Analysis of Vue principle: thoroughly understand the generation process from virtual Dom to real Dom

Previous: Analysis of Vue principle (4): do you know how to generate virtual Dom?

After another tree structured JavaScript object, what we need to do now is to form a mapping relationship between this tree and the real Dom tree. First, we briefly review the previous mountComponent methods:

export function mountComponent(vm, el) {
  vm.$el = el
  ...
  callHook(vm, 'beforeMount')
  ...
  const updateComponent = function () {
    vm._update(vm._render())
  }
  ...
}

Now that we have executed the VM. Render method and got the VNode, we will pass it as a parameter to the VM. Update method and execute it. The function of VM. Update is to turn VNode into a real Dom, but it has two execution opportunities:

First render

  • When new Vue is executed, it is the first time to render, and the incoming VNode object will be mapped to the real Dom.

Update page

  • Data change will drive page change, which is one of the most unique features of vue. Before and after data change, two vnodes will be generated for comparison. However, how to make the smallest change on the old VNode to render the page is quite complex. If you don't know what the data response is, it's not very good to talk about diff directly to understand the overall process of vue. So after analyzing the first rendering in this chapter, the next chapter is the data response, and then the diff comparison, so we can understand it.

Let's take a look at the definition of VM. Update method:

Vue.prototype._update = function(vnode) {
  ... First render 
  vm.$el = vm.__patch__(vm.$el, vnode)  // Overwrite the original vm.$el
  ...
}

Here vm.$el is a real Dom element that was previously mounted in the mountComponent method. The first rendering will pass in vm.$el and the resulting VNode, so take a look at the definition of VM. \

Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules }) 

__patch is a method returned inside the createPatchFunction method, which accepts an object:

nodeOps attribute: a collection of methods that encapsulate the operation of native Dom, such as creating, inserting, removing, and explaining where to use.

modules attribute: to create a real Dom, you need to generate its attributes such as class/attrs/style. modules is an array collection. Each item of the array is a hook method corresponding to these attributes. There are corresponding hook methods for the creation, update and destruction of these attributes. When something needs to be done at a certain time, just execute the corresponding hook. For example, they all have the create hook method, such as collecting these create hooks into an array. When you need to create these properties on the real Dom, you need to execute each item of the array in turn, that is, you need to create them in turn.

Ps: here, the hook method in the modules attribute is platform specific. The way that web, weex and SSR call v node method is not the same. Therefore, vue uses the function corrilization operation here. In the create patch function, the platform difference is smoothed, so that the \\\.

Generate Dom

Here you can remember that no matter what type of node VNode is, only three types of nodes will be created and inserted into the Dom: element node, annotation node, and text node.

Let's take a look at the createPatchFunction and what kind of method it returns:

export function createPatchFunction(backend) {
  ...
  const { modules, nodeOps } = backend  // Deconstruct the incoming set
  
  return function (oldVnode, vnode) {  // Receive new and old vnode s
    ...
    
    const isRealElement = isDef(oldVnode.nodeType) // Is it real Dom
    if(isRealElement) {  // $el is real Dom
      oldVnode = emptyNodeAt(oldVnode)  // Change to VNode format to overwrite yourself
    }
    ...
  }
}

When rendering for the first time, there is no oldVnode. oldVnode is $el, a real dom, which is wrapped by the emptyNodeAt(oldVnode) method:

function emptyNodeAt(elm) {
  return new VNode(
    nodeOps.tagName(elm).toLowerCase(), // Corresponding tag attribute
    {},  // Corresponding data
    [],   // Corresponding to children
    undefined,  //Corresponding to text
    elm  // The real dom is assigned to the elm attribute
  )
}

//Packed:
{
  tag: 'div',
  elm: '<div id="app"></div>' // Real dom
}

-------------------------------------------------------

nodeOps: 
export function tagName (node) {  // Returns the label name of the node
  return node.tagName  
}

After the passed in $el attribute is converted to VNode format, we continue:

export function createPatchFunction(backend) { 
  ...
  
  return function (oldVnode, vnode) {  // Receive new and old vnode s
  
    const insertedVnodeQueue = []
    ...
    const oldElm = oldVnode.elm  //Real DOM after packaging
    const parentElm = nodeOps.parentNode(oldElm)  // The first parent node is < body ></body>
  	
    createElm(  // Create real Dom
      vnode, // Second parameter
      insertedVnodeQueue,  // Empty array
      parentElm,  // <body></body>
      nodeOps.nextSibling(oldElm)  // Next node
    )
    
    return vnode.elm // Return real Dom override vm.$el
  }
}
                                              
------------------------------------------------------

nodeOps: 
export function parentNode (node) {  // Get parent node
  return node.parentNode 
}

export function nextSibling(node) {  // Get next node
  return node.nextSibing  
}

The createElm method starts to generate the real Dom. There are two ways for VNode to generate the real Dom: element node and component. Therefore, we use the VNode generated in the previous chapter to explain.

1. Generating Dom from element node

{  // Element node VNode
  tag: 'div',
  children: [{
      tag: 'h1',
      children: [
        {text: 'title h1'}
      ]
    }, {
      tag: 'h2',
      children: [
        {text: 'title h2'}
      ]
    }, {
      tag: 'h3',
      children: [
        {text: 'title h3'}
      ]
    }
  ]
}

You can have an impression by looking at this flow chart first, and then you can see the specific implementation. I believe that the ideas will be much clearer:

To start creating Dom, let's look at its definition:

function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) { 
  ...
  const children = vnode.children  // [VNode, VNode, VNode]
  const tag = vnode.tag  // div
  
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return  // If it is the result of a component that returns true, it will not continue. Later, explain createComponent in detail
  }
  
  if(isDef(tag)) {  // Element node
    vnode.elm = nodeOps.createElement(tag)  // Create parent node
    createChildren(vnode, children, insertedVnodeQueue)  // Create child nodes
    insert(parentElm, vnode.elm, refElm)  // insert
    
  } else if(isTrue(vnode.isComment)) {  // Comment Nodes 
    vnode.elm = nodeOps.createComment(vnode.text)  // Create annotation node
    insert(parentElm, vnode.elm, refElm); // Insert into parent node
    
  } else {  // Text node
    vnode.elm = nodeOps.createTextNode(vnode.text)  // Create text node
    insert(parentElm, vnode.elm, refElm)  // Insert into parent node
  }
  
  ...
}

------------------------------------------------------------------

nodeOps: 
export function createElement(tagName) {  // Create node
  return document.createElement(tagName)
}

export function createComment(text) {  //Create annotation node
  return document.createComment(text)
}

export function createTextNode(text) {  // Create text node
  return document.createTextNode(text)
}

function insert (parent, elm, ref) {  //Insert dom operation
  if (isDef(parent)) {  // With parent node
    if (isDef(ref)) { // With reference node
      if (ref.parentNode === parent) {  // The parent of the reference node is equal to the passed in parent
        nodeOps.insertBefore(parent, elm, ref)  // Insert elm before the reference node in the parent node
      }
    } else {
      nodeOps.appendChild(parent, elm)  //  Add elm to parent
    }
  }  // Do nothing without a parent node
}
//This is an important method, because it will be used in many places.

Determine whether it is an element node, annotation node and text node in turn, create them respectively and insert them into the parent node. Here, we mainly introduce the creation of element nodes, and the other two do not have complex logic. Let's look at the createChild method definition:

function createChild(vnode, children, insertedVnodeQueue) {
  if(Array.isArray(children)) {  // Is an array
    for(let i = 0; i < children.length; ++i) {  // Traverse every entry of vnode
      createElm(  // Recursive call
        children[i], 
        insertedVnodeQueue, 
        vnode.elm, 
        null, 
        true, // Not root insertion
        children, 
        i
      )
    }
  } else if(isPrimitive(vnode.text)) {  //typeof is one of string/number/symbol/boolean
    nodeOps.appendChild(  // Create and insert into parent node
      vnode.elm, 
      nodeOps.createTextNode(String(vnode.text))
    )
  }
}

-------------------------------------------------------------------------------

nodeOps:
export default appendChild(node, child) {  // Add child node
  node.appendChild(child)
}

Start to create child nodes, traverse each item of VNode, and each item still uses the previous createElm method to create Dom. If an item is an array again, continue to call createChild to create a child node of an item; if an item is not an array, create a text node and add it to the parent node. Use recursion like this to create all the nested vnodes as real Dom.

Looking at the flow chart again, I believe that your doubts have been reduced a lot:

In short, it is to create a real Dom one by one from the inside out, then insert it into its parent node, and finally insert the created Dom into the body to complete the creation process. The creation of element nodes is relatively simple. Let's see how components are created next.

2. Generating Dom from component VNode

{  // Component VNode
  tag: 'vue-component-1-app',
  context: {...},
  componentOptions: {
    Ctor: function(){...},  // Subcomponent constructor
    propsData: undefined,
    children: undefined,
    tag: undefined,
    children: undefined
  },
  data: {
    on: undefined,  // Native event
    hook: {  // Component hook
      init: function(){...},
      insert: function(){...},
      prepatch: function(){...},
      destroy: function(){...}
    }
  }
}

-------------------------------------------

<template>  // Template within app component
  <div>app text</div>
</template>

First of all, it's better to look at a simple flow chart and leave an impression, which is convenient to sort out the logical order after that:

We use the VNode generated by components in the previous chapter to see how to create the component Dom branch logic in createElm:

function createElm(vnode, insertedVnodeQueue, parentElm, refElm) { 
  ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { // Component branch
    return  
  }
  ...

Execute the createComponent method. If it is an element node, it will not return anything, so it is undefined. It will continue to follow the logic of creating an element node. Now it's a component. Let's see the implementation of createComponent:

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if(isDef(i)) {
    if(isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode)  // Execute init method
    }
    
    ...
  }
}

First, the vnode.data of the component will be assigned to I. whether there is this attribute can determine whether it is a component vnode. The subsequent if (isdef (I = i.hook) & & isdef (I = i.init)) integrates judgment and assignment. i(vnode) in if is the executed component init(vnode) method. At this time, let's see what the init hook method of the component does:

import activeInstance  // global variable

const init = vnode => {
  const child = vnode.componentInstance = 
    createComponentInstanceForVnode(vnode, activeInstance)
  ...
}

activeInstance is a global variable, which is assigned as the current instance in the update method, and then passed in as the parent instance of the child component during the process of "patch" of the current instance. The component relationship is built during the initLifecycle of the child component. Assign the result of createComponentInstanceForVnode execution to vnode.componentInstance, so see what it returns:

export  createComponentInstanceForVnode(vnode, parent) {  // parent is the global variable activeInstance
  const options = {  // options for components
    _isComponent: true,  // Set a flag bit to indicate a component
    _parentVnode: vnode, 
    parent  // The parent vm instance of the child component, enabling initialization of initLifecycle to establish parent-child relationship
  }
  
  return new vnode.componentOptions.Ctor(options)  // The constructor of the subcomponent is defined as Ctor
}

In the init method of the component, the createComponentInstanceForVnode method is executed first. In this method, the constructor of the subcomponent will be instantiated. Because the constructor of the subcomponent inherits all the capabilities of the base class Vue, this time is equivalent to executing new Vue({...}). Next, the init method will be executed for a series of initialization logic of the subcomponent. Let's go back to "ini" T method, because there are still some differences between them:

Vue.prototype._init = function(options) {
  if(options && options._isComponent) {  // Merge options of components, and "isComponent" is the previously defined tag bit
    initInternalComponent(this, options)  // The difference is that the merging of components is much simpler
  }
  
  initLifecycle(vm)  // Parenting
  ...
  callHook(vm, 'created')
  
  if (vm.$options.el) { // Component has no el attribute, so how can I stop here
    vm.$mount(vm.$options.el)
  }
}

----------------------------------------------------------------------------------------

function initInternalComponent(vm, options) {  // Merge subcomponent options
  const opts = vm.$options = Object.create(vm.constructor.options)
  opts.parent = options.parent  // Component init assignment, global variable activeInstance
  opts._parentVnode = options._parentVnode  // Component init assignment, component vnode 
  ...
}

All of the above are well executed. In the end, because there is no el attribute, there is no mount. The createComponentInstanceForVnode method has been executed. At this time, we return to the init method of the component to complete the rest of the logic:

const init = vnode => {
  const child = vnode.componentInstance = // Get an instance of the component
    createComponentInstanceForVnode(vnode, activeInstance)
    
  child.$mount(undefined)  // Then mount it manually
}

We mount this component manually in init method, and then execute the ﹣ render() method of the component to get the element node VNode in the component, and then execute VM. ﹣ update(), and execute the ﹣ patch ﹣ method of the component, because the passed in of $mount method is undefined, and the oldVnode is undefined, and the logic in ﹣ patch ﹣

return function patch(oldVnode, vnode) {
  ...
  if (isUndef(oldVnode)) {
    createElm(vnode, insertedVnodeQueue)
  }
  ...
}

When executing createElm this time, the third parameter parent node is not passed in. Where does the Dom created by the component take effect? If you do not have a parent node, you need to generate a Dom. At this time, you need to execute the "patch" of the component. Therefore, the parameter vnode is the vnode of the element node in the component

<template> // Template within app component
  <div>app text</div>
</template>

-------------------------

{  // app internal element vnode
  tag: 'div',
  children: [
    {text: app text}
  ],
  parent: {  // Execute the relationship established by initLifecycle when subcomponent is init
    tag: 'vue-component-1-app',
    componentOptions: {...}
  }
}

Obviously, it's not a component at this time, even if it's a component, it doesn't matter. The big problem is to execute the logic of createComponent to create a component again, because there will always be components composed of element nodes. At this time, we will execute the logic of creating element nodes once, because there is no third parameter parent node, so although the Dom of the component is created, it will not be inserted here. Please note that the init of the component has been completed at this time, but the createComponent method of the component has not been completed. We complete its logic:

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data;
  if (isDef(i)) {
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode)  // init completed
    }
    
    if (isDef(vnode.componentInstance)) {  // Assigned when executing component init
    
      initComponent(vnode)  // Assign real dom to vnode.elm
      
      insert(parentElm, vnode.elm, refElm)  // Component Dom is inserted here
      ...
      return true  // So it will return directly
    }
  }
}

-----------------------------------------------------------------------

function initComponent(vnode) {
  ...
  vnode.elm = vnode.componentInstance.$el  // __Real dom returned by patch
  ...
}

No matter how deep a component is nested, execute init when encountering a component, and then execute init when encountering a nested component during init's "patch" process. When the nested component finishes "patch", insert the real Dom into its parent node, then insert the "patch" of the outer component into its parent node, and finally insert it into the body to complete the nesting of the component In a word, the creation process of is still a process from inside to outside.

Looking back at this picture, I believe it will be easy to understand~ We will complete the logic after the initial mountComponent in this chapter:

export function mountComponent(vm, el) {
  ...
  const updateComponent = () => {
    vm._update(vm._render())
  }
  
  new Watcher(vm, updateComponent, noop, {
    before() {
      if(vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }   
  }, true)
  
  ...
  callHook(vm, 'mounted')
  
  return vm
}

Next, we will pass updateComponent into a Watcher class. What is this class for? We will explain in the next chapter. Next, we will execute the mounted hook method. So far, the whole process of new Vue has been completed. Let's review the execution sequence starting with new Vue:

new Vue ==> vm._init() ==> vm.$mount(el) ==> vm._render()  ==> vm.update(vnode) 

In the end, let's finish this chapter with an interview question that vue may be asked~

The interviewer asked with a smile and politeness:

  • The parent-child components define four hooks at the same time: beforeCreate, created, beforemount, and mounted. What is the order of their execution?

Connect back:

  • If you read the previous chapters, I believe that the problem is clear. First, the initialization process of the parent component will be executed, so the beforeCreate and created will be executed in turn, and the beforeMount hook will be executed before the mount. However, in the process of generating the real DOM's "patch menu", the embedded subcomponent will be converted to the initialization hook of the subcomponent beforeCreate and created after encountering the nested subcomponent. The subcomponent will execute the beforeMount before the mount, and then complete the subcomponent's Mount after DOM creation. The parent component's "patch" process is completed, and finally the parent component's mounted hook is executed, which is their execution order. The execution sequence is as follows:
parent beforeCreate
parent created
parent beforeMounte
    child beforeCreate
    child created
    child beforeMounte
    child mounted
parent mounted

Next: Analysis of Vue principle (VI): comprehensive and in-depth understanding of responsive principle (I) - object chapter

Just like it or pay attention to it. It's convenient to find it~

Reference resources:

Full and in-depth analysis of Vue.js source code

Vue.js in depth

Vue.js component elaboration

Analysis of the internal operation mechanism of Vue.js

Keywords: Front-end Vue Attribute Javascript REST

Added by EXiT on Mon, 23 Dec 2019 12:23:54 +0200