从零开始解释并创建了一个简单的虚拟 DOM。
抱歉我的英语写得这么差,我不会说英语。
当我第一次听说虚拟DOM时,我很好奇它是如何工作的,以及如何创建自己的虚拟DOM。经过一番研究和实践后,我将展示我创建的虚拟DOM。
什么是 dom?
文档对象模型 (DOM) 是一种以结构化层次方式表示网页的方法,使程序员和用户能够更轻松地浏览文档。借助 DOM,我们可以使用文档对象提供的命令或方法轻松访问和操作标签、ID、类、属性或元素。
为什么称之为对象模型?
文档是使用对象建模的,该模型不仅包括文档的结构,还包括文档的行为以及构成文档的对象,例如 HTML 中带有属性的标签元素。
DOM结构:
DOM 可以被视为一棵树或一片森林(多棵树)。术语“结构模型”有时用来描述文档的树状表示。DOM 结构模型的一个重要特性是结构同构:如果使用任意两种 DOM 实现来创建同一文档的表示,它们将创建相同的结构模型,包含完全相同的对象和关系。
什么是虚拟DOM?
虚拟 DOM 是对象中真实 DOM 元素的内存表示。例如:
const myButton = {
tagName: 'button',
attrs: {
id: 'btn',
class: 'save-btn'
},
children: ['save']
};
HTML 等效项
<button id="btn" class="save-btn">save</button>
了解了这些之后,我们开始吧😊
我们需要一个函数来创建一个表示元素的对象并返回该对象。
// createElement.js
function createElement(tagName, { attrs = {}, children = [] } = {}){
return {
tagName,
attrs,
children
}
}
export default createElement;
现在我们需要创建一个函数来渲染元素。
// render.js
function render({ tagName, attrs = {}, children = [] }){
let element = document.createElement(tagName);
// insert all children elements
children.forEach( child => {
if (typeof child === 'string'){
// if the children is a kind of string create a text Node object
element.appendChild(document.createTextNode(child));
}
else {
// repeat the process with the children elements
element.appendChild(render(child));
}
});
// if it has attributes it adds them to the element
if (Object.keys(attrs).length){
for (const [key, value] of Object.entries(attrs)) {
element.setAttribute(key, value);
}
}
return element;
};
export default render;
然后创建一个函数将该元素插入到 DOM 中。
// insert.js
function insertElement(element, domElement){
domElement.replaceWith(element);
return element;
}
export default insertElement;
现在我们有了工具,让我们来试试吧!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>my vDOM</title>
</head>
<body>
<div id="root">
</div>
<script src="./main.js" type="module"></script>
</body>
</html>
// main.js
import createElement from './createElement.js';
import render from './render.js';
import insertElement from './insert.js';
let myVirtualElement = createElement("div", {
attrs: { id: "container" },
children: [
createElement("p", {
attrs: { id: "text" },
children: ["hello world"],
}),
]
});
let element = render(myVirtualElement);
let rootElemet = insertElement(element, document.querySelector('#root'));
这段代码可以在任何 Web 服务器上运行,我是在 VS Code 中用 Live Server 运行的。
我们成功了!🥳
现在我们可以让它更有趣,采用Jason Yu在这篇文章中创建的算法来区分虚拟元素。
// diff.js
import render from './render.js';
const zip = (xs, ys) => {
const zipped = [];
for (let i = 0; i < Math.max(xs.length, ys.length); i++) {
zipped.push([xs[i], ys[i]]);
}
return zipped;
};
const diffAttrs = (oldAttrs, newAttrs) => {
const patches = [];
// set new attributes
for (const [k, v] of Object.entries(newAttrs)) {
patches.push($node => {
$node.setAttribute(k, v);
return $node;
});
}
// remove old attributes
for (const k in oldAttrs) {
if (!(k in newAttrs)) {
patches.push($node => {
$node.removeAttribute(k);
return $node;
});
}
}
return $node => {
for (const patch of patches) {
patch($node);
}
};
};
const diffChildren = (oldVChildren, newVChildren) => {
const childPatches = [];
oldVChildren.forEach((oldVChild, i) => {
childPatches.push(diff(oldVChild, newVChildren[i]));
});
const additionalPatches = [];
for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
additionalPatches.push($node => {
$node.appendChild(render(additionalVChild));
return $node;
});
}
return $parent => {
for (const [patch, child] of zip(childPatches, $parent.childNodes)) {
patch(child);
}
for (const patch of additionalPatches) {
patch($parent);
}
return $parent;
};
};
const diff = (vOldNode, vNewNode) => {
if (vNewNode === undefined) {
return $node => {
$node.remove();
return undefined;
};
}
if (typeof vOldNode === 'string' || typeof vNewNode === 'string') {
if (vOldNode !== vNewNode) {
return $node => {
const $newNode = render(vNewNode);
$node.replaceWith($newNode);
return $newNode;
};
} else {
return $node => undefined;
}
}
if (vOldNode.tagName !== vNewNode.tagName) {
return $node => {
const $newNode = render(vNewNode);
$node.replaceWith($newNode);
return $newNode;
};
}
const patchAttrs = diffAttrs(vOldNode.attrs, vNewNode.attrs);
const patchChildren = diffChildren(vOldNode.children, vNewNode.children);
return $node => {
patchAttrs($node);
patchChildren($node);
return $node;
};
};
export default diff;
现在我们来修改 main.js
// main.js
import createElement from './createElement.js';
import render from './render.js';
import insertElement from './insert.js';
import diff from './diff.js';
let myElement = createElement('div', {
attrs: { class: 'container'},
children: [createElement('img', {
attrs: { id: 'img', src: 'https://i.picsum.photos/id/1/200/300.jpg' },
children: []
})]
})
let element = render(myElement);
let rootElemet = insertElement(element, document.querySelector('#root'));
let count = 0;
setInterval(()=> {
count += 1;
let myVirtualElemet = createElement('div', {
attrs: { class: 'img'},
children: [createElement('img', {
attrs: { id: 'img', src: `https://i.picsum.photos/id/${count}/200/300.jpg` },
children: []
})]
})
const patch = diff(myElement, myVirtualElemet);
rootElemet = patch(rootElemet);
myElement = myVirtualElemet;
}, 1000);
运行它🤞
我们成功了!🥳
我们每秒都会用新的 id 更改链接中的 src 属性,以便更新 DOM 并应用更改。
