虚拟DOM的实现

[维护状态,更新视图]
用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结构为:

  • Item 1
  • Item 2
  • Item 3
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构建真正的

    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里面就有了真正的

      的DOM结构:

      • Item 1
      • Item 2
      • Item 3

      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)
      

      参考:深度剖析:如何实现一个 Virtual DOM 算法

你可能感兴趣的:(虚拟DOM的实现)