要构建自己的虚拟DOM,需要知道两件事。你甚至不需要深入 React 的源代码或者深入任何其他虚拟DOM实现的源代码,因为它们是如此庞大和复杂——但实际上,虚拟DOM的主要部分只需不到50行代码。
![]() 要构建自己的虚拟DOM,需要知道两件事。你甚至不需要深入 React 的源代码或者深入任何其他虚拟DOM实现的源代码,因为它们是如此庞大和复杂——但实际上,虚拟DOM的主要部分只需不到50行代码。 有两个概念:
用JS对象模拟DOM树首先,我们需要以某种方式将 DOM 树存储在内存中。可以使用普通的 JS 对象来做。假设我们有这样一棵树: <ul class=”list”> <li>item 1</li> <li>item 2</li> </ul> 看起来很简单,对吧? 如何用JS对象来表示呢? { type: ‘ul’, props: { ‘class’: ‘list’ }, children: [
{ type: ‘li’, props: {}, children: [‘item 1’] },
{ type: ‘li’, props: {}, children: [‘item 2’] }
] }这里有两件事需要注意:
{ type: ‘…’, props: { … }, children: [ … ] }
但是用这种方式表示内容很多的 Dom 树是相当困难的。这里来写一个辅助函数,这样更容易理解: function h(type, props, …children) {
return { type, props, children };
}用这个方法重新整理一开始代码: h(‘ul’, { ‘class’: ‘list’ },
h(‘li’, {}, ‘item 1’),
h(‘li’, {}, ‘item 2’),
);这样看起来简洁多了,还可以更进一步。这里使用 JSX,如下: <ul className=”list”> <li>item 1</li> <li>item 2</li> </ul> 编译成: React.createElement(‘ul’, { className: ‘list’ },
React.createElement(‘li’, {}, ‘item 1’),
React.createElement(‘li’, {}, ‘item 2’),
);是不是看起来有点熟悉?如果能够用我们刚定义的 /** @jsx h */ <ul className=”list”> <li>item 1</li> <li>item 2</li> </ul> 它实际上告诉 Babel ' 嘿,小老弟帮我编译 JSX 语法,用 综上所述,我们将DOM写成这样: /** @jsx h */
const a = (
<ul className=”list”>
<li>item 1</li>
<li>item 2</li>
</ul>
);Babel 会帮我们编译成这样的代码: const a = (
h(‘ul’, { className: ‘list’ },
h(‘li’, {}, ‘item 1’),
h(‘li’, {}, ‘item 2’),
);
);当函数 const a = (
{ type: ‘ul’, props: { className: ‘list’ }, children: [
{ type: ‘li’, props: {}, children: [‘item 1’] },
{ type: ‘li’, props: {}, children: [‘item 2’] }
] }
);从Virtual DOM 映射到真实 DOM好了,现在我们有了 DOM 树,用普通的 JS 对象表示,还有我们自己的结构。这很酷,但我们需要从它创建一个真正的DOM。 首先让我们做一些假设并声明一些术语:
* 就像在 React 中一样,只能有一个根节点——所有其他节点都在其中 那么,来编写一个函数 function createElement(node) {
if (typeof node === ‘string’) {
return document.createTextNode(node);
}
return document.createElement(node.type);
}上述方法我也可以创建有两种节点分别是文本节点和 Dom 元素节点,它们是类型为的 JS 对象: { type: ‘…’, props: { … }, children: [ … ] }因此,可以在函数 现在让我们考虑子节点——它们中的每一个都是文本节点或元素。所以它们也可以用 createElement(…) 函数创建。是的,这就像递归一样,所以我们可以为每个元素的子元素调用 createElement(…),然后使用 function createElement(node) {
if (typeof node === ‘string’) {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}哇,看起来不错。先把节点 完整代码如下: /** @jsx h */
function h(type, props, ...children) {
return { type, props, children };
}
function createElement(node) {
if (typeof node === 'string') {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
const a = (
<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ul>
);
const $root = document.getElementById('root');
$root.appendChild(createElement(a));比较两棵虚拟DOM树的差异现在我们可以将虚拟 DOM 转换为真实的 DOM,这就需要考虑比较两棵 DOM 树的差异。基本的,我们需要一个算法来比较新的树和旧的树,它能够让我们知道什么地方改变了,然后相应的去改变真实的 DOM。 怎么比较 DOM 树?需要处理下面的情况:
如果节点相同的——就需要需要深度比较子节点
编写一个名为 updateElement(…) 的函数,它接受三个参数—— 添加新节点function updateElement($parent, newNode, oldNode) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
}
}移除老节点这里遇到了一个问题——如果在新虚拟树的当前位置没有节点——我们应该从实际的 DOM 中删除它—— 这要如何做呢? 如果我们已知父元素(通过参数传递),我们就能调用 好的,让我们假设这个索引将被传递给 updateElement 函数(它确实会被传递——稍后将看到)。代码如下: function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
}
}节点的替换首先,需要编写一个函数来比较两个节点(旧节点和新节点),并告诉节点是否真的发生了变化。还有需要考虑这个节点可以是元素或是文本节点: function changed(node1, node2) {
return typeof node1 !== typeof node2 ||
typeof node1 === ‘string’ && node1 !== node2 ||
node1.type !== node2.type
}现在,当前的节点有了 index 属性,就可以很简单的用新节点替换它: function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
}
}比较子节点最后,但并非最不重要的是——我们应该遍历这两个节点的每一个子节点并比较它们——实际上为每个节点调用updateElement(…)方法,同样需要用到递归。
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
} else if (newNode.type) {
const newLength = newNode.children.length;
const oldLength = oldNode.children.length;
for (let i = 0; i < newLength || i < oldLength; i++) {
updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
);
}
}
}完整的代码Babel+JSX function h(type, props, ...children) {
return { type, props, children };
}
function createElement(node) {
if (typeof node === 'string') {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
function changed(node1, node2) {
return typeof node1 !== typeof node2 ||
typeof node1 === 'string' && node1 !== node2 ||
node1.type !== node2.type
}
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
} else if (newNode.type) {
const newLength = newNode.children.length;
const oldLength = oldNode.children.length;
for (let i = 0; i < newLength || i < oldLength; i++) {
updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
);
}
}
}
// ---------------------------------------------------------------------
const a = (
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
);
const b = (
<ul>
<li>item 1</li>
<li>hello!</li>
</ul>
);
const $root = document.getElementById('root');
const $reload = document.getElementById('reload');
updateElement($root, a);
$reload.addEventListener('click', () => {
updateElement($root, b, a);
});HTML <button id="reload">RELOAD</button> <p id="root"></p> CSS #root {
border: 1px solid black;
padding: 10px;
margin: 30px 0 0 0;
}打开开发者工具,并观察当按下“Reload”按钮时应用的更改。
总结现在我们已经编写了虚拟 DOM 实现及了解它的工作原理。作者希望,在阅读了本文之后,对理解虚拟 DOM 如何工作的基本概念以及在幕后如何进行响应有一定的了解。 然而,这里有一些东西没有突出显示(将在以后的文章中介绍它们):
更多编程相关知识,请访问:编程入门!! 以上就是如何编写自己的虚拟DOM?方法介绍的详细内容,更多请关注模板之家(www.mb5.com.cn)其它相关文章! |
