什么是Virtual-DOM, 为什么需要Virtual-DOM

Virtual-DOM是现在前端框架中比较火的一个话题。不仅在React中使用,Vue2.0中也使用了Virtual-DOM。诚然Virtual-DOM对服务器端渲染有很大帮助,但本文主要从数据流方面来看Virtual-DOM带来的变化。

首先看这样一段代码,在input中输入内容可以改变counter的颜色,点击button可以增加counter的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<span id="colored-counter">0</span>
<input id="color"></input>
<button id="inc"></button>

<script>
$('#color').on('keyup', function () {
$('#colored-counter').css('color', this.value);
})

$('#inc').on('click', function () {
var oldValue = $('#colored-counter').html();
var newValue = 1 + Number(oldValue);
$('#colored-counter').html(newValue);
})
</script>

这段代码也可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<span id="colored-counter">0</span>
<input id="color"></input>
<button id="inc"></button>

<script>
var state = {color: '', value: 0};

function updateUI() {
$('#colored-counter').css('color', state.color);
$('#colored-counter').html(state.value);
}

$('#color').on('keyup', function () {
state.color = this.value;
updateUI();
})

$('#inc').on('click', function () {
state.value++;
updateUI();
})
</script>

实践来看第二种要比第一种好,为什么呢?可以用下图来说明:

常规

第一种方式每个事件直接刷新元素。如果事件和元素很多,就会非常混乱:

常规2

第二种方式将状态独立出来,事件改变状态,元素根据状态而改变,降低了复杂度:

常规2

在实际使用中,往往不会像上例这么简单。我们再来看这样一个需求,增加或删除DOM节点:

1
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
<span id="count">2</span>
<ul>
<li>hi</li>
<li>there</li>
</ul>
<button id="add"></button>

<script>
var state = {items: ['hi', 'there']}

function updateUI() {
$('#count').html(state.items.length);
// Compare ul.childNodes to state.items and make updates
// ...
}

$('ul').on('click', 'li', function () {
state.items.splice($(this).index(), 1);
updateUI();
})

$('#add').on('click', function () {
state.items.push(getNextString());
updateUI();
})
</script>

在这个例子中,有两个不好的地方:

  • 初始状态和预生成的html内容有冗余。
  • 由于要将数据结构与DOM节点进行比较,每次更新都会很复杂。

解决这两个问题的一个简单方法是使用纯函数来渲染界面,这样的话state就成了决定界面展现的唯一因素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="ui"></div>

<script>
// ...

function render(state) {
var span = '<span id="count">' + state.items.length + '</span>';
var lis = state.items.map(function (item) {
return '<li>' + item + '</li>';
});
return span + '<ul>' + lis.join('') + '</ul>'
}

function updateUI() {
$('#ui').html(render(state));
}

// ...
</script>

即使是很小的变化,上面的代码在每个事件中调用updateUI()时都会重新绘制一遍DOM节点,非常没有效率。Virtual-DOM首先由state生成虚拟节点树,再由节点树绘制DOM节点,之后每次更新时,对节点树进行diff算法,从而进行局部的DOM更新,解决了这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var root = document.getElementById('ui');
var prevState = state, prevTree = [];

function render(state) {
// Virtual DOM is really just a tree of JavaScript objects or arrays
return [
['span', {id: 'count'}, state.items.length],
['ul', {}, state.items.map(function (item) {
return ['li', {}, item]
})]
]
}

function updateUI() {
var vTree = render(state);
var diff = vDiff(prevTree, vTree); // Just a diff on data structures, haha :)
vApply(root, diff) // Apply series of patches to real DOM

prevState = deepcopy(state);
prevTree = vTree;
}

我们可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:

  • innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
  • Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)

Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。

  • 相关文献: