摘要:但浏览器没这么智能,收到第一个更新请求后,并不知道后续还有次更新操作,因此会马上执行流程,最终执行次流程。从拿出当前节点的差异深度遍历子节点对当前节点进行操作,根据不同类型的差异对当前节点进行操作结语算法主要是实现上面步骤的三个函数,,。
大三战五渣的我,平时也就只能用用别人的轮子,可总用不顺心,毕竟不知道原理,最近用vue写项目,里面涉及到的Virtual DOM虽然已不是什么新概念,但我也只是听说而已,不知其所以然,既然看到大佬们解析后,那就记录下吧
参考资料:
戴嘉华:https://github.com/livoras/bl...
张歆琳:https://www.jianshu.com/p/616...
王沛:https://www.infoq.cn/article/...
首先先了解一下加载一个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,头发减一堆
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/102374.html
摘要:具体而言,就是每次数据发生变化,就重新执行一次整体渲染。而给出了解决方案,就是。由于只关注,通过阅读两个库的源码,对于的定位有了更深一步的理解。第二个而且,技术本身不是目的,能够更好地解决问题才是王道嘛。 前言 React 好像已经火了很久很久,以致于我们对于 Virtual DOM 这个词都已经很熟悉了,网上也有非常多的介绍 React、Virtual DOM 的文章。但是直到前不久...
摘要:二原理每个都有两个,一个是新的,一个是原来的。三实现过程四算法的理解与实现本质上就是在和之间做了一个缓存。将差异的应用到真正的树上对真实上的树进行深度优先遍历,在所有的差异列表中找出当前遍历的节点差异,然后根据不同进行操作。 React Virtual DOM 一、概念 在react中,对于每个DOM对象都有一个相应的虚拟DOM对象,相当于DOM对象的轻量级副本 由于是Virtual...
摘要:本文为笔者通过实际操作,实现了一个非常简单的,加深对现今主流前端框架中的理解。用对象表示树是用对象表示,并存储在内存中的。如果类型不一致,那么属性一定是被更新的。如果有不相等的属性,则认为发生改变,需要处理的变化。 众所周知,对前端而言,直接操作 DOM 是一件及其耗费性能的事情,以 React 和 Vue 为代表的众多框架普遍采用 Virtual DOM 来解决如今愈发复杂 Web ...
摘要:不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。它们分别是标签名属性和子元素对象。我们先来看下页面的更新一般会经过几个阶段。元素有可能是数组的形式,需要将数组解构一层。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的两大前端框架,React和Vue,都不约...
摘要:目录前言问题的提出模板引擎和结合的实现编译原理相关模版引擎的词法分析语法分析与抽象语法树代码生成完整的结语前言本文尝试构建一个前端模板引擎,并且把这个引擎和进行结合。于是就构思了一个方案,在前端模板引擎上做手脚。 作者:戴嘉华 转载请注明出处并保留原文链接( https://github.com/livoras/blog/issues/14 )和作者信息。 目录 前言 问题的提出...
摘要:变化的只有种更新和删除。页面的元素的数量随着而变。四总结本文详细介绍如何实现一个简单的算法,再根据计算出的差异去更新真实的。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的两大前端框架,React 和 Vue,都不约而同的借助 Virtual DOM 技术提高页面的渲染...
阅读 2488·2021-10-19 11:45
阅读 2465·2021-09-30 09:56
阅读 1433·2021-09-30 09:47
阅读 592·2019-08-30 15:53
阅读 1835·2019-08-30 15:44
阅读 585·2019-08-30 12:52
阅读 1086·2019-08-30 11:16
阅读 1607·2019-08-29 16:36