美文网首页
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