Environment Setup
- Create a folder, go to the folder directory, and use NPM init-y to generate the package.json file.
Download webpack, webpack-cli, webpack-dev-server, with particular attention to the various versions of webpack.
Download the corresponding version
cnpm i webpack@5 webpack-cli@3 webpack-dev-server@3 -S
- Create webpack.config.js file
module.exports = { entry: { index: './src/index.js' }, output: { path: __dirname + '/public', filename: './js/[name].js' }, devServer: { contentBase: './public', inline: true } }
- Create a file based on the contents of the webpack.config.js file
File directory after creation
- Introducing a js file into the index.html file
- Run the program
Run commands need to be added to the package.json file
{ "name": "diff", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "webpack-dev-server" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "webpack": "^5.52.1", "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.2" } }
- Run the program
npm run dev
Create h Function
- Create a new folder dom under src, create a new file h.js
import vnode from './vnode' export default function(sel, data, params) { // console.log(sel, data, params) // Without child elements if(typeof params === 'string') { return vnode(sel, data, undefined, params, undefined) } else if(Array.isArray(params)) { // With child elements let children = [] for(let item of params) { children.push(item) } return vnode(sel, data, children, undefined, undefined) } }
- Create a new vnode.js under the dom folder, which converts a real dom into a virtual dom
export default function(sel, data, children, text, elm) { /** * sel: Virtual dom * data: key * children: Child Elements * text: Text Content * elm: True dom */ return { sel, data, children, text, elm } }
- Create virtual dom in index.js
import h from './dom/h' let vnode1 = h("div", {}, "How do you do"); console.log(vnode1) let vnode2 = h("ul", {}, [ h("li", {}, "a"), h("li", {}, "b"), h("li", {}, "c"), h("li", {}, "d"), h("li", {}, "e"), ]); console.log(vnode2)
- Run Results
Turn a real dom into a virtual dom and compare the old and new virtual nodes
- If the old and new nodes are not the same node name, then the old node is deleted and the new node is inserted violently for a long time.
1. Write a new div under index.html
<div id="container">This is container</div>
2. Get the div real dom node under index.html under index.js
// Get the real dom node const container = document.getElementById('container')
3. Create a virtual node to patch the virtual node onto the real dom
// Virtual Node let vnode1 = h("h1", {}, "How do you do"); // console.log(vnode1) let vnode2 = h("ul", {}, [ h("li", {}, "a"), h("li", {}, "b"), h("li", {}, "c"), h("li", {}, "d"), h("li", {}, "e"), ]); patch(container, vnode1)
4. Create a new patch.js function to convert a real DOM into a virtual dom, and then compare the new virtual DOM with the old one.
import vnode from './vnode' import createElement from './createElement' /** * @param {*} oldVnode Old Virtual Node * @param {*} newVnode New Virtual Node */ export default function (oldVnode, newVnode) { // If oldVnode has no sel, it is a non-virtual node if (oldVnode.sel === undefined) { // console.log(oldVnode.tagName.toLowerCase()) oldVnode = vnode( oldVnode.tagName.toLowerCase(), //Virtual Node Name {}, [], undefined, oldVnode // Real dom elements ) } // console.log(oldVnode) // Start judging old and new virtual nodes if (oldVnode.sel !== newVnode.sel) { // If the old and new nodes are not the same node name, then the old node is deleted and the new node is inserted violently for a long time. // Create a new virtual node as a real dom node let newVnodeElm = createElement(newVnode) // It's a good idea to write a function that creates a real dom for recursive calls // console.log(newVnodeElm) let oldVnodeElm = oldVnode.elm if(newVnodeElm) { oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm) } oldVnodeElm.parentNode.removeChild(oldVnodeElm) } else { // Is the same node, logic becomes complex } }
5. Create a new createElement.js file to convert virtual nodes into real nodes and mount them on the real dom
export default function createElement (vnode) { let domNode = document.createElement(vnode.sel) // Determine if there are children if(vnode.children === undefined) { domNode.innerText = vnode.text }else if(Array.isArray(vnode.children)) { for(let child of vnode.children) { let childDom = createElement(child) domNode.appendChild(childDom) } } vnode.elm = domNode return domNode }
- If the nodes are the same, there are several cases
- (1) The new node has no children and is replaced directly
- (2) The new node has children, the old node has children, and the diff algorithm core
- (3) New node has children, old node does not, directly create element add
Create a new patchVnode.js file to determine if the old and new virtual nodes are the same
import createElement from './createElement' export default function(oldVnode, newVnode) { // Determine if a new node has child elements if(newVnode.children === undefined) { // No child elements if(newVnode.text !== oldVnode.text) { oldVnode.elm.innerText = newVnode.text } } else { // Has child elements // Determine if the old node has child elements if(oldVnode.children && oldVnode.children.length > 0) { // Old node has child elements, which is more complex } else { // Old node has no child elements oldVnode.elm.innerHTML = '' // Delete the contents of old nodes for(let child of newVnode.children) { let childDom = createElement(child) oldVnode.elm.appendChild(childDom) } console.log(oldVnode.elm) } } }
patch.js file
import vnode from './vnode' import createElement from './createElement' import patchVnode from './patchVnode' /** * @param {*} oldVnode Old Virtual Node * @param {*} newVnode New Virtual Node */ export default function (oldVnode, newVnode) { // If oldVnode has no sel, it is a non-virtual node if (oldVnode.sel === undefined) { // console.log(oldVnode.tagName.toLowerCase()) oldVnode = vnode( oldVnode.tagName.toLowerCase(), //Virtual Node Name {}, [], undefined, oldVnode // Real dom elements ) } // console.log(oldVnode) // Start judging old and new virtual nodes if (oldVnode.sel !== newVnode.sel) { // If the old and new nodes are not the same node name, then the old node is deleted and the new node is inserted violently for a long time. // Create a new virtual node as a real dom node let newVnodeElm = createElement(newVnode) // It's a good idea to write a function that creates a real dom for recursive calls // console.log(newVnodeElm) let oldVnodeElm = oldVnode.elm if(newVnodeElm) { oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm) } oldVnodeElm.parentNode.removeChild(oldVnodeElm) } else { // Is the same node, logic becomes complex patchVnode(oldVnode, newVnode) } }
index.js file
import h from './dom/h' import patch from './dom/patch' // Virtual Node // let vnode1 = h("div", {}, "hello"); let vnode1 = h("div", {}, [ h("span", {}, "a"), h("span", {}, "b"), h("span", {}, "c"), h("span", {}, "d"), h("span", {}, "e"), ]); // Get the real dom node const container = document.getElementById('container') patch(container, vnode1)
diff algorithm core
- 1. Old and New
Match: Old Front Pointer++, New Front Pointer++. - 2. Old and New
Match: Old Post Pointer -, New Post Pointer -. - 3. Before and after the old
Match: Old Front Pointer++, New Back Pointer- - 4. After and Before the Old
Match: Old Back Pointer - New Front Pointer+. - 5. None of the above meets the criteria=== to find
Find: New pointer++, renders the element pointed to by the new pointer to the page, and assigns undefined to the corresponding element found in the old node - 6. Create or delete
Create a new updateChildren.js to handle five cases in the diff algorithm
import patchVnode from './patchVnode' import createElement from './createElement' // Determine whether two virtual nodes are the same node function sameVnode(vnode1, vnode2) { return vnode1.key === vnode2.key } /** * * @param {*} parentElm Old parent node * @param {*} oldCh Old Child Node * @param {*} newCh New Child Node */ export default function(parentElm, oldCh, newCh) { // console.log(parentElm, oldCh, newCh) let oldStartIdx = 0 // Old Front Pointer let oldEndIdx = oldCh.length - 1 // Old back pointer let newStartIdx = 0 // New Front Pointer let newEndIdx = newCh.length - 1 // New Post Pointer let oldStartVnode = oldCh[0] // Old Former Virtual Node let oldEndVnode = oldCh[oldEndIdx] // Old Post Virtual Node let newStartVnode = newCh[0] // New Front Virtual Node let newEndVnode = newCh[newEndIdx] // New Post Virtual Node while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if(oldStartVnode === undefined) { oldStartVnode = [++oldStartIdx] } if(oldEndVnode === undefined) { oldEndVnode = [--oldEndIdx] }else if(sameVnode(oldStartVnode, newStartVnode)) { // Old and New console.log(1) patchVnode(oldStartVnode, newStartVnode) if(newStartVnode) { newStartVnode.elm = oldStartVnode?.elm } oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }else if(sameVnode(oldEndVnode, newEndVnode)) { // Old and New console.log(2) patchVnode(oldEndVnode, newEndVnode) if(oldEndVnode) { newEndVnode.elm = oldEndVnode?.elm } oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] }else if(sameVnode(oldStartVnode, newEndVnode)) { // Before and after console.log(3) patchVnode(oldStartVnode, newEndVnode) if(oldStartVnode) { newEndVnode.elm = oldStartVnode?.elm } // Move the previously specified node behind the old pointed node parentElm.insertBefore( oldStartVnode.elm, oldEndVnode.elm.nextSibling ) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if(sameVnode(oldEndVnode, newStartVnode)) { // Old behind and new before console.log(4) patchVnode(oldEndVnode, newStartVnode) if(oldEndVnode) { newStartVnode.elm = oldEndVnode?.elm } // Moves the old specified node to the front of the old pointed node parentElm.insertBefore( oldEndVnode.elm, oldStartVnode.elm ) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // None of the above satisfies the criteria=== search console.log(5) // Create an object storage virtual node let keyMap = {} for(let i = oldStartIdx;i <= oldEndIdx;i++) { const key = oldCh[i]?.key if(key) { keyMap[key] = i } } // console.log(keyMap) // Finding the Node to which the Front Node Points in the Old Node let indxInOld = keyMap[newStartVnode.key] if(indxInOld) { // Can find const elmMove = oldCh[indxInOld] patchVnode(elmMove, newStartVnode) // Processed node, set to undefined in the array of old virtual nodes oldCh[indxInOld] = undefined parentElm.insertBefore(elmMove.elm, oldStartVnode.elm) }else { // Can't find // Description is a new node and needs to be recreated parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm) } // New Node Pointer+1 newStartVnode = newCh[++newStartIdx] } } }
Reference updateChildren method in patchVnode.js
import createElement from './createElement' import updateChildren from './updateChildren' export default function patchVnode (oldVnode, newVnode) { // console.log(oldVnode) // Determine if a new node has child elements if (newVnode.children === undefined) { // No child elements if (newVnode.text !== oldVnode.text) { oldVnode.elm.innerText = newVnode.text } } else { // Has child elements // Determine if the old node has child elements if (oldVnode.children && oldVnode.children.length > 0) { // Old node has child elements, which is more complex updateChildren(oldVnode.elm, oldVnode.children, newVnode.children) // console.log(oldVnode.elm, oldVnode.children, newVnode.children) } else { // Old node has no child elements oldVnode.elm.innerHTML = '' // Delete the contents of old nodes for (let child of newVnode.children) { let childDom = createElement(child) oldVnode.elm.appendChild(childDom) } // console.log(oldVnode.elm) } } }
- Create or delete nodes
Add the following code to updateChild.js
// There are only two cases to end while (add and delete) // 1,oldStartIdx > oldEndIdx // 2,newStartIdx > newEndIdx if(oldStartIdx > oldEndIdx) { const before = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null for(let i = newStartIdx;i <= newEndIdx;i++) { parentElm.insertBefore((createElement(newCh[i])), before) } } else { // Enter Delete Operation for(let i = oldStartIdx;i <= oldEndIdx;i++) { // console.log(oldCh[i]) parentElm.removeChild(oldCh[i].elm) } }
Reference
https://www.bilibili.com/video/BV1K64y1s7ot?p=9&spm_id_from=pageDriver
All Code
gitee address https://gitee.com/jjm1/diff