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
Just like it or pay attention to it. It's convenient to find it~