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树的差异
在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:
首先,我们要明确我们要得到的东西:一个记录每个节点变化情况的对象数组,如:
深度循环节点树:
// 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新增了属性id为container,就记录下:
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)
}
})
}