Virtual DOM
node
Virtual DOM is a JS abstract representation of DOM. They are JS objects that can describe the structure and relationship of DOM. Various state changes of the application will be reflected on the virtual Dom and finally mapped to the real dom.
Before rendering the virtual DOM, we need to make some preparations. By observing the real Dom and components, we can know:
-
Virtual DOM needs to have its own types, such as HTML tags, pure text, components (components are divided into class components and function components), etc. because we are implementing a simple virtual DOM, we only implement plain text and HTML tags
-
We know that the real DOM is similar to a tree structure, so we need to know the type of DOM sub elements, whether they are single sub elements, multiple sub elements or empty (plain text or empty)
-
At the same time, the virtual DOM is a description of the real dom. For a single element, we can simply summarize it as label and attribute (attributes include style, id, class, click event, etc.)
So we first need to define some constants to distinguish the above
//Type of virtual DOM const vnodeType={ HTML:'HTML',//html tag TEXT:'TEXT',//pure text COMPONENT:'COMPONENT',//function component CLASS_COMPONENT:'CLASS_COMPONENT',//class component } // Type of child element const childType = { EMPTY:'EMPTY',//The child element is empty (this simply refers to the case of plain text) SINGLE:'SINGLE',//Single child element MULTIPLE:'MULTIPLE'//There are multiple child elements }
Because we often need to use the real DOM corresponding to vnode when updating in the future, we define a field to store the dom. In subsequent update operations, we need to use the key attribute to perform diff algorithm when updating old and new DOMS
/** * New virtual DOM * @param {string} tag Because it implements a simple virtual DOM, this tag is a simple element tag name * @param {obj} data attribute * @param {obj} children Child element */ function createElement(tag, data, children = null) { //New virtual DOM .......................................... //Return virtual DOM: vnode return { flag,//The type used to mark the current vnode tag,//Tags: div, text (empty), component (a function, not involved temporarily) data,//data children,//Child element childrenFlag,//Types of children el:null//Store the real dom, which defaults to null at the beginning } } /** * Handle vnode where children become text type * @param {*} text children element */ function createTextVnode(text) { return { flag: vnodeType.TEXT, tag: null, key:data&&data.key, children: text, childrenFlag: childType.EMPTY, el: null } }
Render
When there are nodes, start rendering nodes
function render(){ //First, we need to distinguish between first rendering and second rendering mount(vnode,container) } /** * Render virtual DOM for the first time, Mount * @param {obj} vnode Virtual DOM * @param {*} container Render container * @param {} flagNode It is used to determine whether to insert when */ function mount(vnode,container,flagNode){ //First, render according to the flag of vnode let {flag} = vnode //Here, the mounting of the execution mode is determined according to the type of vnode if(flag === vnodeType.HTML){ //If html tag mountElement(vnode,container,flagNode) }else if(flag ===vnodeType.TEXT){ //If text is plain text mountText(vnode,container) } } /** * Mount virtual DOM of html type * @param {obj} vnode Virtual DOM * @param {*} container container * @param {} flagNode It is used to determine whether to insert when */ function mountElement(vnode,container,flagNode){ //vnode of html type creates dom based on tag name let dom = document.createElement(vnode.tag) //When you first mount, mount the real dom corresponding to the current vnode to the current vnode for later use when you mount child elements vnode.el =dom let {data,children,childrenFlag} = vnode //Mount properties if (data) { for (let key in data) { //Mount data patchData(vnode.el, key, null, data[key]) } } //Start mounting child elements if(childrenFlag !==childType.EMPTY){ //If the child element is not empty if(childrenFlag===childType.SINGLE){ //Mount child element mount(children,vnode.el) }else if(childrenFlag===childType.MULTIPLE){ for(let i=0;i<children.length;i++){ mount(children[i],vnode.el) } } } //Mount dom flagNode?container.insertBefore(el, flagNode):container.appendChild(dom) } /** * Mount virtual DOM of plain text type * @param {odj} vnode Virtual DOM * @param {*} container container */ function mountText(vnode, container) { //For vnode of plain text type, the child element is text, so it is executed directly let dom = document.createTextNode(vnode.children) vnode.el = dom container.appendChild(vnode.el) }
Mount properties
When rendering the virtual DOM, we also need a method to render the data attribute, that is, the attachment of the attribute. When attaching attributes, we need to distinguish them and deal with different attributes differently.
/** * Mount properties * @param {*} dom Node real dom * @param {string} key data Corresponding key * @param {obj} preData data Old value * @param {obj} newData data New value */ function patchData(dom, key, preData, newData) { //Render in different ways according to different types of attributes switch (key) { case 'style': for (let k in newData) { //Mount the corresponding properties dom.style[k] = newData[k] } //Some attributes need to be deleted during patch break; case 'class': dom.className = newData break; default: if (key[0] === '@') { //The presence of the @ symbol is considered a click event if (newData) { dom.addEventListener(key.split(1), newData) } } else { //Otherwise, we'll deal with it in a rough way here dom.setAttribute(key, newData) } break; } }
Virtual DOM update
The previous code implements the initial mounting of the virtual dom. But when we make changes to the virtual DOM, what we need is an update operation. At the same time, the update operation is also a complex part of the render process.
Change rendering method
function render(vnode, container) { //First, we need to distinguish between first rendering and second rendering if(container.vnode){ patch(container.vnode,vnode,container) }else{ mount(vnode, container) } //After mounting, mount the vnode into the container to judge whether it is the first rendering or the subsequent update rendering container.vnode = vnode }
Update method: patch
The main function of this function is to distinguish the update operation according to the flag attribute in new and old vnode s. The replacement operation and update text operation are relatively simple
Both initialization and update are completed by patch
function patch(prev,next,container){ let nextFlag = next.flag let prevFlag = prev.flag //Different processing is carried out according to the types of new and old virtual DOM S if(nextFlag!==prevFlag){ //If the flag types are different, we directly perform the replacement operation replaceVnode(prev,next,container); }else if(nextFlag==vnodeType.HTML){ //Update element patchElement(prev,next,container) }else if(nextFlag==vnodeType.TEXT){ //Update text patchText(prev,next) } } /** * Update Text * @param {ovj} prev Old vnode * @param {*} next New vnode */ function patchText(prev,next){ let el = (next.el = prev.el) if(next.children!==prev.children){ //Directly change text in dom el.nodeValue = next.children } } /** * Replace operation to update virtual DOM * @param {obj} prev Old vnode * @param {obj} next New vnode * @param {*} container container */ function replaceVnode(prev,next,container){ //Replace directly container.removeChild(prev.el) mount(next,container) }
const patchVNode = (oldVNode, newVNode) => { // The element labels are the same, and patch if (oldVNode.tag === newVNode.tag) { // If the element types are the same, the old elements must be reused let el = newVNode.el = oldVNode.el // The child node of the new node is the text node if (newVNode.text) { // Remove child nodes of old nodes if (oldVNode.children) { oldVNode.children.forEach((item) => { el.removeChild(item.el) }) } // Update the text if the text content is different if (oldVNode.text !== newVNode.text) { el.textContent = newVNode.text } } else { // ... } } else { // Replace oldNode with newNode differently // ... } }
Update the new node. Here we use diff to compare the old and new nodes
1. Get the types of child nodes and child nodes. There are three types of child nodes in the new node n2 and the old node n1: text node, array node and empty node
2. The old node is an empty text node, and then the old node is an empty text node
3. If the new node is a text node and the old node is a text node, compare whether the text of the old and new nodes is consistent. If not, replace the text content
4. If the old node is an array node and the new node is also an array node, enter patchKeyedChildren, which is the process of diff
5. If the old node is an array node and the new node is an empty node, go to delete the old node
6. In this way, the remaining situation is that the previous node is either a text node or empty, while the new node is either an array or empty
7. Therefore, if the previous node is a text node, delete the text content of the old node
8. If the new node is an array, add the new node to the dom tree
9. Finally, the new node is left as an empty node without any operation (the old node has been deleted at point 7)