目前广为人知的React和Vue都采用了virtual-dom,Virtual DOM凭借其高效的diff算法,让咱们再也不关心性能问题,能够为所欲为的修改数据状态。在实际开发中,咱们并不须要关心Virtual DOM是如何实现的,可是理解Virtual DOM的实现原理确实有必要的。本文是参照github.com/livoras/sim… 源码进行理解vitual DOM。html
在日益复杂的前端应用中,状态管理是一个常常被说起的话题,从早期的刀耕火种时代到jQuery,再到如今流行的MVVM时代,状态管理的形式发生了翻天覆地的变化,咱们不再用维护茫茫多的事件回调、监听来更新视图,转而使用使用双向数据绑定,只须要维护相应的数据状态,就能够自动更新视图,极大提升开发效率。前端
可是,双向数据绑定也并非惟一的办法,还有一个很是粗暴有效的方式:一旦数据发生变化,从新绘制整个视图,也就是从新设置一下innerHTML。这样的作法确实简单、粗暴、有效,可是若是只是由于局部一个小的数据发生变化而更新整个视图,性价比未免过低了,并且,像事件,获取焦点的输入框等,都须要从新处理。因此,对于小的应用或者说局部的小视图,这样处理彻底是能够的,可是面对复杂的大型应用,这样的作法不可取。因此咱们能够采起用JavaScript的方法来模拟DOM树,用新渲染的对象树去和旧的树进行对比,记录下变化的变化,而后应用到真实的DOM树上,这样咱们只须要更改与原来视图不一样的地方,而不须要所有从新渲染一次。这就是virtual-DOM的优点node
相对于DOM对象,原生的JavaScript对象处理得更快,并且简单。DOM树上的结构,属性信息咱们都能经过JavaScript进行表示出来,例如:react
var element = {
tagName: 'ul', // 节点标签名
props: { // dom的属性键值对
id: 'list'
},
children: [
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}
]
}复制代码
那么在html渲染的结果就是:git
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>复制代码
既然可以经过JavaScript表示DOM树的信息,那么就能够经过使用JavaScript来构建DOM树。github
然而光是构建DOM树,没什么卵用,咱们须要将JavaScript构建的DOM树渲染到真实的DOM树上,用JavaScript表现一个dom一个节点很是简单,咱们只须要记录他的节点类型,属性键值对,子节点:算法
function Element(tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}复制代码
那么ul标签咱们就可使用这种方式来表示bash
var ul = new Element('ul', {id: 'list'}, [
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}
])复制代码
说了这么多,他只是用JavaScript表示的一个结构,那该如何将他渲染到真实的DOM结构中呢:app
Element.prototype.render = function() {
let el = document.createElement(this.tagName), // 节点名称
props = this.props // 节点属性
for (var propName in props) {
propValue = props[propName]
el.setAttribute(propName, propValue)
}
this.children.forEach((child) => {
var childEl = (child instanceof Element)
? child.render()
: document.createTextNode(child)
el.appendChild(childEl)
})
return el
}复制代码
若是咱们想将ul渲染到DOM结构中,就只须要dom
ulRoot = ul.render()
document.appendChild(ulRoot)复制代码
这样就完成了ul到DOM的渲染,也就有了真正的DOM结构
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>复制代码
React的核心算法是diff算法(这里指的是优化后的算法)咱们来看看diff算法是如何实现的:
diff只会对相同颜色方框内的DOM节点进行比较,即同一个父节点下的全部子节点。当发现节点不存在,则该节点和子节点会被彻底删除,不会作进一步的比较。
在实际的代码中,会对新旧两棵树进行深度的遍历,给每个节点进行标记。而后在新旧两棵树的对比中,将不一样的地方记录下来。
// diff 算法,对比两棵树
function diff(oldTree, newTree) {
var index = 0 // 当前节点的标志
var patches = {} // 记录每一个节点差别的地方
dfsWalk(oldTree, newTree, index, patches)
return patches
}
function dfsWalk(oldNode, newNode, index, patches) {
// 对比newNode和oldNode的差别地方进行记录
patches[index] = [...]
diffChildren(oldNode.children, newNode.children, index, patches)
}
function diffChildren(oldChildren, newChildren, index, patches) {
let leftNode = null
var currentNodeIndex = index
oldChildren.forEach((child, i) => {
var newChild = newChildren[i]
currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标记
? currentNodeIndex + leftNode.count + 1
: currentNodeIndex + 1
dfsWalk(child, newChild, currentNodeIndex, patches) // 遍历子节点
leftNode = child
})
}复制代码
例如:
在图中若是div有差别,标记为0,那么:
patches[0] = [{difference}, {difference}]复制代码
同理,有p是patches[1], ul是patches[3],以此类推
patches指的是差别变化,这些差别包括:一、节点类型的不一样,二、节点类型相同,可是属性值不一样,文本内容不一样。因此有这么几种类型:
var REPLACE = 0, // replace 替换
REORDER = 1, // reorder 父节点中子节点的操做
PROPS = 2, // props 属性的变化
TEXT = 3 // text 文本内容的变化复制代码
若是节点类型不一样,就说明是须要替换,例如将div替换成section,就记录下差别:
patches[0] = [{
type: REPLACE,
node: newNode // section
},{
type: PROPS,
props: {
id: 'container'
}
}]复制代码
在标题二中构建了真正的DOM树的信息,因此先对那一棵DOM树进行深度优先的遍历,遍历的时候同
patches对象进行对比,找到其中的差别,而后应用到DOM操做中。
function patch(node, patches) {
var walker = {index: 0} // 记录当前节点的标志
dfsWalk(node, walker, patches)
}
function dfsWalk(node, walker, patches) {
var currentPatches = patches[walker.index] // 这是当前节点的差别
var len = node.childNodes
? node.childNodes.length
: 0
for (var i = 0; i < len; i++) { // 深度遍历子节点
var child = node.childNodes[i]
walker.index++
dfsWalk(child, walker, patches)
}
if (currentPatches) {
applyPatches(node, currentPatches) // 对当前节点进行DOM操做
}
}
// 将差别的部分应用到DOM中
function applyPatches(node, currentPatches) {
currentPatches.forEach((currentPatch) => {
switch (currentPatch.type) {
case REPLACE:
var newNode = (typeof currentPatch.node === 'string')
? document.createTextNode(currentPatch.node)
: currentPatch.node.render()
node.parentNode.replaceChild(newNode, node)
break;
case REORDER:
reorderChldren(node, currentPatch.moves)
break
case PROPS:
setProps(node, currentPatch.props)
break
case TEXT:
if (node.textContent) {
node.textContent = currentPatch.content
} else {
node.nodeValue = currentPatch.content
}
break
default:
throw new Error('Unknown patch type ' + currentPatch.type)
}
})
}复制代码
此次的粗糙的virtual-dom基本已经实现了,具体的状况更加复杂。但这已经足够让咱们理解virtual-dom。
具体的带解析的代码已经上传到github
www.cnblogs.com/justany/arc…
github.com/livoras/blo…
github.com/y8n/blog/is…
medium.com/@deathmood/…
www.infoq.com/cn/articles…