Vue’s virtual DOM and Diff algorithm adds a lot of annotations and a lot of rewriting, Add many illustrations to explain the diff algorithm chapter 8 elaborates on some of the more ambiguous places the teacher said source link gitee.com/ykang2020/v…
0. Preview of article structure
0.1 How is the virtual DOM generated by the render function (h function)
Objective: Handwriting h function
0.2 Principle of diFF algorithm
Target: handwritten diff algorithm
0.3 How does the virtual DOM become the real DOM through diff
1. Introduction
1.1 the diff algorithm
Fine comparison
1.2 virtual DOM
Not in this courseHow does the DOM become a virtual DOMThis is part of the template compilation principle butVirtual nodes become DOM nodesYou can do that in diff
1.3 Relationships — Diff happens in the virtual DOM
Diff the new virtual DOM and the old virtual DOM to figure out how to minimize the number of updates that are reflected in the real DOM
2. Introduction and preparation of SNabbDOM
2.1 introduction
Snabbdom (Swedish, “speed”) is a well-known virtual DOM library, the ancestor of the Diff algorithm Vue source code reference snabbDOM source using TypeScript github.com/snabbdom/sn… Download the build version of JavaScript from NPM
npm install -D snabbdom
Copy the code
2.2 Setting up the initial Environment
1. Install snabbdom
cnpm install -S snabbdom
Copy the code
2. Install webPack5 and configure it
cnpm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
Copy the code
Configuration webpack5
module.exports = {
// Webpack5 does not need to configure mode
/ / the entry
entry: "./src/index.js"./ / export
output: {
// Virtual package path, folder is not actually generated, but virtual generated on port 8080
publicPath: "xuni".// The name of the packaged file
filename: "bundle.js",},/ / configuration webpack - dev - server
devServer: {
// Static root directory
contentBase: 'www'./ / the port number
port: 8080,}};Copy the code
3. Copy the official demo Example
src/index.js
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
]);
const container = document.getElementById("container");
const vnode = h("div#container.two.classes", { on: { click: function () { } } }, [
h("span", { style: { fontWeight: "bold"}},"This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo"}},"I'll take you places!"),]);// Patch into empty DOM element -- This modifies the DOM as a side effect
patch(container, vnode);
const newVnode = h(
"div#container.two.classes",
{ on: { click: function () { } } },
[
h(
"span",
{ style: { fontWeight: "normal".fontStyle: "italic"}},"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar"}},"I'll take you places!"),]);// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
Copy the code
3. Introduction and use of H function
3.1 introduction
Used to generate virtual nodes (VNodes)Attributes of a vnode
{
children: undefined// Array of child elements
data: {} // Attributes, styles, keys
elm: undefined // The corresponding real DOM node (object), undefined means the node is not yet in the DOM tree
key: // Unique identifier
sel: "" / / selector
text: "" // Text content
}
Copy the code
3.2 Creating a Virtual Node using the h function
// Create a virtual node
var myVnode1 = h('a', { props: { href: 'https://www.baidu.com'}},'YK bacteria')
console.log(myVnode1)
Copy the code
3.3 Use the patch function to add the DOM tree on the virtual node
// Create patch function
const patch = init([
classModule,
propsModule,
styleModule,
eventListenersModule,
]);
// Create a virtual node
var myVnode1 = h(
"a",
{ props: { href: "https://www.baidu.com".target: "_blank"}},"YK bacteria"
);
// Put the virtual node on the tree
let container = document.getElementById("container");
patch(container, myVnode1);
Copy the code
3.4h Function nested use, get virtual DOM tree (important)
const myVnode3 = h('ul', [
h('li'.'apple'),
h('li'.'banana'),
h('li'.'watermelon'),
h('li'.'tomato'),])// Put the virtual node on the tree
let container = document.getElementById("container");
patch(container, myVnode3);
Copy the code
const myVnode3 = h('ul', [
h('li'.'apple'),
h('li', [
h('div', [
h('p'.'banana'),
h('p'.'strawberry')
])
]),
h('li', h('span'.'watermelon')),
h('li'.'tomato'),])Copy the code
Write h functions by hand
vnode.js
Returns the passed parameters as an object
/** * produces a virtual node * returns * by combining the passed parameters into objects@param {string} Sel selector *@param {object} Data attribute, style *@param {Array} Children *@param {string|number} Text Indicates the text content *@param {object} The real DOM node (object) corresponding to ELM, undefined means that the node is not on the DOM tree *@returns * /
export default function(sel, data, children, text, elm) {
const key = data.key;
return { sel, data, children, text, elm, key };
}
Copy the code
h.js
Generates a virtual DOM tree that returns an object
import vnode from "./vnode";
/** * generates a virtual DOM tree, which returns an object * a lower-configured version of the h function, which must take three arguments@param {*} sel
* @param {*} data
* @param {*} c* call only three form text, array, function h * (1) h (' div ', {}, 'text') * (2) h (' div ', {}, []) * (3) h (' div ', {}, h ()) * /
export default function (sel, data, c) {
// Check the number of parameters
if (arguments.length ! = =3) {
throw new Error("Please pass only three parameters!");
}
// Check the type of the third parameter c
if (typeof c === "string" || typeof c === "number") {
// The description is now a text
return vnode(sel, data, undefined, c, undefined);
} else if (Array.isArray(c)) {
// Indicates array ②
let children = [];
// iterate over the C array
for (let item of c) {
if(! (typeof item === "object" && item.hasOwnProperty("sel"))) {
throw new Error("The array passed in has items that are not functions of H.");
}
// Instead of executing item, collect every object in the array
children.push(item);
}
return vnode(sel, data, children, undefined.undefined);
} else if (typeof c === "object" && c.hasOwnProperty("sel")) {
// ③ h is an object (the return value of h is an object)
let children = [c];
return vnode(sel, data, children, undefined.undefined);
} else {
throw new Error("Wrong type of argument passed!"); }}Copy the code
index.js
import h from "./my_snabbdom/h";
const myVnode1 = h("div", {}, [
h("p", {}, "Hee hee"),
h("p", {}, "Ha ha"),
h("p", {}, h('span', {}, '呵呵')));console.log(myVnode1);
Copy the code
Results show
5. Handwritten diff algorithm preparation
5.1 Principle of diFF algorithm
Minimum update, key is key. The key is the unique identifier of the node, telling the Diff algorithm that they are the same DOM node before and after the change.
Only the same virtual node is used for fine comparison (add LI to UL li), otherwise it is violent to delete old li and insert new li (replace UL Li to OL).
Question: How to define the same virtual node? A: Same selectors and same keys
Comparisons are made within layers, not across layers. Even if it is the same virtual node, but across layers, diff is the violent deletion of old nodes and the insertion of new ones
5.2 Handwritten Diff preparation
5.2.1 How to define “One Node” in the source Code
5.2.2 Creating child nodes in the source code requires recursion
6. Handwritten diff — First time on a DOM treepatch(container, myVnode1)
6.0 DOM preparation
The 6.0.1 Node. The insertBefore ()
var insertedNode = parentNode.insertBefore(newNode, referenceNode);
Copy the code
ParentNode: indicates the parentNode of the newNode. NewNode: indicates the node used for insertion. ReferenceNode: NewNode will be inserted before this Node, adding a child Node Node under the current Node and placing the child Node before the reference Node.
6.0.2 Node. The appendChild ()
element.appendChild(aChild)
Copy the code
Appends a node to the end of the list of children of the specified parent node. If the node to be inserted already exists in the document tree of the current document, appendChild() will simply move it from its original location to the new location (no need to remove the node to be moved beforehand).
6.0.3 Element. TagName
Returns the label name of the current element
elementName = element.tagName
Copy the code
ElementName is a string that contains the tag name of the Element. In an HTML document, the tagName is returned in uppercase
6.0.4 Node. RemoveChild
Removes a child node from the DOM. Returns the deleted node
let oldChild = node.removeChild(child);
//OR
element.removeChild(child);
Copy the code
Child is the child to be removed. Node is the parent of the child. OldChild holds a reference to the deleted child. OldChild === child.
6.0.5/6.5.4 document. The createElement method
var element = document.createElement(tagName[, options]);
Copy the code
TagName: This method does not allow qualified names (e.g., “HTML :a”), Calling createElement() on an HTML document converts the tagName to lowercase before creating the element. In the Firefox, Opera, and Chrome kernels, CreateElement (null) equals createElement(“null”) returns the newly created Element(Element)
The 6.1 patch. Js
import vnode from "./vnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
// Determine whether the first argument passed in is a DOM node or a virtual node
if (oldVnode.sel == "" || oldVnode.sel === undefined) {
// oldVnode is a DOM node. In this case, it must be wrapped as a virtual node
oldVnode = vnode(
oldVnode.tagName.toLowerCase(), // sel
{}, // data[].// children
undefined.// text
oldVnode // elm
);
}
// Check whether oldVnode and newVnode are the same node
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
console.log("It's the same node, it needs to be refined.");
} else {
console.log("Not the same node, forcibly insert new node, delete old node.");
// Create a new virtual node as a DOM node
// To manipulate the DOM, both are converted to DOM nodes
let newVnodeElm = createElement(newVnode);
let oldVnodeElm = oldVnode.elm;
// Insert the new node before the old node
if (newVnodeElm) {
// Check that newVnodeElm exists and insert a new node before the old one
oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm);
}
// Delete the old nodeoldVnodeElm.parentNode.removeChild(oldVnodeElm); }}Copy the code
6.2 createElement method. Js
Create a node. Create a VNode as a DOM node
/** * Create a node. Create a VNode virtual node as a DOM node * that is orphan and does not insert *@param {object} vnode* /
export default function createElement(vnode) {
// Create a DOM node based on the sel selector property of the virtual node. This node is now an orphan node
let domNode = document.createElement(vnode.sel);
// Determine whether there are child nodes or text
if( vnode.text ! = ="" &&
(vnode.children === undefined || vnode.children.length === 0)) {// Indicates that there are no child nodes
domNode.innerText = vnode.text;
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// The inner node is a child node, and the node needs to be created recursively
// go through the number group
for (let ch of vnode.children) {
A recursive call to createElement creates its DOM. A call to createElement means the DOM is created. And its ELM attribute points to the created DOM, but it's not on the tree, so it's an orphan node
let chDOM = createElement(ch); // Return a text node
console.log(ch);
// domNode tree on text nodedomNode.appendChild(chDOM); }}// Add the ELM attribute for the virtual node
vnode.elm = domNode;
// Return the domNode DOM object
return domNode;
}
Copy the code
6.3 the index. Js
import h from "./my_snabbdom/h";
import patch from "./my_snabbdom/patch";
let container = document.getElementById("container");
let btn = document.getElementById("btn");
// const myVnode1 = h("h1", {}, "hello ");
const myVnode1 = h("ul", {}, [
h("li", {}, "A"),
h("li", {}, "B"),
h("li", {}, "C"),
h("li", {}, "D"),]);/ / on the tree
patch(container, myVnode1);
Copy the code
6.4 Display Effect
7. Handle the situation that the old and new nodes are the same node
7.1 analysis
7.2 Different text situations of new and old nodes are realized
7.2.1 patch. Js
// Check whether oldVnode and newVnode are the same node
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
console.log("It's the same node, it needs to be refined.");
patchVnode(oldVnode, newVnode);
}
Copy the code
7.2.3 patchVnode. Js
export default function patchVnode(oldVnode, newVnode) {
// 1. Check whether the old and new VNodes are the same object
if (oldVnode === newVnode) return;
// 2. Check whether newVndoe has a text attribute
if( newVnode.text ! = =undefined &&
(newVnode.children === undefined || newVnode.children.length === 0)) {// newVnode has a text attribute
2.1 Check whether the text attribute of newVnode is the same as that of oldVnode
if(newVnode.text ! == oldVnode.text) {// If the text in newVnode is different from the text in oldVnode, just write the new text to the old ELM.
// If oldVnode is children, it will also disappear immediatelyoldVnode.elm.innerText = newVnode.text; }}else {
// newVnode has no text property and children property
// 2.2 Check whether oldVnode has the children attribute
if(oldVnode.children ! = =undefined && oldVnode.children.length > 0) {
// oldVnode has children attribute most complex case, new and old nodes have children
} else {
// oldVnode does not have children. NewVnode has the children attribute
// Clear the contents of oldVnode
oldVnode.elm.innerHTML = "";
// Traverses the children of the new vNode virtual node, creating the DOM, and ascending the tree
for (let ch of newVnode.children) {
letchDOM = createElement(ch); oldVnode.elm.appendChild(chDOM); }}}}Copy the code
7.2.2 index. Js
import h from "./my_snabbdom/h";
import patch from "./my_snabbdom/patch";
let container = document.getElementById("container");
let btn = document.getElementById("btn");
const myVnode1 = h("h1", {}, "Hello");
/ / on the tree
patch(container, myVnode1);
const myVnode2 = h("ul", {}, [
h("li", {}, "A"),
h("li", {}, "B"),
h("li", {}, "C"),
h("li", {}, "D"),]); btn.onclick =function () {
patch(myVnode1, myVnode2);
}
Copy the code
7.3 show
8. Analyze diff algorithm to update child node operations (important)
Here teacher PPT problem, ③ should not be new before but after
8.1 Circulating the four matching searches
1. Compare ① newStart with oldStart
If ① is hit, the header pointer newStart++ oldStart++ is moved after patch
if (checkSameVnode(oldStartVnode, newStartVnode)) {
// The new and the old
console.log("①1 New and old hit");
OldStartVnode is now the same as newStartVnode
patchVnode(oldStartVnode, newStartVnode);
// Move the pointer to change the node to which the pointer points, indicating that both nodes are finished
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}
Copy the code
And if you don’t, then you go to the next situation
2. Compare ② newEnd with oldEnd
If ② is hit, newEnd– oldEnd– is moved after patch
if (checkSameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
}
Copy the code
And if you don’t, then you go to the next situation
3. Compare ③ newEnd with oldStart
If ③ is hit, move the node pointed to by the new post-newend after the old post-oldEnd
if (checkSameVnode(oldStartVnode, newEndVnode)) {
// The new and the old
console.log("③3 The new queen matches the old one");
patchVnode(oldStartVnode, newEndVnode);
// When ③ new and old hit, move the node. Move the node pointed to by the new node to the node behind the old node
// Move node: Whenever a node is inserted that is already in the DOM tree, it will be moved
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}
Copy the code
Hit ③ complex case example — reverse order
And if you don’t, then you go to the next situation
4. Compare ④ newStart with oldEnd
If ④ is hit, move the node pointed to before oldStart to before oldStart
if (checkSameVnode(oldEndVnode, newStartVnode)) {
// New before and old after
console.log("④4 New before and old after hit");
patchVnode(oldEndVnode, newStartVnode);
// When ④ new front and old rear hit, the node should be moved. Move the node pointed to by the new front (old back) to the old front of the old node
// Move node: Whenever a node is inserted that is already in the DOM tree, it will be moved
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
Copy the code
If you miss, you miss in all four cases
5. None of the four types will traverse keys in an oldVnode
If you find it, move it. Move the pointer newStart++What is not found is the new node, which is inserted directly before all the unprocessed old nodes
// None of the four types are matched
console.log("All four missed.");
// Find a mapping object in keyMap, so you don't have to iterate over old objects every time
if(! keyMap) { keyMap = {};// Record the key of the node in oldVnode
// Create keyMap from oldStartIdx to oldEndIdx
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key;
if(key ! = =undefined) { keyMap[key] = i; }}}console.log(keyMap);
// Find the sequence number of the current item (newStartIdx) mapped in keyMap
const idxInOld = keyMap[newStartVnode.key];
if (idxInOld === undefined) {
// If idxInOld is undefined, it is a new entry and needs to be inserted
// The added item (newStartVnode) is not a real DOM node
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
} else {
// indicates that the item is not new and needs to be moved
const elmToMove = oldCh[idxInOld];
patchVnode(elmToMove, newStartVnode);
// Set this item to undefined to indicate that I am done with this item
oldCh[idxInOld] = undefined;
// Move, insertBefore can also move.
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
}
// newStartIdx++;
newStartVnode = newCh[++newStartIdx];
Copy the code
8.2 Loop End
After the end of
- NewVnode has a surplus
The rest of the new nodes areinsertThe old node oldEnd or before oldStart 2. The oldVnode has remaining nodes
// The loop ends
if (newStartIdx <= newEndIdx) {
// Indicates that newVndoe has unprocessed nodes, so these nodes need to be added
for (let i = newStartIdx; i <= newEndIdx; i++) {
// insertBefore automatically recognizes null and automatically goes to the end of the queue, as in appendChildparentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm); }}else if (oldStartIdx <= oldEndIdx) {
// Indicates that the oldVnode has unprocessed nodes, so these nodes need to be deleted
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if(oldCh[i]) { parentElm.removeChild(oldCh[i].elm); }}}Copy the code
9. Update child nodes by hand diff
patchVnode.js
// 2.2 Check whether oldVnode has the children attribute
if(oldVnode.children ! = =undefined && oldVnode.children.length > 0) {
// oldVnode has children attribute most complex case, new and old nodes have children
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
}
Copy the code
updateChildren.js
import createElement from "./createElement";
import patchVnode from "./patchVnode";
/ * * * *@param {object} ParentElm Dom node *@param {Array} OldCh oldVnode's child node array *@param {Array} NewCh An array of children of newVnode */
export default function updateChildren(parentElm, oldCh, newCh) {
console.log("updateChildren()");
console.log(oldCh, newCh);
// Four Pointers
/ / the old before
let oldStartIdx = 0;
/ / new front
let newStartIdx = 0;
/ / after the old
let oldEndIdx = oldCh.length - 1;
/ / new after
let newEndIdx = newCh.length - 1;
// The four nodes to which the pointer points
// Old former node
let oldStartVnode = oldCh[0];
// Old back node
let oldEndVnode = oldCh[oldEndIdx];
// New front node
let newStartVnode = newCh[0];
// New post-node
let newEndVnode = newCh[newEndIdx];
let keyMap = null;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
console.log("** in ** loop");
// The first step is not to judge the four hits, but to skip the items marked with 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(oldStartVnode, newStartVnode)) {
// The new and the old
console.log("①1 New and old hit");
OldStartVnode is now the same as newStartVnode
patchVnode(oldStartVnode, newStartVnode);
// Move the pointer to change the node to which the pointer points, indicating that both nodes are finished
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (checkSameVnode(oldEndVnode, newEndVnode)) {
// New queen and old queen
console.log("②2 New queen and old queen hit");
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newEndVnode)) {
// The new and the old
console.log("③3 The new queen matches the old one");
patchVnode(oldStartVnode, newEndVnode);
// When ③ new and old hit, move the node. Move the node pointed to by the new node to the node behind the old node
// Move node: Whenever a node is inserted that is already in the DOM tree, it will be moved
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldEndVnode, newStartVnode)) {
// New before and old after
console.log("④4 New before and old after hit");
patchVnode(oldEndVnode, newStartVnode);
// When ④ new front and old rear hit, the node should be moved. Move the node pointed to by the new front (old back) to the old front of the old node
// Move node: Whenever a node is inserted that is already in the DOM tree, it will be moved
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// None of the four types are matched
console.log("All four missed.");
// Find a mapping object in keyMap, so you don't have to iterate over old objects every time
if(! keyMap) { keyMap = {};// Record the key of the node in oldVnode
// Create keyMap from oldStartIdx to oldEndIdx
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key;
if(key ! = =undefined) { keyMap[key] = i; }}}console.log(keyMap);
// Find the sequence number of the current item (newStartIdx) mapped in keyMap
const idxInOld = keyMap[newStartVnode.key];
if (idxInOld === undefined) {
// If idxInOld is undefined, it is a new entry and needs to be inserted
// The added item (newStartVnode) is not a real DOM node
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
} else {
// indicates that the item is not new and needs to be moved
const elmToMove = oldCh[idxInOld];
patchVnode(elmToMove, newStartVnode);
// Set this item to undefined to indicate that I am done with this item
oldCh[idxInOld] = undefined;
// Move, insertBefore can also move.
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
}
// newStartIdx++;newStartVnode = newCh[++newStartIdx]; }}// The loop ends
if (newStartIdx <= newEndIdx) {
// Indicates that newVndoe has unprocessed nodes, so these nodes need to be added
// // insert the benchmark
// const before =
// newCh[newEndIdx + 1] === null ? null : newCh[newEndIdx + 1].elm;
for (let i = newStartIdx; i <= newEndIdx; i++) {
// insertBefore automatically recognizes null and automatically goes to the end of the queue, as in appendChildparentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm); }}else if (oldStartIdx <= oldEndIdx) {
// Indicates that the oldVnode has unprocessed nodes, so these nodes need to be deleted
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if(oldCh[i]) { parentElm.removeChild(oldCh[i].elm); }}}}// Check if it is the same node
function checkSameVnode(a, b) {
return a.sel === b.sel && a.key === b.key;
}
Copy the code
Source link gitee.com/ykang2020/v…