正常的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']}
]
}
寫個幫助方法創(chuàng)建js的dom
function helper(type, props, ...children) {
return {type, props, children};
}
現(xiàn)在就可以這樣寫:
helper('ul', {'class': 'list'},
helper('li', {}, 'item 1'),
helper('li', {}, 'item 2')
)
可以通過babel 來轉(zhuǎn)換jsx
實現(xiàn)從我們的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>
- 節(jié)點(diǎn)一致硕旗,子節(jié)點(diǎn)不一致
// 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>
所以我們可以寫一個更新函數(shù)臀突,接收三個參數(shù),$parent抵代、newNode、oldNode, 其中$parent是真實dom元素褥影,并且是虛擬節(jié)點(diǎn)的父節(jié)點(diǎn)。(暫時不考慮props)
當(dāng)無新節(jié)點(diǎn)或者舊節(jié)點(diǎn)時
function updateElement($parent, newNode, oldNode, index = 0) {
// 無舊節(jié)點(diǎn)
if (!oldNode) {
$parent.appendChild(newNode);
// 無新節(jié)點(diǎn)
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index];
);
}
}
有新節(jié)點(diǎn)和舊節(jié)點(diǎn)時,需要判斷節(jié)點(diǎn)是否改變斩郎,所以我們可以先寫一個判斷節(jié)點(diǎn)是否改變的函數(shù)。
function changed(node1, node2) {
// 基礎(chǔ)數(shù)據(jù)類型判斷
return typeof node1 !== typeof node2 ||
// 文本節(jié)點(diǎn)時是否一致
typeof node1 == 'string' && node1 !== node2 ||
// 元素節(jié)點(diǎn)時類型是否一致
node1.type !== node2.type;
}
那么現(xiàn)在我們就可以完善一下 updateElement 函數(shù):
function updateElement($parent, newNode, oldNode, index = 0) {
// 無舊節(jié)點(diǎn)
if (!oldNode) {
$parent.appendChild(newNode);
// 無新節(jié)點(diǎn)
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index];
);
// 新舊節(jié)點(diǎn)發(fā)生變化時
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index];
)
}
}
最后不過也非常重要的事情
我們在對比節(jié)點(diǎn)時喻频,需要保證它們的子節(jié)點(diǎn)也需要對比缩宜,才能判斷他們的差異。在寫代碼之前我們需要考慮以下幾個問題:
- 我們只需要對比元素節(jié)點(diǎn)而不用對比文本節(jié)點(diǎn)(文本節(jié)點(diǎn)無子節(jié)點(diǎn))甥温;
- 我們把現(xiàn)在這個節(jié)點(diǎn)當(dāng)做父節(jié)點(diǎn)脓恕;
- 我們需要一個一個節(jié)點(diǎn)對比,甚至是undefined窿侈,我們函數(shù)中需要有能應(yīng)對undefined這種情況的能力炼幔;
- index只是子節(jié)點(diǎn)的索引。
考慮到以上史简,我們可以繼續(xù)完善 updateElement 函數(shù):
function updateElement($parent, newNode, oldNode, index = 0) {
// 無舊節(jié)點(diǎn)
if (!oldNode) {
$parent.appendChild(newNode);
// 無新節(jié)點(diǎn)
} 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
);
}
}
}
現(xiàn)在我們從整體來看
// 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) {
// 基礎(chǔ)數(shù)據(jù)類型判斷
return typeof node1 !== typeof node2 ||
// 文本節(jié)點(diǎn)時是否一致
typeof node1 == 'string' && node1 !== node2 ||
// 元素節(jié)點(diǎn)時類型是否一致
node1.type !== node2.type;
}
function updateElement($parent, newNode, oldNode, index = 0) {
// 無舊節(jié)點(diǎn)
if (!oldNode) {
$parent.appendChild(newNode);
// 無新節(jié)點(diǎn)
} 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);
})
總結(jié)
我們到現(xiàn)在已經(jīng)基本完成了 Virtual Dom 的簡單實現(xiàn)乃秀,通過這我們應(yīng)該能夠了解 Virtual Dom 的基本原理,和了解 React 內(nèi)部基本原理圆兵。
在這篇文章中我們還有一些我們沒完成的東西跺讯,如下:
- 設(shè)置節(jié)點(diǎn)的屬性,并且計算差別和更新它們殉农;
- 節(jié)點(diǎn)的事件監(jiān)聽刀脏;
- 讓我們的 Virtual Dom 和組件工作,比如 React超凳;
- 拿到真實的Dom的引用愈污;
- 支持其它庫直接操作真實DOM;
- 其它...