In the first article, the basic implementation of virtual DOM is explained. A brief review shows that virtual DOM is a virtual Node tree described by json data, and its real DOM nodes are generated by render function. And add it to its corresponding element container. While creating a real DOM node, register events for it and add some subsidiary attributes.
As mentioned in the previous article, when the state changes, we compare the newly rendered JavaScript object with the JavaScript object of the old virtual DOM, recording the difference between the two trees, reflecting the difference to the real DOM structure, and only operate the change of the difference part when we finally operate the real DOM. However, the last article only mentioned a simple sentence, but did not carry out a substantive implementation, this article mainly describes how the virtual DOM is updated. Let's get started. O()
DIFF algorithm is used to update the virtual DOM. I think most of the small partners should have heard of this word. DIFF is the core part of the whole virtual DOM, because it is impossible to replace the whole DOM tree when the state of the virtual DOM node changes. If so, two DOM nodes will appear. Operations are nothing more than a great impact on performance. If so, it is better to operate DOM directly.
In the first article, render is used to render the virtual DOM node tree, but only one thing is done in render function. It is just the initialization of the virtual DOM. In fact, in retrospect, whether new operation or modification operation, it should be done through render function in react. All DOM rendering is done through render function, so this conclusion is drawn.
// Rendering Virtual DOM // Virtual DOM Node Tree // Containers that carry DOM nodes, parent elements function render(vnode,container) { // First render mount(vnode,container); };
Since both update and create operations are done through render functions, how should we distinguish between new or updated current operations in the method? After all, in react we don't give a clear identification to tell its method which operation is currently being performed. There are two parameters in executing render function, one is the incoming vnode tree, and the other is the container carrying real DOM nodes. In fact, we can mount its virtual DOM node tree in its container. If the node tree exists in the container, it is an update operation, and vice versa, it is a new operation.
// Rendering Virtual DOM // Virtual DOM Node Tree // Containers that carry DOM nodes, parent elements function render(vnode, container) { if (!container.vnode) { // First render mount(vnode, container); } else { // Old virtual DOM nodes // New DOM Node // Containers Bearing DOM Nodes patch(container.vnode, vnode, container); } container.vnode = vnode; };
Now that the operation of render function has been determined, the next step should be taken. If you want to update, you must know the following parameters: what is the original virtual DOM node and what is the new virtual DOM. The original virtual DOM node has been saved in the parent container and can be used directly.
// Update function // Old virtual DOM nodes // New DOM Node // Containers Bearing DOM Nodes function patch(oldVNode, newVNode, container) { // VNode type of new node let newVNodeFlag = newVNode.flag; // VNode Type of Old Node let oldVNodeFlag = oldVNode.flag; // If the new node is not the same type as the old one // If inconsistency occurs, the node changes. // Replacement can be done directly. // The judgement here is if one is TEXT and one is Element. // Type Judgment if (newVNodeFlag !== oldVNodeFlag) { replaceVNode(oldVNode, newVNode, container); } // Because Element and Text are created at new time using two functions to operate on // It's the same when updating. // Different operations should also be carried out for different modifications. // If the HTML of the new node is the same as that of the old node else if (newVNodeFlag == vnodeTypes.HTML) { // Replacement element operation patchMethos.patchElement(oldVNode, newVNode, container); } // If the TEXT of the new node is the same as that of the old node else if (newVNodeFlag == vnodeTypes.TEXT) { // Replacement text operation patchMethos.patchText(oldVNode, newVNode, container); } } // Update the VNode method set const patchMethos = { // Replacement text operation // Old virtual DOM nodes // New DOM Node // Containers Bearing DOM Nodes patchText(oldVNode,newVNode,container){ // Get el and assign oldVNode to newVNode let el = (newVNode.el = oldVNode.el); // If newVNode.children is not equal to oldVNode.children. // Otherwise, equality has no operation and no update is required. if(newVNode.children !== oldVNode.children){ // Direct substitution el.nodeValue = newVNode.children; } } }; // Replace virtual DOM function replaceVNode(oldVNode, newVNode, container) { // Delete the old node in the original node container.removeChild(oldVNode.el); // Rendering new nodes mount(newVNode, container); }
The above method simply implements a replacement operation for Text update. Because the replacement operation of Text is relatively simple, it is not enough to complete the update of Text. Element also needs to be updated when it operates. Relatively speaking, Text updates are much simpler than Element updates. Element updates are more complex, so they are put in the back because they are more important. Haha.~
First, before you want to replace Element, you need to determine which Data data has changed before you can replace it. In this case, you need to determine the Data to be changed, and then replace the original Data before you can do the next update operation.
// Update the VNode method set const patchMethos = { // Replacement element operation // Old virtual DOM nodes // New DOM Node // Containers Bearing DOM Nodes patchElement(oldVNode,newVNode,container){ // If the label name of newVNode is different from that of oldVNode // Since the labels are different, it's better to replace them directly. There's no need to do any other superfluous operations. if(newVNode.tag !== oldVNode.tag){ replaceVNode(oldVNode,newVNode,container); return; } // Update el let el = (newVNode.el = oldVNode.el); // Getting old Data data let oldData = oldVNode.data; // Get new Data data let newData = newVNode.data; // If new Data data exists // Update and add if(newData){ for(let attr in newData){ let oldVal = oldData[attr]; let newVal = newData[attr]; domAttributeMethod.patchData(el,attr,oldVal,newVal); } } // If the old Data exists // Detection update if(oldData){ for(let attr in oldData){ let oldVal = oldData[attr]; let newVal = newData[attr]; // If old data exists, new data does not exist. // It means deleted and needs to be updated if(oldVal && !newVal.hasOwnProperty(attr)){ // Since the new data does not exist, the new data is passed into Null domAttributeMethod.patchData(el,attr,oldVal,null); } } } } }; // dom Adding Attribute Method const domAttributeMethod = { // Modifying Data Data Data Method patchData (el,key,prv,next){ switch(key){ case "style": this.setStyle(el,key,prv,next); // Add here, look at me () // Add traversal loops // Loop old data this.setOldVal(el,key,prv,next); break; case "class": this.setClass(el,key,prv,next); break; default : this.defaultAttr(el,key,prv,next); break; } }, // Traversing through old data setOldVal(el,key,prv,next){ // Traversing through old data for(let attr in prv){ // If old data exists, new data does not exist. if(!next.hasOwnProperty(attr)){ // Direct assignment to strings el.style[attr] = ""; } } }, // Modifying the Event Registration Method addEvent(el,key,prev,next){ // Add here, look at me () // prev exists to delete the original event and rebind the new event if(prev){ el.removeEventListener(key.slice(1),prev); } if(next){ el.addEventListener(key.slice(1),next); } } }
In fact, the above operation only replaces the Data part, but its sub-elements are not replaced, so the sub-elements need to be replaced. The substitution sub-elements can be divided into six cases:
- There is only one old element.
- The old element is empty
- Old elements are multiple
- There is only one new element
- The new element is empty
- New elements are multiple
// Update the VNode method set const patchMethos = { // Replacement element operation // Old virtual DOM nodes // New DOM Node // Containers Bearing DOM Nodes patchElement(oldVNode,newVNode,container){ // If the label name of newVNode is different from that of oldVNode // Since the labels are different, it's better to replace them directly. There's no need to do any other superfluous operations. if(newVNode.tag !== oldVNode.tag){ replaceVNode(oldVNode,newVNode,container); return; } // Update el let el = (newVNode.el = oldVNode.el); // Getting old Data data let oldData = oldVNode.data; // Get new Data data let newData = newVNode.data; // If new Data data exists // Update and add if(newData){ for(let attr in newData){ let oldVal = oldData[attr]; let newVal = newData[attr]; domAttributeMethod.patchData(el,attr,oldVal,newVal); } } // If the old Data exists // Detection update if(oldData){ for(let attr in oldData){ let oldVal = oldData[attr]; let newVal = newData[attr]; // If old data exists, new data does not exist. // It means deleted and needs to be updated if(oldVal && !newVal.hasOwnProperty(attr)){ // Since the new data does not exist, the new data is passed into Null domAttributeMethod.patchData(el,attr,oldVal,null); } } } // Added here // Update child elements // Old child element type // New subelement types // children of old child elements // children of new child elements // el element, container this.patchChildren( oldVNode.childrenFlag, newVNode.childrenFlag, oldVNode.children, newVNode.children, el, ); }, // Update child elements // Old child element type // New subelement types // children of old child elements // children of new child elements // el element, container patchChildren(...arg){ let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg; switch(oldChildrenFlag){ // If the child element of the old element is one case childTeyps.SINGLE: this.upChildSingle(...arg); break; // If the child element of the old element is empty case childTeyps.EMPTY: this.upChildEmpty(...arg); break; // If the child elements of the old element are more than one case childTeyps.MULTIPLE: this.upChildMultiple(...arg); break; } }, upChildSingle(...arg){ let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg; // Cycling new subelements switch(newChildrenFlag){ // If the child element of the new element is one case childTeyps.SINGLE: patch(oldChildren,newChildren,container); break; // If the child element of the new element is empty case childTeyps.EMPTY: container.removeChild(oldChildren.el); break; // If there are more than one child element of the new element case childTeyps.MULTIPLE: container.removeChild(oldChildren.el); for(let i = 0;i<newChildren.length;i++){ mount(newChildren[i],container); } break; } }, upChildEmpty(...arg){ let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg; // Cycling new subelements switch(newChildrenFlag){ // If the child element of the new element is one case childTeyps.SINGLE: mount(newChildren,container); break; // If the child element of the new element is empty case childTeyps.EMPTY: break; // If there are more than one child element of the new element case childTeyps.MULTIPLE: container.removeChild(oldChildren.el); for(let i = 0;i<newChildren.length;i++){ mount(newChildren[i],container); } break; } }, upChildMultiple(...arg){ let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg; // Cycling new subelements switch(newChildrenFlag){ // If the child element of the new element is one case childTeyps.SINGLE: for(let i = 0;i<oldChildren.length;i++){ container.removeChild(oldChildren[i].el); } mount(newChildren,container); break; // If the child element of the new element is empty case childTeyps.EMPTY: for(let i = 0;i<oldChildren.length;i++){ container.removeChild(oldChildren[i].el); } break; // If there are more than one child element of the new element case childTeyps.MULTIPLE: // ** // Let's put it aside for now. Here's a comparison of all the nodes. // ** break; } } };
The above code is messy because of nested multi-layer loops. The general logic is to use the six cases mentioned above to pair one by one and use their corresponding solutions.
In the six cases mentioned above, switch matching logic:
New data | Old data |
---|---|
There is only one old element. | There is only one new element |
There is only one old element. | The new element is empty |
There is only one old element. | New elements are multiple |
The old element is empty | There is only one new element |
The old element is empty | The new element is empty |
The old element is empty | New elements are multiple |
Old elements are multiple | There is only one new element |
Old elements are multiple | The new element is empty |
Old elements are multiple | New elements are multiple |
The most complex is the last case, where there are many new and old elements, but react and vue are treated differently. The following is the diff algorithm of react.
When replacing virtual DOM, there is no need to change the order between elements. That is to say, if the original order is 123456 and the new order is 654321, the order between them will change. At this time, it needs to be changed, if the order of elements is insertion. Moreover, 192939495969 added a 9 after each number. In fact, there is no need to update at this time. In fact, the order between them is the same as before, only adding some element values. If it becomes 213456, it is time to change 12, and the rest is not necessary to make any changes. Moving. Next you need to add the most critical logic.
// Update the VNode method set // Add oldMoreAndNewMore method const patchMethos = { upChildMultiple(...arg) { let [oldChildrenFlag, newChildrenFlag, oldChildren, newChildren, container] = arg; // Cycling new subelements switch (newChildrenFlag) { // If the child element of the new element is one case childTeyps.SINGLE: for (let i = 0; i < oldChildren.length; i++) { // Traverse to delete old elements container.removeChild(oldChildren[i].el); } // Add new elements mount(newChildren, container); break; // If the child element of the new element is empty case childTeyps.EMPTY: for (let i = 0; i < oldChildren.length; i++) { // Delete all child elements container.removeChild(oldChildren[i].el); } break; // If there are more than one child element of the new element case childTeyps.MULTIPLE: // Modified here (`') this.oldMoreAndNewMore(...arg); break; }, oldMoreAndNewMore(...arg) { let [oldChildrenFlag, newChildrenFlag, oldChildren, newChildren, container] = arg; let lastIndex = 0; for (let i = 0; i < newChildren.length; i++) { let newVnode = newChildren[i]; let j = 0; // Is the new element found? let find = false; for (; j < oldChildren.length; j++) { let oldVnode = oldChildren[j]; // The same key is the same element if (oldVnode.key === newVnode.key) { find = true; patch(oldVnode, newVnode, container); if (j < lastIndex) { if(newChildren[i-1].el){ // Need to move let flagNode = newChildren[i-1].el.nextSibling; container.insertBefore(oldVnode.el, flagNode); } break; } else { lastIndex = j; } } } // If the old element is not found, it needs to be added if (!find) { // Markup elements that need to be inserted let flagNode = i === 0 ? oldChildren[0].el : newChildren[i-1].el; mount(newVnode, container, flagNode); } // Removing Elements for (let i = 0; i < oldChildren.length; i++) { // Old node const oldVNode = oldChildren[i]; // Whether the new node key exists in the old node const has = newChildren.find(next => next.key === oldVNode.key); if (!has) { // If deletion does not exist container.removeChild(oldVNode.el) } } } } }; // Modify mount function // flagNode flags where new elements of node need to be inserted function mount(vnode, container, flagNode) { // The type of rendering label required let { flag } = vnode; // If it is a node if (flag === vnodeTypes.HTML) { // Call the Create Node Method mountMethod.mountElement(vnode, container, flagNode); } // If it is text else if (flag === vnodeTypes.TEXT) { // Call the Create Text Method mountMethod.mountText(vnode, container); }; }; // Modify mountElement const mountMethod = { // Creating HTML Element Method // Modified here (') to add flagNode parameter mountElement(vnode, container, flagNode) { // Attribute, label name, sub-element, sub-element type let { data, tag, children, childrenFlag } = vnode; // Real Nodes Created let dom = document.createElement(tag); // Adding attributes data && domAttributeMethod.addData(dom, data); // Save real DOM nodes in VNode vnode.el = dom; // If it is not empty, it means that there are child elements. if (childrenFlag !== childTeyps.EMPTY) { // If it's a single element if (childrenFlag === childTeyps.SINGLE) { // Pass in the child element and the DOM node currently created as the parent element // In fact, it's about mounting children into the currently created element. mount(children, dom); } // If there are multiple elements else if (childrenFlag === childTeyps.MULTIPLE) { // Loop subnodes and create children.forEach((el) => mount(el, dom)); }; }; // Adding element nodes modifies this () flagNode ? container.insertBefore(dom, flagNode) : container.appendChild(dom); } }
Ultimately:
const VNODEData = [ "div", {id:"test",key:789}, [ createElement("p",{ key:1, style:{ color:"red", background:"pink" } },"Node 1"), createElement("p",{ key:2, "@click":() => console.log("click me!!!") },"Node 2"), createElement("p",{ key:3, class:"active" },"Node 3"), createElement("p",{key:4},"Node 4"), createElement("p",{key:5},"Node 5") ] ]; let VNODE = createElement(...VNODEData); render(VNODE,document.getElementById("app")); const VNODEData1 = [ "div", {id:"test",key:789}, [ createElement("p",{ key:6 },"Node 6"), createElement("p",{ key:1, style:{ color:"red", background:"pink" } },"Node 1"), createElement("p",{ key:5 },"Node 5"), createElement("p",{ key:2 },"Node 2"), createElement("p",{ key:4 },"Node 4"), createElement("p",{ key:3, class:"active" },"Node 3") ] ]; setTimeout(() => { let VNODE = createElement(...VNODEData1); render(VNODE,document.getElementById("app")); },1000)
The above code uses a lot of logic to deal with which uses a lot of computation, comparing peer nodes between two trees. This completely reduces the complexity and does not cause any loss. Because it is not possible to move a component across the DOM tree in a web application.
In fact, both React and Vue need to assign a key attribute to their elements when rendering the list, because in diff algorithm, the original elements will be used first to adjust the location, which is also a highlight of performance optimization.
epilogue
This paper is only a simple implementation of diff algorithm, which may not meet all the requirements. The basic principle of React is the same. I hope this article can help you understand diff algorithm.
Thank you very much for reading this article for such a long time. The code in this article is too long. If there are any mistakes, please point out in the comment area. I will make corrections in time.