正常的dom
<ul class=”list”>
<li>item 1</li>
<li>item 2</li>
</ul>
用js的object来代表dom
{
type: 'ul', props: {'class': 'list'}, children: [
{type: 'li', props: {}, children: ['item 1']},
{type: 'li', props: {}, children: ['item 2']}
]
}
写个帮助方法创建js的dom
function helper(type, props, ...children) {
return {type, props, children};
}
现在就可以这样写:
helper('ul', {'class': 'list'},
helper('li', {}, 'item 1'),
helper('li', {}, 'item 2')
)
可以通过babel 来转换jsx
实现从我们的js的object到真实dom
function createElement(node) {
if (typeof node == 'string') {
return document.createTextNode(node);
}
$el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
接下来处理diff
有四种情况
- 新增
// old
<ul>
<li>item 1</li>
</ul>
// new
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
- 删除
// old
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
// new
<ul>
<li>item 1</li>
</ul>
- 替换
// old
<div>
<p>item 1</p>
<button>cpck it</button>
</div>
// new
<div>
<p>item 1</p>
<p>hello</p>
</div>
- 节点一致,子节点不一致
// old
<ul>
<li>item 1</li>
<li>
<span>hello</span>
<div>hi!</div>
</li>
</ul>
// new
<ul>
<li>item 1</li>
<li>
<span>hello</span>
<span>hi!</span>
</li>
</ul>
所以我们可以写一个更新函数,接收三个参数,$parent、newNode、oldNode, 其中$parent是真实dom元素,并且是虚拟节点的父节点。(暂时不考虑props)
当无新节点或者旧节点时
function updateElement($parent, newNode, oldNode, index = 0) {
// 无旧节点
if (!oldNode) {
$parent.appendChild(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;
}
那么现在我们就可以完善一下 updateElement 函数:
function updateElement($parent, newNode, oldNode, index = 0) {
// 无旧节点
if (!oldNode) {
$parent.appendChild(newNode);
// 无新节点
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index];
);
// 新旧节点发生变化时
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index];
)
}
}
最后不过也非常重要的事情
我们在对比节点时,需要保证它们的子节点也需要对比,才能判断他们的差异。在写代码之前我们需要考虑以下几个问题:
- 我们只需要对比元素节点而不用对比文本节点(文本节点无子节点);
- 我们把现在这个节点当做父节点;
- 我们需要一个一个节点对比,甚至是undefined,我们函数中需要有能应对undefined这种情况的能力;
- index只是子节点的索引。
考虑到以上,我们可以继续完善 updateElement 函数:
function updateElement($parent, newNode, oldNode, index = 0) {
// 无旧节点
if (!oldNode) {
$parent.appendChild(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 len = newNode.children.length > oldNode.children.length ? newNode.children.length : oldNode.Children.length;
for (var i = 0; i<len; i++) {
updateElement(
$parent.childNodes[index],
newNode.childNodes[i],
oldNode.childNodes[i],
i
);
}
}
}
现在我们从整体来看
// index.html
<button id="reload">RELOAD</button>
<div id="root"></div>
js(babel+jsx)
function createElement(node) {
if (typeof node == 'string') {
return document.createTextNode(node);
}
$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(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 len = newNode.children.length > oldNode.children.length ? newNode.children.length : oldNode.Children.length;
for (var i = 0; i<len; i++) {
updateElement(
$parent.childNodes[index],
newNode.childNodes[i],
oldNode.childNodes[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, a, b);
})
总结
我们到现在已经基本完成了 Virtual Dom 的简单实现,通过这我们应该能够了解 Virtual Dom 的基本原理,和了解 React 内部基本原理。
在这篇文章中我们还有一些我们没完成的东西,如下:
- 设置节点的属性,并且计算差别和更新它们;
- 节点的事件监听;
- 让我们的 Virtual Dom 和组件工作,比如 React;
- 拿到真实的Dom的引用;
- 支持其它库直接操作真实DOM;
- 其它...