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

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、测试





  
  
  
  diff-patch demo
  
  
  
  



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

参考

  • 本文源码
  • 深度剖析:如何实现一个 Virtual DOM 算法

你可能感兴趣的:(JS 实现一个简易 dom 的 diff-patch 算法)