Briefly introduce the virtual DOM and diff algorithm
demand
Method 1: dismantle and rebuild
Method 2: diff
primary coverage
snabbdom introduction and test environment construction
Introduction to snabbdom
-
snabbdom is a Swedish word, which originally means "speed";
-
Snabbdom is a famous virtual DOM library and the ancestor of diff algorithm. Vue source code draws on snabbdom;
-
Official git: snabbdom
Installing snabbdom
-
The snabbdom source code on Git is written in TypeScript, and git does not provide a compiled JavaScript version;
-
If you want to directly use the built JavaScript version of the snabbdom library, you can download it from npm:
npm i -S snabbdom
-
When learning the bottom layer of the library, it is recommended that you read the original TS code, preferably with the original comments of the library author, which will greatly improve your reading ability of the source code.
Construction of snabbdom test environment
-
The snabbdom library is a DOM library and cannot run in the nodejs environment. Therefore, it is necessary to build a webpack and webpack dev server development environment without installing any loader s
-
Note here that the latest version must be installed webpack@5 , cannot be installed webpack@4 , because there is no ability to read exports in webpack4
npm i -S webpack@5 webpack-cli@3 webpack-dev-server@3
-
Refer to the official website of webpack and write webpack config. JS file
Run through the demo program on the official git home page
-
Run through s nabbdom official git home page demo program, which proves that the debugging environment has been successfully built
-
Don't forget in index Place a div#container in HTML
Virtual DOM and h functions
Virtual DOM is similar to token in mustache
diff occurs on the virtual DOM
The course does not look at how DOM becomes a virtual dom
research contents
-
How are virtual functions generated by rendering functions (h functions)?
Handwritten h function
-
Principle of diff algorithm?
Handwritten diff algorithm
-
How does a virtual DOM become a real DOM through diff
In fact, changing the virtual DOM back to the real DOM is covered by the diff algorithm
How are virtual functions generated by rendering functions (h functions)?
The h function is used to generate virtual nodes
The h function is used to generate a virtual node (vnode)
For example, this will call the h function
You will get such a virtual node
{ "sel": "a", "data": { props: { href: "http://www.atguigu.com" } }, "text": "Shang Silicon Valley" }
It represents the real DOM node
What attributes does a virtual node have
{ children: undefined, // Child element data: {}, // Attributes, styles elm: undefined, // It corresponds to the real DOM node. If it is undefined, it means that the node has not been in the tree key: undefined, // Node unique ID sel: "div", // selector text: "I am a box" // written words }
The h function can be nested to obtain a virtual DOM tree( 🧨)
For example, use the h function nested like this
You will get such a virtual DOM tree
The usage of h function is very flexible
for example
Handwritten h function
vnode
vnode.js
/** * vnode The function of the function is very simple, that is, it returns the incoming five parameter combination object */ export default function (sel, data, children, text, elm) { return { sel, data, children, text, elm }; }
h function
h.js
import vnode from "./vnode"; /* * Write a low configuration version of h function. This function must receive three parameters, which are indispensable - the overload function is weak * That is to say, the calling mode must be one of the following three: * Form â‘ h('div ', {},' text ') * Form â‘¡ h('div ', {}, []) * Configuration â‘¢ h('div', {}, h()) * */ export default function (sel, data, c) { // Check the number of parameters if (arguments.length !== 3) throw new Error('I'm sorry,h Function must pass in 3 parameters,We are a low configuration version h function'); // Check the type of parameter c if (typeof c === 'string' || typeof c === 'number') { return vnode(sel, data, undefined, c, undefined); } else if (Array.isArray(c)) { // It shows that calling h function is form â‘¡ let children = []; // Traversal c, mobile children for (let i = 0; i < c.length; i++) { // Check c[i] must be an object if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) throw new Error('An item in the passed in array parameter is not h function'); // There is no need to execute c[i], because it has been executed in the test statement // Just collect the children children.push(c[i]); } // When the cycle is over, it means that children have been collected. At this time, you can return to the virtual node. There are children nodes return vnode(sel, data, children, undefined, undefined); } else if (typeof c === 'object' && c.hasOwnProperty('sel')) { // It shows that calling h function is form â‘¢ // That is, the incoming c is the only children You do not need to execute c because c is already executed in the test statement return vnode(sel, data, [c], undefined, undefined); } else { throw new Error('The parameter type passed in is incorrect'); } };
Look at the TS code and write the JS code
- Look at the TS version of the source code, and then copy the JS code
- As long as the trunk function, give up the implementation of some details
Perceptual diff algorithm
By changing the content of the li tag, we know that the diff algorithm is updated with the minimum amount
The key can uniquely identify the node and serve the minimum amount of updates
import {init} from 'snabbdom/init'; import {classModule} from "snabbdom/modules/class"; import {propsModule} from "snabbdom/modules/props"; import {styleModule} from "snabbdom/modules/style"; import {eventListenersModule} from "snabbdom/modules/eventlisteners"; import {h} from 'snabbdom/h'; // Create a patch function const patch = init([classModule, propsModule, styleModule, eventListenersModule]); // Get the box and button const container = document.getElementById('container'); const btn = document.getElementById('btn'); // Create virtual node const vnode1 = h('ul', {}, [ h('li', {key: 'A'}, 'A'), h('li', {key: 'B'}, 'B'), h('li', {key: 'C'}, 'C'), h('li', {key: 'D'}, 'D') ]) patch(container, vnode1) const vnode2 = h('ul', {}, [ h('li', {key: 'E'}, 'E'), h('li', {key: 'A'}, 'A'), h('li', {key: 'B'}, 'B'), h('li', {key: 'C'}, 'C'), h('li', {key: 'D'}, 'D'), ]) // Click the button to change vnode1 to vnode2 btn.onclick = () => { patch(vnode1, vnode2) }
Experience
-
The minimum update is very powerful! It's really the least amount of updates. Of course, key is very important.
key is the unique identifier of this node and tells the diff algorithm that they are the same DOM node before and after the change.
-
Only the same virtual node can be used for fine comparison., Otherwise, it is violent to delete the old and insert the new.
Extension problem: how to define the same virtual node? Answer: the selectors are the same and the key s are the same.
-
Only the same layer comparison is performed, and cross layer comparison is not performed.. Even if it's the same virtual node, it's cross layer. Sorry, refinement doesn't diff you. Instead, it violently deletes the old one and then inserts the new one.
diff is not so meticulous! Does it really affect efficiency??
A: the above 2 and 3 operations will not be encountered in the actual Vue development, so this is a reasonable optimization mechanism.
Schematic diagram of comparison on the same floor
// Create virtual node const vnode1 = h('ul', {}, [ h('li', {key: 'A'}, 'A'), h('li', {key: 'B'}, 'B'), h('li', {key: 'C'}, 'C'), h('li', {key: 'D'}, 'D') ]) patch(container, vnode1) const vnode2 = h('ul', {}, h('section', {}, [ h('li', {key: 'E'}, 'E'), h('li', {key: 'A'}, 'A'), h('li', {key: 'B'}, 'B'), h('li', {key: 'C'}, 'C'), h('li', {key: 'D'}, 'D'), ]))
As in the above code, adding a layer of nodes will no longer update the minimum amount, but reconstruct it.
diff algorithm deals with the problem that the old and new nodes are not the same node
How to define "same node"
The key of the old node should be the same as that of the new node
And
The selector of the old node should be the same as that of the new node
When creating nodes, all child nodes need to be created recursively
When I first went up the tree
patch.js
import vnode from "./vnode"; import createElement from "./createElement"; export default function (oldVnode, newVnode) { // Determine whether the first parameter passed in is a DOM node or a virtual node if (oldVnode.sel === '' || oldVnode.sel === undefined) { // The first parameter passed in is the DOM node, which should be wrapped as a virtual node oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode); } // Judge whether oldVnode and newVnode are the same node if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) { // Is the same node // TODO: fine comparison } else { // Not the same node createElement(newVnode, oldVnode.elm); } };
createElement.js
// Really create nodes. Create vnode as DOM and insert it before the pivot element export default function (vnode, pivot) { // The purpose is to insert the virtual node vnode before the benchmark pivot // Create a DOM node, which is still an orphan node let domNode = document.createElement(vnode.sel); // Child nodes or text if (vnode.text !== "" && (vnode.children === undefined || vnode.children.length === 0)) { // Inside is text domNode.innerText = vnode.text; // Put the orphan node on the tree. Let the parent element of the benchmark node call the insertBefore method to insert the new orphan node before the label node pivot.parentNode.insertBefore(domNode, pivot); } else if (Array.isArray(vnode.children) && vnode.children.length > 0) { } };
Create child nodes recursively
To accommodate recursive operations, insert operations into patch JS instead of createElement
Here, diff is used to deal with the situation that the old and new nodes are not the same node. Create a new insert and delete it violently
index.js
import h from './mySnabbdom/h'; import patch from './mySnabbdom/patch' const container = document.getElementById('container'); const btn = document.getElementById('btn'); const myVnode1 = h('h1', {}, 'Hello'); const myVnode2 = h('ul', {}, [ h('li', {}, 'A'), h('li', {}, 'B'), h('li', {}, [ h('div', {}, [ h('ol', {}, [ h('li', {}, 'Ha ha ha'), h('li', {}, 'Hey, hey, hey'), h('li', {}, 'Interesting'), ]) ]) ]), h('li', {}, 'D'), ]) const myVnode3 = h('section', {}, [ h('h1', {}, 'I'm new h1'), h('h2', {}, 'I'm new h2'), h('h3', {}, 'I'm new h3'), ]) patch(container, myVnode2); btn.onclick = function () { patch(myVnode2, myVnode3); }
createElement.js
// Really create nodes. Create vnode as DOM, which is an orphan node and will not be inserted export default function createElement(vnode) { // console.log(` the purpose is to turn the virtual node ${vnode} into a real DOM `) // Create a DOM node, which is still an orphan node let domNode = document.createElement(vnode.sel); // Child nodes or text if (vnode.text !== "" && (vnode.children === undefined || vnode.children.length === 0)) { // Inside is text domNode.innerText = vnode.text; // Supplementary elm attribute vnode.elm = domNode; } else if (Array.isArray(vnode.children) && vnode.children.length > 0) { // If it is not a child node, you need to create a node recursively for (let i = 0; i < vnode.children.length; i++) { // Get the current children let ch = vnode.children[i]; // Create its dom. Once createElement is called, it means that the DOM has been created, and its elm attribute points to the created DOM, but it has not been up the tree. It is an orphan node let chDom = createElement(ch); // Go up the tree domNode.appendChild(chDom); } } // Supplementary elm attribute vnode.elm = domNode; // Returns elm, which is a pure DOM object return vnode.elm; };
patch.js
import vnode from "./vnode"; import createElement from "./createElement"; export default function (oldVnode, newVnode) { // Determine whether the first parameter passed in is a DOM node or a virtual node if (oldVnode.sel === '' || oldVnode.sel === undefined) { // The first parameter passed in is the DOM node, which should be wrapped as a virtual node oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode); } // Judge whether oldVnode and newVnode are the same node if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) { // Is the same node // TODO: fine comparison } else { // Not the same node let newVnodeElm = createElement(newVnode); // Before inserting into the old node if (oldVnode.elm.parentNode && newVnodeElm) oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm); // Delete old node oldVnode.elm.parentNode.removeChild(oldVnode.elm); } };
diff handles when the old and new nodes are the same node
Different situations of handwritten old and new text nodes
patch.js
import vnode from "./vnode"; import createElement from "./createElement"; export default function (oldVnode, newVnode) { // Determine whether the first parameter passed in is a DOM node or a virtual node if (oldVnode.sel === '' || oldVnode.sel === undefined) { // The first parameter passed in is the DOM node, which should be wrapped as a virtual node oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode); } // Judge whether oldVnode and newVnode are the same node if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) { console.log('Is the same node') // Judge whether the old and new node s are the same object if (oldVnode === newVnode) return; // Determine whether newVnode has text attribute if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) { // The new vnode has a text attribute // console.log('New vnode has text attribute ') if (newVnode.text !== oldVnode.text) // If the text in the new virtual node is different from the text in the old virtual node, you can directly write the new text into the old elm. If the old elm is children, it will disappear immediately oldVnode.elm.innerText = newVnode.text; } else { // The new vnode has no text attribute and children // console.log('New vnode has no text attribute ') // Judge whether the old have children if (oldVnode.children !== undefined && oldVnode.children.length > 0) { // Old people have children. This is the most complicated situation. There are children both old and new } else { // The old has no children, the new has children // Empty the contents of the old node oldVnode.elm.innerHTML = ''; // Traverse the child nodes of newVnode, create dom, and loop up the tree newVnode.children.forEach(node => { let dom = createElement(node); oldVnode.elm.appendChild(dom); }) } } } else { // Not the same node let newVnodeElm = createElement(newVnode); // Before inserting into the old node if (oldVnode.elm.parentNode && newVnodeElm) oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm); // Delete old node oldVnode.elm.parentNode.removeChild(oldVnode.elm); } };
Add key to vnode
If there is a key in the data, bind the key to vnode
vnode.js
/** * vnode The function of the function is very simple, that is, it returns the incoming five parameter combination object */ export default function (sel, data, children, text, elm) { const key = data.key; return { sel, data, children, text, elm, key }; }
Attempt to write diff to update child nodes
patchVnode.js
import createElement from "./createElement"; export default function patchVnode(oldVnode, newVnode) { // Judge whether the old and new node s are the same object if (oldVnode === newVnode) return; // Judge whether newVnode has text attribute if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) { // The new vnode has a text attribute // console.log('New vnode has text attribute ') if (newVnode.text !== oldVnode.text) // If the text in the new virtual node is different from the text in the old virtual node, you can directly write the new text into the old elm. If the old elm is children, it will disappear immediately oldVnode.elm.innerText = newVnode.text; } else { // The new vnode has no text attribute and children // console.log('New vnode has no text attribute ') // Judge whether the old have children if (oldVnode.children !== undefined && oldVnode.children.length > 0) { // There are old children. This is the most complicated situation. There are children both old and new // The beginning of all unprocessed nodes let un = 0; for (let i = 0; i < newVnode.children.length; i++) { let ch = newVnode.children[i]; // Traverse again to see if there is a node in oldVnode and it is the same let isExist = false; for (let j = 0; j < oldVnode.children.length; j++) { if (oldVnode.children[j].sel === ch.sel && oldVnode.children[j].key === ch.key) { isExist = true; } } if (!isExist) { let dom = createElement(ch); ch.elm = dom; if (un < oldVnode.children.length) oldVnode.elm.insertBefore(dom, oldVnode.children[un].elm); else oldVnode.elm.appendChild(dom); } else { // Move the processed node down one bit un++; // Judge whether the positions of front and rear nodes are consistent } } } else { // The old has no children, the new has children // Empty the contents of the old node oldVnode.elm.innerHTML = ''; // Traverse the child nodes of newVnode, create dom, and loop up the tree newVnode.children.forEach(node => { let dom = createElement(node); oldVnode.elm.appendChild(dom); }) } } }
For example, in the following three cases, all cases are very complex and mixed together, so an excellent update algorithm is needed
New information
Newly created nodes are inserted before all unprocessed nodes, not after all processed nodes.
For example, on the way, if MN is inserted and all processed nodes are AB, MN will be inserted into B in turn, which is inconsistent with the purpose.
Deletion
Update status
Sub node update strategy of diff algorithm
Four hit lookups:
Before new: all unprocessed nodes in the new virtual node
After new: all the last unprocessed nodes in the new virtual node
Old: all unprocessed nodes in the old virtual nodes
After old: all the last unprocessed nodes in the old virtual nodes
Optimization strategy of classical diff algorithm
- New front and old front: if the old node completes the cycle first, it indicates that there are nodes to be inserted in the new node.
- New after and old after: if the new node completes the cycle first, if there are still remaining nodes in the old node (the node between the old and new pointers), it indicates that they are the nodes to be deleted.
- New and old: (if this happens and involves a mobile node, the node pointed to by the new front will be after the old one)
- New front and old rear: (if this happens and involves a mobile node, the node pointed to by the new front is before the old front of the mobile)
If you hit one, you will no longer judge the hit
If you don't hit, you need to find it with a loop. Move to before oldStartIdx.
New information
Deletion
Multiple deletion
Complex situation
Handwriting child node update strategy
updateChildren.js
import patchVnode from "./patchVnode"; import createElement from "./createElement"; // Determine whether it is the same virtual node function checkSameVnode(vnodeA, vnodeB) { return vnodeA.sel === vnodeB.sel && vnodeA.key === vnodeB.key; } export default function updateChildren(parentElm, oldCh, newCh) { // console.log('I'm updateChildren ') // console.log(oldCh, newCh) // Define old, new, old and new numbers let oldStartIdx = 0, newStartIdx = 0, oldEndIdx = oldCh.length - 1, newEndIdx = newCh.length - 1; // Define old, old, new and new nodes let oldStartVnode = oldCh[oldStartIdx], oldEndVnode = oldCh[oldEndIdx], newStartVnode = newCh[newStartIdx], newEndVnode = newCh[newEndIdx]; let keyMap = null; // Start big while while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // First, it is not to judge â‘ â‘¡ â‘¢ â‘£ hit, but to skip what has been marked undefined if (oldStartVnode == null || oldCh[oldStartIdx] === undefined) { oldStartVnode = oldCh[++oldStartIdx]; } else if (oldEndVnode == null || oldCh[oldEndIdx] === undefined) { oldEndVnode = oldCh[--oldEndIdx]; } else if (newStartVnode == null || newCh[newStartIdx] === undefined) { newStartVnode = newCh[++newStartIdx]; } else if (newEndVnode == null || newCh[newEndIdx] === undefined) { newEndVnode = newCh[--newEndIdx]; } else if (checkSameVnode(newStartVnode, oldStartVnode)) { // New and old console.log("â‘ New and old hits") patchVnode(oldStartVnode, newStartVnode); newStartVnode = newCh[++newStartIdx]; oldStartVnode = oldCh[++oldStartIdx]; } else if (checkSameVnode(newEndVnode, oldEndVnode)) { // New and old console.log("â‘¡New and old hits") patchVnode(oldEndVnode, newEndVnode); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (checkSameVnode(newEndVnode, oldStartVnode)) { // New and old console.log("â‘¢New and old hits") patchVnode(oldStartVnode, newEndVnode); // When â‘¢ new and old hits, move the node at this time. Move the node pointed to before the new node to the back of the old node // How to move a node?? As long as you insert a node that is already on the DOm tree, it will be moved parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling); newEndVnode = newCh[--newEndIdx]; oldStartVnode = oldCh[++oldStartIdx]; } else if (checkSameVnode(newStartVnode, oldEndVnode)) { // New front and old back console.log("â‘£New front and old rear hits") patchVnode(oldEndVnode, newStartVnode); // â‘£ when the new front and old rear hit, move the node at this time. Move the node pointed by the new node to the front of the old node // How to move a node?? As long as you insert a node that is already on the DOm tree, it will be moved parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; oldEndVnode = oldCh[--oldEndIdx]; } else { // None of the four hits hit // Make the map mapping object of the key so that you don't have to traverse the old object every time. if (!keyMap) { keyMap = {}; // From oldStartIdx to oldEndIdx, create a KeyMap mapping object for (let i = oldStartIdx; i <= oldEndIdx; i++) { const key = oldCh[i].key; if (key !== undefined) keyMap[key] = i; } } //console.log(keyMap); // Find the position sequence number of the mapping of the current item (nextStartIdx) in the keyMap const idxInOld = keyMap[newStartVnode.key]; // console.log(idxInOld); if (idxInOld === undefined) { // Judge that if idxInOlx is undefined, it means it is a new item and only needs to be inserted // The added item (newStartVnode) is not a real DOM node at present parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm); } else { // If it is not undefined, it needs to be moved const elmToMove = oldCh[idxInOld]; patchVnode(elmToMove, newStartVnode); // Setting this item to undefined indicates that this item has been processed oldCh[idxInOld] = undefined; // To move, you can also call insertBefore to move parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm); } // Move the pointer down to move only the new head newStartVnode = newCh[++newStartIdx]; } } // After the while is finished, you need to continue to see if there are any remaining nodes to judge whether you need to delete or add nodes if (newStartIdx <= newEndIdx) { // console.log('new, there are still remaining nodes that have not been processed, and items should be added. To insert all remaining nodes before oldStart '); // After the loop ends, start is still smaller than old // before is the inserted benchmark // const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; // Traverse the new newCh and add it to the old one without processing for (let i = newStartIdx; i <= newEndIdx; i++) { // The insertBefore method can automatically identify null. If it is null, it will automatically queue at the end of the queue. It's consistent with appendChild. // newCh[i] there is no real DOM yet, so you need to call the createElement() function to change it into dom // parentElm.insertBefore(createElement(newCh[i]), before); parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm); } } else if (oldStartIdx <= oldEndIdx) { // console.log('old has remaining nodes that have not been processed, items to be deleted ') // Batch delete items between oldStart and oldEnd pointers for (let i = oldStartIdx; i <= oldEndIdx; i++) if (oldCh[i]) parentElm.removeChild(oldCh[i].elm); } };
Full handwritten
patch.js
import vnode from "./vnode"; import createElement from "./createElement"; import patchVnode from "./patchVnode"; export default function patch(oldVnode, newVnode) { // Determine whether the first parameter passed in is a DOM node or a virtual node if (oldVnode.sel === '' || oldVnode.sel === undefined) { // The first parameter passed in is the DOM node, which should be wrapped as a virtual node oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode); } // Judge whether oldVnode and newVnode are the same node if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) { // console.log('Is the same node ') patchVnode(oldVnode, newVnode); } else { // Not the same node let newVnodeElm = createElement(newVnode); // Before inserting into the old node if (oldVnode.elm.parentNode && newVnodeElm) oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm); // Delete old node oldVnode.elm.parentNode.removeChild(oldVnode.elm); } };
vnode.js
/** * vnode The function of the function is very simple, that is, it returns the incoming five parameter combination object */ export default function (sel, data, children, text, elm) { const key = data.key; return { sel, data, children, text, elm, key }; }
createElement.js
// Really create nodes. Create vnode as DOM, which is an orphan node and will not be inserted export default function createElement(vnode) { // console.log(` the purpose is to turn the virtual node ${vnode} into a real DOM `) // Create a DOM node, which is still an orphan node let domNode = document.createElement(vnode.sel); // Child nodes or text if (vnode.text !== "" && (vnode.children === undefined || vnode.children.length === 0)) { // Inside is text domNode.innerText = vnode.text; // Supplementary elm attribute vnode.elm = domNode; } else if (Array.isArray(vnode.children) && vnode.children.length > 0) { // If it is not a child node, you need to create a node recursively for (let i = 0; i < vnode.children.length; i++) { // Get the current children let ch = vnode.children[i]; // Create its dom. Once createElement is called, it means that the DOM has been created, and its elm attribute points to the created DOM, but it has not been up the tree. It is an orphan node let chDom = createElement(ch); // Go up the tree domNode.appendChild(chDom); } } // Supplementary elm attribute vnode.elm = domNode; // Returns elm, which is a pure DOM object return vnode.elm; };
patchVnode.js
import createElement from "./createElement"; import updateChildren from "./updateChildren"; // Compare the same virtual node export default function patchVnode(oldVnode, newVnode) { // Judge whether the old and new node s are the same object if (oldVnode === newVnode) return; // Determine whether newVnode has text attribute if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) { // The new vnode has a text attribute // console.log('New vnode has text attribute ') if (newVnode.text !== oldVnode.text) // If the text in the new virtual node is different from the text in the old virtual node, you can directly write the new text into the old elm. If the old elm is children, it will disappear immediately oldVnode.elm.innerText = newVnode.text; } else { // The new vnode has no text attribute and children // console.log('New vnode has no text attribute ') // Judge whether the old have children if (oldVnode.children !== undefined && oldVnode.children.length > 0) { // There are children in the old and children in the new. This is the most complicated situation. There are children both old and new updateChildren(oldVnode.elm, oldVnode.children, newVnode.children); } else { // The old has no children, the new has children // Empty the contents of the old node oldVnode.elm.innerHTML = ''; // Traverse the child nodes of newVnode, create dom, and loop up the tree newVnode.children.forEach(node => { let dom = createElement(node); oldVnode.elm.appendChild(dom); }) } } }
h.js
import vnode from "./vnode"; /* * Write a low configuration version of h function. This function must receive three parameters, which are indispensable - the overload function is weak * That is to say, the calling mode must be one of the following three: * Form â‘ h('div ', {},' text ') * Form â‘¡ h('div ', {}, []) * Configuration â‘¢ h('div', {}, h()) * */ export default function (sel, data, c) { // Check the number of parameters if (arguments.length !== 3) throw new Error('I'm sorry,h Function must pass in 3 parameters,We are a low configuration version h function'); // Check the type of parameter c if (typeof c === 'string' || typeof c === 'number') { return vnode(sel, data, undefined, c, undefined); } else if (Array.isArray(c)) { // It shows that calling h function is form â‘¡ let children = []; // Traversal c, mobile children for (let i = 0; i < c.length; i++) { // Check c[i] must be an object if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) throw new Error('An item in the passed in array parameter is not h function'); // There is no need to execute c[i], because it has been executed in the test statement // Just collect the children children.push(c[i]); } // When the cycle is over, it means that children have been collected. At this time, you can return to the virtual node. There are children nodes return vnode(sel, data, children, undefined, undefined); } else if (typeof c === 'object' && c.hasOwnProperty('sel')) { // It shows that calling h function is form â‘¢ // That is, the incoming c is the only children You do not need to execute c because c is already executed in the test statement return vnode(sel, data, [c], undefined, undefined); } else { throw new Error('The parameter type passed in is incorrect'); } };
updateChildren.js
import patchVnode from "./patchVnode"; import createElement from "./createElement"; // Determine whether it is the same virtual node function checkSameVnode(vnodeA, vnodeB) { return vnodeA.sel === vnodeB.sel && vnodeA.key === vnodeB.key; } export default function updateChildren(parentElm, oldCh, newCh) { // console.log('I'm updateChildren ') // console.log(oldCh, newCh) // Define old, new, old and new numbers let oldStartIdx = 0, newStartIdx = 0, oldEndIdx = oldCh.length - 1, newEndIdx = newCh.length - 1; // Define old, old, new and new nodes let oldStartVnode = oldCh[oldStartIdx], oldEndVnode = oldCh[oldEndIdx], newStartVnode = newCh[newStartIdx], newEndVnode = newCh[newEndIdx]; let keyMap = null; // Start big while while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // First, it is not to judge â‘ â‘¡ â‘¢ â‘£ hit, but to skip what has been marked undefined if (oldStartVnode == null || oldCh[oldStartIdx] === undefined) { oldStartVnode = oldCh[++oldStartIdx]; } else if (oldEndVnode == null || oldCh[oldEndIdx] === undefined) { oldEndVnode = oldCh[--oldEndIdx]; } else if (newStartVnode == null || newCh[newStartIdx] === undefined) { newStartVnode = newCh[++newStartIdx]; } else if (newEndVnode == null || newCh[newEndIdx] === undefined) { newEndVnode = newCh[--newEndIdx]; } else if (checkSameVnode(newStartVnode, oldStartVnode)) { // New and old console.log("â‘ New and old hits") patchVnode(oldStartVnode, newStartVnode); newStartVnode = newCh[++newStartIdx]; oldStartVnode = oldCh[++oldStartIdx]; } else if (checkSameVnode(newEndVnode, oldEndVnode)) { // New and old console.log("â‘¡New and old hits") patchVnode(oldEndVnode, newEndVnode); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (checkSameVnode(newEndVnode, oldStartVnode)) { // New and old console.log("â‘¢New and old hits") patchVnode(oldStartVnode, newEndVnode); // When â‘¢ new and old hits, move the node at this time. Move the node pointed to before the new node to the back of the old node // How to move a node?? As long as you insert a node that is already on the DOm tree, it will be moved parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling); newEndVnode = newCh[--newEndIdx]; oldStartVnode = oldCh[++oldStartIdx]; } else if (checkSameVnode(newStartVnode, oldEndVnode)) { // New front and old back console.log("â‘£New front and old rear hits") patchVnode(oldEndVnode, newStartVnode); // â‘£ when the new front and old rear hit, move the node at this time. Move the node pointed by the new node to the front of the old node // How to move a node?? As long as you insert a node that is already on the DOm tree, it will be moved parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; oldEndVnode = oldCh[--oldEndIdx]; } else { // None of the four hits hit // Make the map mapping object of the key so that you don't have to traverse the old object every time. if (!keyMap) { keyMap = {}; // From oldStartIdx to oldEndIdx, create a KeyMap mapping object for (let i = oldStartIdx; i <= oldEndIdx; i++) { const key = oldCh[i].key; if (key !== undefined) keyMap[key] = i; } } //console.log(keyMap); // Find the position sequence number of the mapping of the current item (nextStartIdx) in the keyMap const idxInOld = keyMap[newStartVnode.key]; // console.log(idxInOld); if (idxInOld === undefined) { // Judge that if idxInOlx is undefined, it means it is a new item and only needs to be inserted // The added item (newStartVnode) is not a real DOM node at present parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm); } else { // If it is not undefined, it needs to be moved const elmToMove = oldCh[idxInOld]; patchVnode(elmToMove, newStartVnode); // Setting this item to undefined indicates that this item has been processed oldCh[idxInOld] = undefined; // To move, you can also call insertBefore to move parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm); } // Move the pointer down to move only the new head newStartVnode = newCh[++newStartIdx]; } } // After the while is finished, you need to continue to see if there are any remaining nodes to judge whether you need to delete or add nodes if (newStartIdx <= newEndIdx) { // console.log('new, there are still remaining nodes that have not been processed, and items should be added. To insert all remaining nodes before oldStart '); // After the loop ends, start is still smaller than old // before is the inserted benchmark // const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; // Traverse the new newCh and add it to the old one without processing for (let i = newStartIdx; i <= newEndIdx; i++) { // The insertBefore method can automatically identify null. If it is null, it will automatically queue at the end of the queue. It's consistent with appendChild. // newCh[i] there is no real DOM yet, so you need to call the createElement() function to change it into dom // parentElm.insertBefore(createElement(newCh[i]), before); parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm); } } else if (oldStartIdx <= oldEndIdx) { // console.log('old has remaining nodes that have not been processed, items to be deleted ') // Batch delete items between oldStart and oldEnd pointers for (let i = oldStartIdx; i <= oldEndIdx; i++) if (oldCh[i]) parentElm.removeChild(oldCh[i].elm); } };