大三战五渣的我,平时也就只能用用别人的轮子,可总用不顺心,毕竟不知道原理,最近用vue写项目,里面涉及到的Virtual DOM虽然已不是什么新概念,但我也只是据说而已,不知其因此然,既然看到大佬们解析后,那就记录下吧
参考资料:
戴嘉华:https://github.com/livoras/bl...
张歆琳:https://www.jianshu.com/p/616...
王沛:https://www.infoq.cn/article/...前端
首先先了解一下加载一个HTML会发生哪些事情vue
当你用传统的源生api或jQuery去操做DOM时,浏览器会从构建DOM树开始从头至尾执行一遍流程。好比当你在一次操做时,须要更新10个DOM节点,理想状态是一次性构建完DOM树,再执行后续操做。但浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操做,所以会立刻执行流程,最终执行10次流程。显然例如计算DOM节点的坐标值等都是白白浪费性能,可能此次计算完,紧接着的下一个DOM更新请求,这个节点的坐标值就变了,前面的一次计算是无用功。
DOM是很慢的,咱们能够打印一下一个简单的div元素的属性node
这还只是一层而已,真实的DOM会更加庞大,轻微的触碰可能就会致使页面重排,这但是杀死性能的罪魁祸首。而相对于操做DOM对象,原生的JS对象处理起来更快并且简单react
在 JS 和 DOM 之间作了一个缓存。能够类比 CPU 和硬盘,既然硬盘这么慢,咱们就在它们之间加个缓存:既然 DOM 这么慢,咱们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操做内存(Virtual DOM),最后的时候再把变动写入硬盘(DOM)。git
用JS记录节点的类型,属性和子节点
element.jsgithub
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
api
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)
上面的运行结果:浏览器
两棵树的彻底差别比较的时间复杂度为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}, ...] // 用数组存储新旧节点的不一样
上面出现了四种新旧树不一样的状况:
p
变成了div
,将旧节点卸载并装载新节点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"} ], }
由于步骤一所构建的 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,头发减一堆