Skip to content

Latest commit

 

History

History
194 lines (141 loc) · 7.02 KB

3.3.3 DOM-DIFF算法.md

File metadata and controls

194 lines (141 loc) · 7.02 KB

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)
}

例如上面的 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结构:

      <ul id='list'>
        <li class='item'>Item 1</li>
        <li class='item'>Item 2</li>
        <li class='item'>Item 3</li>
      </ul>
      

      完整代码可见: 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
        })
      }
      

      完整 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)
      

      参考