3.3.3 DOM-DIFF算法
比较只会在同层级进行, 不会跨层级比较。
DIFF算法在执行时有三个维度
分别是Tree DIFF、Component DIFF和Element DIFF,执行时按顺序依次执行,它们的差异仅仅因为DIFF粒度不同、执行先后顺序不同。
- 1.Tree DIFF
是对树的每一层进行遍历,如果某组件不存在了,则会直接销毁。如图所示,左边是旧属,右边是新属,第一层是R组件,一模一样,不会发生变化;
第二层进入Component DIFF,同一类型组件继续比较下去,发现A组件没有,所以直接删掉A、B、C组件;继续第三层,重新创建A、B、C组件。
第一层遍历完,进行第二层遍历时,D和G组件是不同类型的组件,不同类型组件直接进行替换,将D删掉,再将G重建。
2.Element DIFF紧接着以上统一类型组件继续比较下去,常见类型就是列表。
同一个列表由旧变新有三种行为,插入、移动和删除,它的比较策略是对于每一个列表指定key,先将所有列表遍历一遍,确定要新增和删除的,再确定需要移动的。 第一步将D删掉,第二步增加E,再次执行时A和B只需要移动位置即可。
二、深度剖析:如何实现一个 Virtual DOM 算法
- 算法实现:
- 1 步骤一:用JS对象模拟DOM树
- 2 步骤二:比较两棵虚拟DOM树的差异
- 3 步骤三:把差异应用到真正的DOM树上
-1.4.1 步骤一:用JS对象模拟DOM树
用 JavaScript 来表示一个 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)
}
2
3
4
5
6
7
8
9
10
11
例如上面的 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'])
])
2
3
4
5
6
7
现在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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
render方法会根据tagName构建一个真正的DOM节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。所以只需要:
var ulRoot = ul.render()
document.body.appendChild(ulRoot)
2
上面的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
3
4
5
完整代码可见: element.js。
- 2.步骤二:比较两棵虚拟DOM树的差异
正如你所预料的,比较两棵DOM树的差异是 Virtual DOM 算法最核心的部分,这也是所谓的 Virtual DOM 的 diff 算法。
两个树的完全的 diff 算法是一个时间复杂度为 O(n^3) 的问题。但是在前端当中,你很少会跨越层级地移动DOM元素。
所以 Virtual DOM 只会对同一个层级的元素进行对比:
div只会和同一层级的div对比,第二层级的只会跟第二层级对比。这样算法复杂度就可以达到 O(n)。
2.1 深度优先遍历,记录差异
在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记
在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。
// diff 函数,对比两棵树
function diff (oldTree, newTree) {
var index = 0 // 当前节点的标志
var patches = {} // 用来记录每个节点差异的对象
dfsWalk(oldTree, newTree, index, patches)
return patches
}
// 对两棵树进行深度优先遍历
function dfsWalk (oldNode, newNode, index, patches) {
// 对比oldNode和newNode的不同,记录下来
patches[index] = [...]
diffChildren(oldNode.children, newNode.children, index, patches)
}
// 遍历子节点
function diffChildren (oldChildren, newChildren, index, patches) {
var leftNode = null
var currentNodeIndex = index
oldChildren.forEach(function (child, i) {
var newChild = newChildren[i]
currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识
? currentNodeIndex + leftNode.count + 1
: currentNodeIndex + 1
dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点
leftNode = child
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
完整 diff 算法代码可见 diff.js。
- 3步骤三:把差异应用到真正的DOM树上 因为步骤一所构建的 JavaScript 对象树和render出来真正的DOM树的信息、结构是一样的。所以我们可以对那棵DOM树也进行深度优先的遍历,遍历的时候从步骤二生成的patches对象中找出当前遍历的节点差异,然后进行 DOM 操作。
完整代码可见 patch.js。
总结:
Virtual DOM 算法主要是实现上面步骤的三个函数:element,diff,patch。然后就可以实际的进行使用:
// 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)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23