企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 虚拟dom与diff算法 ## 虚拟dom 本质上是使用js对象结构表示dom树的结构。然后使用这个树构建真正的dom结构。虚拟dom使用diff算法完成了没必要的dom操作,从而优化了效率。 在这里先看一下一个简单的实现dom的方式。 一般情况下被称为转译,本质上是一个携带特定参数的函数调用过程。大多数情况是以ReactElement的形式存在, ~~~ class VNode { constructor(tagName, props = {}, children = []) { this.tagName = tagName; this.props = props; this.children = children; } render() { let createdElement = document.createElement(this.tagName); for (let prop in this.props) { createdElement.setAttribute(prop, this.props[prop]); } if (this.children) { this.children.forEach(child => { createdElement.appendChild(child instanceof VNode ? child.render() : document.createTextNode(item)); }) } return createdElement; } } ~~~ 在使用时 ~~~ const h = (tag, props, children) => new VNode(tag, props, children); ~~~ 在写jsx时,其实所有的jsx都使用了React.creatElement()来生成dom。 在正式react中,经常调用的api有: 1. React.creatElement 通过读入节点的type attributes以及children来进行react的绘制。在使用自定义组件例如<Table xxxxx />时,在浏览器端,调用方式是React.creatElement(Table,{xxxxx}); 这里的Table是作为一个引用被我们使用的。 2. 在构建虚拟DOM对象完成之后,ReactDOM.render将会按下面的原则,尝试将其转换为浏览器可以识别和展示的DOM节点: 如果type包含一个带有String类型的标签名称(tag name)—— 创建一个标签,附带上props下所有attributes。 如果type是一个函数(function)或者类(class),调用它,并对结果递归地重复这个过程。 如果props下有children属性 —— 在父节点下,针对每个child重复以上过程。 ## diff算法 > 1.通过比较key来优化效率,比较出具体哪个dom发生了变化。 > 2.合并操作,当进行setState操作时,React将其标记为脏值,当事件循环结束时,React检查脏值,并重新绘制component。 > 3. 想dom树拆解,只比较同级dom。 如果没有key,比如A节点下有B C两个节点,我们想交换这两个节点时,会先将两个节点unmount 再重新生成这两个节点。 但是有key的话就不一样。 可以直接update这两个节点。 ## 如何优化react的效率呢? 1. 使用shouldComponentUpdate 2. 使用key ### diff算法解析 三个基本原则: 1. DOM中跨层级的操作很少,可以忽略不计。 2. 拥有同类型的两个组件生产相似的树形结构,不同的组件生成不同的树形结构。 3. 同一层的子节点,可以通过唯一ID进行区别。 **tree diff** 基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。 既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同 层级的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。 **component diff** 1. 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。 2. 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。 3. 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。 **element diff** 当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。 **INSERT_MARKUP**,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。 **MOVE_EXISTING**,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。 **REMOVE_NODE**,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。 在elementDiff中涉及到key的问题。 > 首先对新集合的节点进行循环遍历,for (name in nextChildren),通过唯一 key 可以判断新老集合中是否存在相同的节点,if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,if (child._mountIndex < lastIndex),则进行节点移动操作,否则不执行该操作。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。 > **大致流程** > 1. 有一个全局变量updateDepth来标识递归的深度,进行diff算法之前+1,diff算法结束之后-1。当其重新变为0的时候表示整个diff算法结束了,可以拿更新队列diffQueue来更新DOM了。 > 2. Diff算法只对同一个父元素的同级子元素进行对比。如果元素的type和key(如果有的话)相同,视为同一个元素,进行更新;否则替换掉。 > 3. Diff使用了一个局部变量:lastIndex——记录已经处理的旧列表中最靠后的元素。当元素的._mountIndex大于lastIndex的时候,不需要移动元素。因为移动元素不会对前面对元素产生任何影响,因此可以省略这个动作。由于很多时候大部分元素处于这种情况下,因此这个局部变量提升了性能(有时候很明显)。 ## 参考 [diff](https://zhuanlan.zhihu.com/p/20346379)