初探虚拟 DOM

前言

若是有这么一张表格要你维护。javascript

后续涉及到表的增删改,你会怎么作?html

  • 增:先找到正确的位置,再插元素进去?
  • 删:找到正确的元素,删掉它?
  • 改:找到正确的元素,修改它?

表格简单的时候还好,用 JavaScript 操做起来还算方便。但随着应用愈来愈复杂,须要处理的数据也愈来愈大,愈来愈复杂的时候,须要利用 JavaScript 操做的地方也会愈来愈多,这个时候准确地修改数据就变得不是那么容易了。java

虚拟 DOM 的产生

针对前面的状况,那么能不能用一个东西来存储页面的视图状态,当视图状态发送变化时,读取这个东西,而后更新页面?node

好比这一段 HTML 代码对应的 DOM,算法

<div>
  <div>
    <span>hello</span>
  </div>
  <span>world</span>
</div>
复制代码

咱们用另外的一个对象来表示它app

let nodesData = {
  tag: 'div',
  children: [
    {
      tag: 'div',
      children: [
        {
          tag: 'span',
          children: [
            {
              tag: '#text',
              text: 'hello'
            }
          ]
        }
      ]
    },
    {
      tag: 'span',
        children: [
          {
            tag: '#text',
            text: 'world'
          }
        ]
    }
  ]
}
复制代码

用这个对象来表示 DOM 结构,咱们能够根据这个对象来构建真正的 DOM。函数

如今咱们须要写一个函数,将这个虚假的 DOM 转化为真实的 DOM。性能

化假为真

function vNode({tag, children, text}){
  this.tag = tag
  this.children = children
  this.text = text
}

vNode.prototype.render = function(){
  if(this.tag === '#text'){
    return document.createTextNode(this.text)
  }
  
  let el = document.createElement(this.tag)
  this.children.map((vChild) => {
    el.appendChild(new vNode(vChild).render())
  })
  
  return el
}
复制代码

调用上面的这个函数能够将咱们用来表示 DOM 的对象(虚假 DOM)变成真正的 DOM。优化

let node = new vNode(nodesData)
node.render()
复制代码

这样,就化假 DOM 为真 DOM 了。ui

当咱们的须要改变 DOM 时,只须要改变其对应的虚假 DOM,再调用一下 render 函数,就能够改变真实 DOM,不须要咱们亲自用 JavaScript 去操做页面中的 DOM。

局部更新

上面虽然实现了从虚假 DOM 到真实 DOM 的转化,可是也有一个问题,那就是每次转化都会遍历全部的 DOM 结构,统统的所有转化一遍。若是只有一个小地方发生了改变,也须要将所有的 DOM 更新一遍,那这样就太耗费性能了,咱们应该比较虚假 DOM 的变化,只更新变化的地方。

function patchElement(parent, newVNode, oldVNode, index = 0) {
  if (!oldVNode) {
    parent.appendChild(newVNode.render());
  } else if (!newVNode) {
    parent.removeChild(parent.childNodes[index]);
  } else if (newVNode.tag !== oldVNode.tag || newVNode.text !== oldVNode.text) {
    parent.replaceChild(new vNode(newVNode).render(), parent.childNodes[index]);
  } else {
    for (
      let i = 0;
      i < newVNode.children.length || i < oldVNode.children.length;
      i++
    ) {
      patchElement(
        parent.childNodes[index],
        newVNode.children[i],
        oldVNode.children[i],
        i
      );
    }
  }
}
复制代码

经过这个算法,逐层比较新旧虚假 DOM 的结构变化,若是没变,就继续往下遍历;若是发现结构发生了变化,就从新生成真实 DOM 替换掉旧的。

来看一看效果。

从图中能够看到,当虚假 DOM 发生变化时,在更新真实 DOM 的过程当中,只更新了发生了变化的那一部分,没有发生变化的地方是没动的,这样就优化了性能。

结语

这是一个很是粗糙的实现,diff 算法很是简单地比较了差别,这里仅仅表达了一下虚拟 DOM 的实现思想,在实际运用过程还有不少地方须要考虑。

这里贴个完整代码。

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>

<body>
  <div id="test"></div>
  <script> let nodesData = { tag: 'div', children: [ { tag: 'div', children: [ { tag: 'span', children: [ { tag: '#text', text: 'hello' } ] } ] }, { tag: 'span', children: [ { tag: '#text', text: 'world' } ] } ] }; let nodesData2 = { tag: 'div', children: [ { tag: 'div', children: [ { tag: 'span', children: [ { tag: '#text', text: 'HELLO' } ] } ] }, { tag: 'span', children: [ { tag: '#text', text: 'WORLD' } ] } ] }; function vNode({ tag, children, text }) { this.tag = tag; this.children = children; this.text = text; } vNode.prototype.render = function () { if (this.tag === '#text') { return document.createTextNode(this.text); } let el = document.createElement(this.tag); this.children.map(vChild => { el.appendChild(new vNode(vChild).render()); }); return el; }; function patchElement(parent, newVNode, oldVNode, index = 0) { if (!oldVNode) { parent.appendChild(newVNode.render()); } else if (!newVNode) { parent.removeChild(parent.childNodes[index]); } else if (newVNode.tag !== oldVNode.tag || newVNode.text !== oldVNode.text) { parent.replaceChild(new vNode(newVNode).render(), parent.childNodes[index]); } else { for ( let i = 0; i < newVNode.children.length || i < oldVNode.children.length; i++ ) { patchElement( parent.childNodes[index], newVNode.children[i], oldVNode.children[i], i ); } } } let node1 = new vNode(nodesData); let node2 = new vNode(nodesData2); let test = document.querySelector('#test'); test.appendChild(node1.render()); setTimeout(() => { patchElement(test, node2, node1, 0); }, 5000); </script>
</body>

</html>
复制代码
相关文章
相关标签/搜索