实现一个简易版虚拟DOM

159 阅读2分钟

原理

面对复杂的数据,为了降低程序的维护难度,我们应该避免对于每个 dom 进行单独操作,而是在父级、根级对于某个大的 dom 分支上进行操作、渲染。就像设置事件代理,而不是逐个子节点设置一个监听。

但是,整个 DOM tree 大面积地重新渲染会非常损耗性能,如果我们只是因为小数据修改而重新渲染整个大节点,那实在是浪费。

虚拟 dom 就是为了解决这个问题,其原理是,在 js 层构用比 dom 对象更简单得多的普通对象结构代表真实存在的 dom,保存它的状态。
当我们需要重新渲染 dom 的时候,我们只要在 js 普通对象层面上把旧的 dom 结构跟新的 dom 结构进行比较,找出差异然后针对性地只渲染差异的部分。

实现

构建虚拟 dom 的类,每个实例自带一个 render 方法,用来把自己映射成为一个真正的 dom.

let root = document.querySelector(".root")

class VirtualDOM {
  constructor(tag, children){
    this.tag = tag
    this.children = children
  }
  render(){
    //我们规定,文本节点的 tag 用 “#text” 表示
    if(this.tag === "#text"){
      return document.createTextNode(this.children)
    }
    let el = document.createElement(this.tag)
    this.children.forEach(child=>{
      el.appendChild(child.render())
    })
    return el
  }
}

// 偷懒一下
function v(tag, child){
  return new VirtualDOM(tag, child)
}

对比新、老对象的方法

function patch(parent, newDOMObject, oldDOMObject, index = 0){
  if(!newDOMObject){
    parent.removeChild(parent.childNodes[index])
  } else if (!oldDOMObject){
    parent.appendChild(newDOMObject.render())
  } else if (newDOMObject.tag !== oldDOMObject.tag){
    parent.replaceChild(newDOMObject.render(), parent.childNodes[index])
  } else if (newDOMObject.tag === "#text" && newDOMObject.children !== oldDOMObject.children){
    parent.replaceChild(newDOMObject.render(), parent.childNodes[index])
  } else {
    for (let i = 0 ; i < newDOMObject.children.length || i < oldDOMObject.children.length; i++){
      patch(parent.childNodes[index], newDOMObject.children[i], oldDOMObject.children[i], i)
    }
  }
}

这样就可以了 …… 下面测试一下:

let newDOM = v("div", [
  v("div", [
    v("#text", 321321)
  ]),
  v("p", [
    v("#text", 123)
  ])
])

let oldDOM = v("div", [
  v("div", [
    v("#text", 123)
  ]),
  v("p", [
    v("#text", 123)
  ])
])

// let a = v("p", [v("#text", 123)])

root.appendChild(oldDOM.render())

setTimeout(()=>{
patch(root, newDOM, oldDOM)
}, 10000)

当当当,成功了!
当然这只是一个最简单的虚拟 dom ,实际开发,还有很多方面需要考虑。