[維護狀態(tài)啡氢,更新視圖]
用js對象表示Dom元素
js:
var element = {
tagName: 'ul', // 節(jié)點標簽名
props: { // DOM的屬性累澡,用一個對象存儲鍵值對
id: 'list'
},
children: [ // 該節(jié)點的子節(jié)點
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
]
}
表示dom結構為:
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
Virtual DOM 算法裹粤,包括幾個步驟:
- 用 JavaScript 對象結構表示 DOM 樹的結構颁褂;然后用這個樹構建一個真正的 DOM 樹仆潮,插到文檔當中
- 當狀態(tài)變更的時候故黑,重新構造一棵新的對象樹儿咱。然后用新的樹和舊的樹進行比較,記錄兩棵樹差異
- 把2所記錄的差異應用到步驟1所構建的真正的DOM樹上场晶,視圖就更新了
算法實現(xiàn)
1. js對象模擬dom元素
js對象表示DOM元素 需要記錄的信息有:節(jié)點類型混埠、屬性,子節(jié)點
element.js
function Element (tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}
module.exports = function (tagName, props, children) {
return new Element(tagName, props, children)
}
例如上面的 DOM 結構就可以簡單的表示:
var el = require('./element')
var ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
])
現(xiàn)在ul只是一個 JavaScript 對象表示的 DOM 結構诗轻,頁面上并沒有這個結構钳宪。我們可以根據(jù)這個ul構建真正的<ul>:
Element.prototype.render = function () {
var el = document.createElement(this.tagName) // 根據(jù)tagName構建
var props = this.props
for (var propName in props) { // 設置節(jié)點的DOM屬性
var propValue = props[propName]
el.setAttribute(propName, propValue)
}
var children = this.children || []
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render() // 如果子節(jié)點也是虛擬DOM,遞歸構建DOM節(jié)點
: document.createTextNode(child) // 如果字符串,只構建文本節(jié)點
el.appendChild(childEl)
})
return el
}
render方法會根據(jù)tagName構建一個真正的DOM節(jié)點使套,然后設置這個節(jié)點的屬性罐呼,最后遞歸地把自己的子節(jié)點也構建起來。所以只需要:
var ulRoot = ul.render()
document.body.appendChild(ulRoot)
上面的ulRoot是真正的DOM節(jié)點侦高,把它塞入文檔中嫉柴,這樣body里面就有了真正的<ul>的DOM結構:
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
2.比較兩顆dom樹的差異
diff算法是重點
- 實現(xiàn)一個簡單的diff算法--比較兩個字符串的差異
var oldStr = 'aaabbbccc';
var newStr = 'aaagggccc';
diff信息:[3, "-3", "+ggg", 3]
整數(shù)代表無變化的字符數(shù)量,“-”開頭的字符串代表被移除的字符數(shù)量奉呛,“+”開頭的字符串代表新加入的字符计螺。所以我們可以寫一個 minimizeDiffInfo 函數(shù):
function minimizeDiffInfo(originalInfo){
var result = originalInfo.map(info => {
if(info.added){
return '+' + info.value;
}
if(info.removed){
return '-' + info.count;
}
return info.count;
});
return JSON.stringify(result);
}
var diffInfo = [
{ count: 3, value: 'aaa' },
{ count: 3, added: undefined, removed: true, value: 'bbb' },
{ count: 3, added: true, removed: undefined, value: 'ggg' },
{ count: 3, value: 'ccc' }
];
minimizeDiffInfo(diffInfo);
//=> '[3, "-3", "+ggg", 3]'
用戶端接受到精簡之后的 diff 信息,生成最新的資源:
mergeDiff('aaabbbccc', '[3, "-3", "+ggg", 3]');
//=> 'aaagggccc'
function mergeDiff(oldString, diffInfo){
var newString = '';
var diffInfo = JSON.parse(diffInfo);
var p = 0;
for(var i = 0; i < diffInfo.length; i++){
var info = diffInfo[i];
if(typeof(info) == 'number'){
newString += oldString.slice(p, p + info);
p += info;
continue;
}
if(typeof(info) == 'string'){
if(info[0] === '+'){
var addedString = info.slice(1, info.length);
newString += addedString;
}
if(info[0] === '-'){
var removedCount = parseInt(info.slice(1, info.length));
p += removedCount;
}
}
}
return newString;
}
- 虛擬dom的diff算法會比較難一點瞧壮,因為會涉及到不僅是同級的元素登馒,要跨越層級進行增刪改移;我們需要對dom進行深度優(yōu)先遍歷咆槽。
3.把差異應用到真正的DOM樹上
結語
虛擬dom實現(xiàn)流程的概括:
// 1. 構建虛擬DOM
var tree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: blue'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li')])
])
// 2. 通過虛擬DOM構建真正的DOM
var root = tree.render()
document.body.appendChild(root)
// 3. 生成新的虛擬DOM
var newTree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li'), el('li')])
])
// 4. 比較兩棵虛擬DOM樹的不同
var patches = diff(tree, newTree)
// 5. 在真正的DOM元素上應用變更
patch(root, patches)