在虚拟dom还没有普及的时候,一些解析html的模板引擎会用innerHTML来输出到页面上,不管这个dom节点和要输出的html数据是否有没有变化,假如这里只是改变了其中一个节点的text,但是它却直接将整个节点的innerHTML重新赋值渲染到页面,一下子销毁那么多元素,而且某些节点如果还绑定了事件,这样就造成了性能浪费,还可能会导致GC出问题。 而虚拟dom解决的就是这个问题,它将模板解析后返回一个生成vdom树的函数似于这样:

    function render (user) {
      return h('div', {class: 'user'}, [
        h('h1', {class: 'name'}, [user.name]), 
        h('strong', {class: 'info'}, [user.info])
      ])
    }

执行这个render函数会返回一个vdom树的对象:

    var tree = render({name: 'tony', age: '23'})
    console.log(tree)
      {
        "tagName": "div",
        "attrs": {
          "class": "user"
        },
        "childVNodes": [{
          "tagName": "h1",
          "attrs": {
            "class": "name"
          },
          "childVNodes": [{
            "text": "tony"
          }],
          "count": 1
        }, {
          "tagName": "strong",
          "attrs": {
            "class": "info"
          },
          "childVNodes": [{
            "text": "23"
          }],
          "count": 1
        }],
        "count": 4
      }

然后调用createElement(tree),就会返回一个真正的dom节点:

var node = createElement(tree)
document.body.appendChild(node)

当user数据变动的时候,这时再执行render方法返回一个新的vdom树:

// 只是名字改变了,年龄还是一样23
var newTree = render({name: 'Jack', info: '23'})

通过对两个vdom树的比较(diff),找出他们之间的差异(patches):

function diff (tree, newTree) {...}
function patch (rootNode, patches) {...}
var patches = diff(tree, newTree)
console.log(patches)
// patches: {7: {text: 'jack'}}

比较结束并且找出差异后再执行patch函数将这个差异局部渲染到真正的dom节点上:

patch(node, patches)

这时虚拟dom就已经工作完毕了,等到下一次数据变动的时候再继续调用render函数返回新的tree与之前的进行比较。

总结:

通过举的这个例子,可以看到diff找出text节点变化,patch函数仅将这个text节点进行重新渲染赋值,而没有影响到其他的节点。这样就实现了局部更新的特点,相比全局刷新性能有所提升,特别是在一些需要频繁操作dom的地方优势会更加明显。 虚拟dom的实现难点在于解析模板生成渲染的函数(render)和里面的diff算法。 通过对虚拟dom的理解自己编写了一个简单的demo:virtual-dom