大三战五渣的我,日常平凡也就只能用用他人的轮子,可总用不顺心,毕竟不知道道理,近来用vue写项目,内里涉及到的Virtual DOM虽然已不是什么新概念,但我也只是据说罢了,不知其所以然,既然看到大佬们剖析后,那就纪录下吧
参考资料:
戴嘉华:https://github.com/livoras/bl…
张歆琳:https://www.jianshu.com/p/616…
王沛:https://www.infoq.cn/article/…
为啥要Virtual DOM
起首先相识一下加载一个HTML会发作哪些事变
- 运用HTML分析器天生DOM Tree
- 运用CSS分析器天生CSSOM
- 运转JS
- 连系DOM Tree和CSSOM天生一棵Render Tree
- 依据render树,浏览器能够盘算出网页中有哪些节点,各节点的CSS以及从属关系,然后能够盘算出每一个节点在屏幕中的位置;
- 绘制出页面
当你用传统的源生api或jQuery去操纵DOM时,浏览器会从构建DOM树最先从头至尾实行一遍流程。比方当你在一次操纵时,须要更新10个DOM节点,抱负状况是一次性构建完DOM树,再实行后续操纵。但浏览器没这么智能,收到第一个更新DOM要求后,并不知道后续另有9次更新操纵,因此会立时实行流程,终究实行10次流程。明显比方盘算DOM节点的坐标值等都是白白浪费机能,能够此次盘算完,紧接着的下一个DOM更新要求,这个节点的坐标值就变了,前面的一次盘算是无用功。
DOM是很慢的,我们能够打印一下一个简朴的div元素的属性
这还只是一层罢了,实在的DOM会越发巨大,细微的触碰能够就会致使页面重排,这但是杀死机能的罪魁祸首。而相对于操纵DOM对象,原生的JS对象处置惩罚起来更快而且简朴
步骤
- JS示意DOM→构建DOM树→插图文档中
- 状况变化→从新组织一颗新的对象树→新旧树比较→纪录两棵树的差别
- 把2所纪录的差别运用到步骤1所构建的真正的DOM树上,从而视图更新了
Virtual DOM 的实质
在 JS 和 DOM 之间做了一个缓存。能够类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操纵内存(Virtual DOM),末了的时刻再把变动写入硬盘(DOM)。
算法完成
步骤一:用JS对象模仿DOM树
用JS纪录节点的范例,属性和子节点
element.js
function Element (tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}
function el(tagName, props, children){
return new Element(tagName, props, children)
}
比方上面的 DOM 构造就能够简朴的示意:
let el = require('./element')
let div= el('div', {id: 'blue-div'}, [
el('p', {class: 'pink-p'}, [
el('span', {class: 'yellow-sapn'}, ['Virtual sapn'])]),
el('ul', {class: 'green-ul'}, [
el('li', {class: 'red-li'}, ['Virtual li1']),
el('li', {class: 'red-li'}, ['Virtual li2']),
el('li', {class: 'red-li'}, ['Virtual li3'])]),
el('div', {class: 'black-div'}, ['Virtual div'])
])
如今的div
只是一个JS对象示意的DOM构造,页面上并没有这个构造,下面用来构建真正的div
Element.prototype.render = function () {
let el = document.createElement(this.tagName) //依据tagName构建
let props = this.props
for (let propName in props) { // 设置节点的DOM属性
let propValue = props[propName]
el.setAttribute(propName, propValue)
}
let children = this.children || []
children.forEach(function (child) {
let childEl = (child instanceof Element)
? child.render() // 假如子节点也是假造DOM,递归构建DOM节点
: document.createTextNode(child) // 假如字符串,只构建文本节点
el.appendChild(childEl)
})
return el
}
render方法会依据tagName构建一个真正的DOM节点,然后设置这个节点的属性,末了递归地把本身的子节点也构建起来。所以只须要:
let divRoot = div.render()
document.body.appendChild(divRoot)
上面的运转效果:
步骤二:比较两棵假造DOM树的差别(diff算法)
两棵树的完整差别比较的时候复杂度为O(n^3),这是不好的,又由于前端不会常常举行跨层地挪动DOM元素,所以Virtual DOM只对统一层级的元素举行比较,从而时候复杂度降为O(n)
深度优先遍历
在现实的代码中,会对新旧两棵树举行一个深度优先的遍历,如许每一个节点都邑有一个唯一的标记,在深度优先遍历的时刻,每遍历到一个节点就把改节点和新的数举行对照,假如有差别就纪录到patches
中
// diff 函数,对照两棵树
function diff (oldTree, newTree) {
let index = 0 // 当前节点的标志
let 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) {
let leftNode = null
let currentNodeIndex = index
oldChildren.forEach(function (child, i) {
let 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}, ...] // 用数组存储新旧节点的差别
四种差别
上面涌现了四种新旧树差别的状况:
- REPLACE:节点范例变了,
p
变成了div
,将旧节点卸载并装载新节点 - PROPS:不触发节点的卸载和装载,实行节点的更新
- TEXT:修正文本内容
- REORDER:挪动、增添(多了
li
)、删除节点,现实操纵如图:
所以我们定义了几种差别范例:
let REPLACE = 0
patches[0] = [{
type: REPALCE,
node: newNode // el('div', props, children) p换成div
}]
let PROPS = 1
patches[0] = [{
type: REPALCE,
node: newNode // el('p', props, children)
}, {
type: PROPS,
props: {//给p新增了id为container
id: "container"
}
}]
let TEXT = 2
patches[1] = [{//修正文本节点
type: TEXT,
content: "Virtual DOM2"
}]
let REORDER = 3 //重排见王沛的https://www.infoq.cn/article/react-dom-diff
终究Diff出来的效果范例以下:
{
1: [ {type: REPLACE, node: Element} ],
4: [ {type: TEXT, content: "after update"} ],
5: [ {type: PROPS, props: {class: "marginLeft10"}}, {type: REORDER, moves: [{index: 2, type: 0}]} ],
6: [ {type: REORDER, moves: [{index: 2, type: 0}]} ],
8: [ {type: REORDER, moves: [{index: 2, type: 0}]} ],
9: [ {type: TEXT, content: "Item 3"} ],
}
步骤三:把差别运用到真正的DOM树上
由于步骤一所构建的 JavaScript 对象树和render出来真正的DOM树的信息、构造是一样的。所以我们能够对那棵DOM树也举行深度优先的遍历,遍历的时刻从步骤二天生的patches对象中找出当前遍历的节点差别,然后举行 DOM 操纵。
function patch (node, patches) {
let walker = {index: 0}
dfsWalk(node, walker, patches)
}
function dfsWalk (node, walker, patches) {
let currentPatches = patches[walker.index] // 从patches拿出当前节点的差别
let len = node.childNodes
? node.childNodes.length
: 0
for (let i = 0; i < len; i++) { // 深度遍历子节点
let 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)
}
})
}
结语
Virtual DOM 算法主如果完成上面步骤的三个函数:element,diff,patch。然后就能够现实的举行运用:
// 1. 构建假造DOM
let tree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: blue'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li')])
])
// 2. 经由过程假造DOM构建真正的DOM
let root = tree.render()
document.body.appendChild(root)
// 3. 天生新的假造DOM
let newTree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li'), el('li')])
])
// 4. 比较两棵假造DOM树的差别
let patches = diff(tree, newTree)
// 5. 在真正的DOM元素上运用变动
patch(root, patches)
道理加1,头发减一堆