把一个div
元素的属性打印出来,以下:javascript
DOM
的元素是很是庞大的,这也是
DOM
加载慢的缘由。 相对于
DOM
对象,原生的
JavaScript
对象处理起来更快,并且更简单。
DOM
树上的结构、属性信息均可以用
JavaScript
对象表示出来:
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
写法是:前端
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
复制代码
DOM
树的信息能够用JavaScript
对象表示出来,则说明能够用JavaScript
对象去表示树结构来构建一棵真正的DOM
树。java
状态变动->从新渲染整个视图的方式能够用新渲染的对象树去和旧的树进行对比,记录这两棵树的差别。二者的不一样之处就是咱们须要对页面真正的DOM
操做,而后把它们应用在真正的DOM
树上,页面就变动了。这样能够作到:视图的结构确实是整个全新渲染了,可是最后操做DOM
的只有变动不一样的地方。node
DOM
树的结构,而后用这个树构建一个真正的DOM
树,插到文档当中2
所记录的差别应用到步骤1所构建的的真正的DOM
树上,视图就更新了Virtual DOM
本质就是在JS和DOM之间作了一个缓存,JS
操做Virtual DOM
,最后再应用到真正的DOM
上。git
步骤一:用JS
对象模拟虚拟DOM
树github
用JavaScript
来表示一个DOM
节点,则须要记录它的节点类型、属性、子节点: element.js算法
function Element (tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}
module.exports = function (tagName, props, children) {
return new Element(tagName, props, children)
}
复制代码
上面的DOM结构能够表示为:数组
var el = require('./element')
var ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
])
复制代码
如今ul
只是一个JavaScript
对象表示的DOM
结构,页面上并无这个结构。能够根据这个ul
构建真正的<ul>
:缓存
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
}
复制代码
render
方法会根据tagName
构建一个真正的DOM
节点,而后设置这个节点的属性,最后递归地把本身的子节点也构建起来。因此须要:app
var ulRoot = ul.render()
document.body.appendChild(ulRoot)
复制代码
上面的ulRoot
是真正的DOM
节点,把它塞进文档中,这样body
里面就有了真正的<ul>
的DOM结构:
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
复制代码
步骤二:比较两棵虚拟DOM树的差别
比较两棵DOM
树的差别是Virtual DOM
算法最核心的部分,就是diff
算法。两棵树的彻底diff
算法是一个时间复杂度为O(n^3)
的问题。但在前端中,不多会跨越层级地移动DOM
元素。因此Virtual DOM
只会对同一层级的元素进行对比:
div
只会和同一层级的
div
对比,第二层级的只会跟第二层级对比。这样算法复杂度就能够达到
O(n)
。
a.深度优先遍历,记录差别 在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每一个节点都会有一个惟一的标记:
// 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
})
}
复制代码
例如,上面的div和新的div有差别,当前的标记是0
,那么:
patches[0] = [{difference}, {difference}, ...] // 用数组存储新旧节点的不一样
复制代码
同理p
是patches[1]
,ul
是patches[3]
,以此类推
b.差别类型
对DOM
操做会有的差别:
div
换成了section
div
的子节点,把p
和ul
顺序互换2
内容为Virtual DOM2
因此定义了几种差别类型:
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"
}]
复制代码
c.列表对比算法
上面若是把div
中的子节点从新排序,看如p
,ul
,div
的顺序换成了div
,p
,ul
。按照同层进行顺序对比的话,它们都会被替换掉,这样DOM
开销很是大。而实际上只须要经过节点移动就能够的了。 假设如今能够英文字母惟一得标志每个子节点: 旧的节点顺序: a b c d e f g h i
如今对节点进行删除、插入、移动的操做。新增j节点,删除e节点,移动h节点: 新的节点顺序: a b c h d f g i j
如今知道了新旧的顺序,求最小的插入、删除操做(移动能够当作是删除和插入操做的结合)。这个问题抽象出来实际上是字符串的最小编辑距离问题(Edition Distance
),最多见的算法是Levenshtein Distance
, 经过动态规划求解,时间复杂度为O(M*N)
。而咱们只须要优化一些常见的移动操做,牺牲必定的DOM
操做,让算法时间复杂度达到线性的O((max(M,N)))
。 获取某个父节点的子节点的操做,就能够记录以下:
patches[0] = [{
type: REORDER,
moves: [{remove or insert}, {remove or insert}, ...]
}]
复制代码
因为tagName
是能够重复的,因此不能用这个来进行对比。须要给子节点加上一盒惟一标识key
,列表对比的时候,使用key
进行对比,这样就能复用旧的DOM
树上的节点。 经过深度优先遍历两棵树,每层节点进行对比,记录下每一个节点的差别。完整的diff
算法访问:github.com/livoras/sim…
步骤三:把差别应用到真正的DOM
树上 由于步骤一所构建的JavaScript
对象树和render
出来的真正的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] // 从patches拿出当前节点的差别
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操做
}
}
复制代码
applyPatches
,根据不一样类型的差别对当前节点进行 DOM
操做:
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)
}
})
}
复制代码
完整patch
代码访问:github.com/livoras/sim…