[维护状态,更新视图]
用js对象表示Dom元素
js:
var element = {
tagName: 'ul', // 节点标签名
props: { // DOM的属性,用一个对象存储键值对
id: 'list'
},
children: [ // 该节点的子节点
{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 树,插到文档当中
- 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
- 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了
算法实现
1. js对象模拟dom元素
js对象表示DOM元素 需要记录的信息有:节点类型、属性,子节点
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'])
])
现在ul只是一个 JavaScript 对象表示的 DOM 结构,页面上并没有这个结构。我们可以根据这个ul构建真正的<ul>:
Element.prototype.render = function () {
var el = document.createElement(this.tagName) // 根据tagName构建
var props = this.props
for (var propName in props) { // 设置节点的DOM属性
var propValue = props[propName]
el.setAttribute(propName, propValue)
}
var children = this.children || []
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
: document.createTextNode(child) // 如果字符串,只构建文本节点
el.appendChild(childEl)
})
return el
}
render方法会根据tagName构建一个真正的DOM节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。所以只需要:
var ulRoot = ul.render()
document.body.appendChild(ulRoot)
上面的ulRoot是真正的DOM节点,把它塞入文档中,这样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算法是重点
- 实现一个简单的diff算法--比较两个字符串的差异
var oldStr = 'aaabbbccc';
var newStr = 'aaagggccc';
diff信息:[3, "-3", "+ggg", 3]
整数代表无变化的字符数量,“-”开头的字符串代表被移除的字符数量,“+”开头的字符串代表新加入的字符。所以我们可以写一个 minimizeDiffInfo 函数:
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进行深度优先遍历。
3.把差异应用到真正的DOM树上
结语
虚拟dom实现流程的概括:
// 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)