美文网首页
JS 实现一个简易 dom 的 diff-patch 算法

JS 实现一个简易 dom 的 diff-patch 算法

作者: 风之化身呀 | 来源:发表于2019-12-01 11:54 被阅读0次

    1、创建 Virtual Dom 及辅助函数

    // 创建vdom,正常html标签是对象,文本节点是字符串
    function createVdom(tag, attrs, children) {
      return {
        tag,
        attrs,
        children
      }
    }
    
    // vdom 转为真实 dom
    function vdom2Element(vdom) {
      if (typeof vdom === 'string') {
        return document.createTextNode(vdom)
      }
      var dom = document.createElement(vdom.tag);
      setAttributes(dom, vdom.attrs)
      vdom.children.forEach(child => dom.appendChild(vdom2Element(child)))
      return dom
    }
    
    function setAttributes(dom, attrs) {
      Object.keys(attrs).forEach(key => {
        var value = attrs[key]
        // 事件
        if (/^on[A-Z]+/.test(key)) {
          dom.addEventListener(key.slice(2).toLowerCase(), value, false)
        } else if (key === 'class') {
          // 类名
          dom.classList.add(value)
        } else if (key === 'style') {
          // 样式
          dom.style.cssText = value
        } else {
          // 通用
          dom.setAttribute(key, value)
        }
      })
    }
    // 挂载节点
    function mount(dom, container) {
      container.appendChild(dom)
    }
    

    2、实现 diff 算法

    /**
     * 总体采用先序深度优先遍历算法得出2棵树的差异
     */
    
    // 对比新旧vdom
    function diff(old, fresh) {
      var patches = {}
      var index = 0
      walk(old, fresh, index, patches)
      return patches
    }
    
    function walk(old, fresh, index, patches) {
      var muteArray = []
      // 新节点不存在
      if (!fresh) {
        muteArray.push({
          type: 'REMOVE'
        })
      } else if (typeof fresh === 'string') {
        // 新节点是文本节点
        muteArray.push({
          type: "REPLACE",
          value: fresh
        })
      } else {
        // 新节点是 tag 节点
        if (typeof old === 'string') {
          // 老节点是文本节点
          muteArray.push({
            type: "REPLACE",
            value: fresh
          })
        } else {
          // 老节点是tag节点
          // 比较属性
          var attrs = diffAttr(old.attrs, fresh.attrs)
          muteArray.push({
            type: 'ATTR',
            value: attrs
          })
          // 比较子节点
          old.children.forEach((child, key) => {
            walk(child, fresh.children[key], index++, patches)
          })
        }
      }
      // 记录当前节点的变化
      if (muteArray.length >= 1) {
        patches[index] = muteArray
      }
    }
    
    function diffAttr(oldAttr, freshAttr) {
      var result = Object.create(null)
      // 覆盖老的
      for (const key in oldAttr) {
        if (oldAttr.hasOwnProperty(key)) {
          result[key] = freshAttr[key]
        }
      }
      // 获取新的
      for (const key in freshAttr) {
        if (!oldAttr.hasOwnProperty(key)) {
          result[key] = freshAttr[key]
        }
      }
      return result
    }
    

    3、实现 patch 算法

    function patch(el, patches) {
      var index = 0;
      traverse(el, index, patches)
    }
    
    function traverse(el, index, patches) {
      var children = el.childNodes
      // 先序深度优先
      children.forEach(child => {
        doPatch(child, index++, patches)
      })
      doPatch(el, index, patches)
    }
    
    function doPatch(el, index, patches) {
      var patch = patches[index]
      patch.forEach(item => {
        if (item.type === 'REMOVE') {
          el.parentNode.removeChild(el)
        } else if (item.type === 'REPLACE') {
          const newDom = vdom2Element(item.value)
          el.parentNode.replaceChild(newDom, el)
        } else {
          setAttributes(el, item.value)
        }
      })
    }
    

    4、测试

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>diff-patch demo</title>
      <style>
        .red {
          color: red
        }
    
        .black {
          color: black;
        }
      </style>
      <script src="./element.js"></script>
      <script src="./diff.js"></script>
      <script src="./patch.js"></script>
    </head>
    
    <body>
      <div id="app"></div>
      <div id="diff"></div>
      <button onclick="Mount()">挂载</button>
      <button onclick="Diff()">diff</button>
      <button onclick="Patch()">patch</button>
      <pre>
        <code>
            // 1、初次渲染
            var vdom1 = createVdom('h1', {
              class: 'red'
            }, [createVdom('p', {
              class: 'red'
            }, ['test']), createVdom('p', {
              class: 'red',
              style: 'color:green'
            }, ['test2']), createVdom('p', {
              class: 'red',
              style: 'color:green',
              onClick: function () {
                console.log('clicked')
              },
              name: 'test'
            }, ['test3'])])
            const el = vdom2Element(vdom1)
            mount(el, document.querySelector('#app'))
        </code>
      </pre>
    
      <pre>
        <code>
        // 2、diff-patch
        var vdom2 = createVdom('h1', {
          class: 'black'
        }, [createVdom('p', {}, ['testtest']), createVdom('p', {}, ['test2test2']), createVdom('p', {}, ['test2'])])
        const patches = diff(vdom1, vdom2)
        console.log('patches', patches)
        patch(el, patches)
        </code>
      </pre>
      <script>
        // 1、初次渲染
        var vdom1 = createVdom('h1', {
          class: 'red'
        }, [createVdom('p', {
          class: 'red'
        }, ['test']), createVdom('p', {
          class: 'red',
          style: 'color:green'
        }, ['test2']), createVdom('p', {
          class: 'red',
          style: 'color:green',
          onClick: function () {
            console.log('clicked')
          },
          name: 'test'
        }, ['test3'])])
        // const el = vdom2Element(vdom1)
        // mount(el, document.querySelector('#app'))
        // 2、diff-patch
        var vdom2 = createVdom('h1', {
          class: 'black'
        }, [createVdom('p', {}, ['testtest']), createVdom('p', {}, ['test2test2']), createVdom('p', {}, ['test2'])])
        // const patches = diff(vdom1, vdom2)
        // console.log('patches', patches)
        // patch(el, patches)
      </script>
      <script>
        var el, patches
    
        function Mount() {
          el = vdom2Element(vdom1)
          mount(el, document.querySelector('#app'))
        }
    
        function Diff() {
          patches = diff(vdom1, vdom2)
          document.querySelector('#diff').innerHTML = JSON.stringify(patches, '', 4)
        }
    
        function Patch() {
          patch(el, patches)
        }
      </script>
    </body>
    
    </html>
    

    参考

    相关文章

      网友评论

          本文标题:JS 实现一个简易 dom 的 diff-patch 算法

          本文链接:https://www.haomeiwen.com/subject/zzdzwctx.html