虚拟DOM

218 阅读3分钟
 Virtual DOM 算是React,Vue最核心的算法之一吧,避免了频繁的操作DOM树,让前端项目更快了。代码:https://github.com/justworkhard/-DOM.git

什么是虚拟DOM树?

       var el = document.createElement('ul')   
       var li = document.createElement('li')   
       li.appendChild(document.createTextNode('test')) 
       el.appendChild(li)    
       document.body.appendChild(el);

在上面的代码中el就是最简单的虚拟DOM树,ul里携带一个li。用js模拟DOM节点并使其具有真实DOM属性。

用JS对象模拟DOM树:

function Element (tagName, props, children) {
  this.tagName = tagName //元素
  this.props = props     //元素属性
  this.children = children  //子元素
}

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
}
var ul = el('ul', {id: 'list'}, [
  el('li', {class: 'item'}, ['Item 1']),
  el('li', {class: 'item'}, ['Item 2']),
  el('li', {class: 'item'}, ['Item 3'])
])
 var ulRoot = ul.render(); //生成jsDOM节点
 document.body.appendChild(ulRoot);  //绑定到真实DOM树

定义Element类,类具有三大属性:tagName(节点名称),props(节点属性),children(子节点)。render通过document.createElement将节点类专成真正的DOM节点。在获取到真正的DOM节点后只需要将其绑定到document中既可以在页面上渲染。 

比较两棵虚拟DOM树的差异

在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:

dfs-walk

首先,我们要明确我们要得到的东西:一个记录每个节点变化情况的对象数组,如:


深度循环节点树:

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


经过深度循环,我们拿到了每个节点,接着需要对新老DOM树的节点变化比较。

差异比较

所以我们定义了几种差异类型:

var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3

对于节点替换,很简单。判断新旧节点的tagName和是不是一样的,如果不一样的说明需要替换掉。如div换成section,就记录下:

patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}]

如果给div新增了属性idcontainer,就记录下:

patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}, {
  type: PROPS,
  props: {
    id: "container"
  }
}]

如果是文本节点,如上面的文本节点2,就记录下:

patches[2] = [{
  type: TEXT,
  content: "Virtual DOM2"
}]

在dfsWalk.js中对变化类型类型进行比较并记录:

function dfsWalk (oldNode, newNode, index, patches) {  var currentPatch = []  // Node is removed.  if (newNode === null) {    // Real DOM node will be removed when perform reordering, so has no needs to do anthings in here  // TextNode content replacing  } else if (_.isString(oldNode) && _.isString(newNode)) {    if (newNode !== oldNode) {      currentPatch.push({ type: patch.TEXT, content: newNode })    }  // Nodes are the same, diff old node's props and children  } else if (      oldNode.tagName === newNode.tagName &&      oldNode.key === newNode.key    ) {    //                                                                                                    Diff props    var propsPatches = diffProps(oldNode, newNode)    if (propsPatches) {      currentPatch.push({ type: patch.PROPS, props: propsPatches })    }    // Diff children. If the node has a `ignore` property, do not diff children    if (!isIgnoreChildren(newNode)) {      diffChildren(        oldNode.children,        newNode.children,        index,        patches,        currentPatch      )    }  // Nodes are not the same, replace the old node with new node  } else {    currentPatch.push({ type: patch.REPLACE, node: newNode })  }  if (currentPatch.length) {    patches[index] = currentPatch  }}

把差异应用到真正的DOM树上

这一块相对简单,上一步我们拿到了每个节点变化的对象数组:patches;

通过相同的方式深度循环节点对照pathes改变节点。

function applyPatches (node, currentPatches) {
  currentPatches.forEach(function (currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        node.parentNode.replaceChild(currentPatch.node.render(), node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        node.textContent = currentPatch.content
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}

参考(抄袭)文献:

github.com/livoras/blo…