上一篇文章中,DOM树的信息可以用JavaScript对象来表示,反过来,可以根据这个用JavaScript对象表示的树结构来真正构建一颗DOM树。
用JavaScript对象表示DOM信息和结构,当状态变更的时候,重新渲染这个JavaScript的对象结构,当然这样做,其实并没有更新到真正的页面上。
但是可以用新渲染的对象树和旧的树进行对比,记录这两棵树的差异。记录下来的不同就是我们需要对页面真正的DOM操作,然后把它们应用在真正的DOM树上,页面才会真正变更。
Virtual DOM算法的实现步骤
1.用JavaScript对象结构表示DOM树的结构,然后用这个树构建一个真正的DOM树,插到文档中。
//函数参数的定义,解构赋值中的一个用途
function Element({tagName,props,children}){
if(!(this instanceof Element)){
return new Element({tagName,props,children});
}
this.tagName = tagName;
this.props = props || {};
this.children = children || [];
}
2.当状态变化的时候,重新构建一颗新的对象树,然后用新的树和旧的树进行比较,记录两颗树的差异。
3.把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了。
Virtual DOM实质上是在JS和DOM之间做了一个缓存,可以类比cpu和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存,既然DOM这么慢,就在它们JS和DOM之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。
步骤一:见前一篇博客
步骤二:比较两颗虚拟DOM树的差异
1.深度优先遍历,记录差异
在实际的代码中,会对新旧两颗树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记。
在深度优先遍历的时候,每当遍历到一个节点的时候就会和新的DOM树进行比较,如果有差异,就会记录到一个对象里面。
2.这里会涉及到差异类型
(1)替换掉原来的节点,例如把上面的div换成了section
(2)移动、删除、新增子节点
(3)修改节点的属性
(4)对于文本节点,文本内容可能会改变。
步骤三:把差异引用到真正的DOM树上
因为步骤一所构建的JavaScript对象树和render出来的真正的DOM树的信息、结构是一样的。所以我们可以对那颗DOM树也进行深度优先遍历,遍历的时候从步骤二生成的差异对象中找出当前遍历的节点差异,然后进行DOM操作。
虚拟DOM算法的思想
标准的DOM机制是用户在应用上的操作实际反映的是直接对真实的dom进行操作,而在React中,用户在应用中对dom的操作实际上是对虚拟dom的操作,用户的操作产生的数据改变或者state变量改变都会应用到虚拟dom上,之后再批量的对这些更改进行diff算法计算,对比操作前后的虚拟dom树,把更改后的变化再同步到真实dom上。(在虚拟dom上执行多次修改,在真实dom中,只会执行一次dom操作,因为在React虚拟dom机制中,它会把所有的操作都合并,只会对比刚开始的状态和最后操作的状态,两者中找出不同,然后再同步到真实dom中。)
react的独特的diff算法
diff算法的处理方法,对操作前后的dom树的同一层的节点进行对比,一层一层对比。,这样时间复杂度就为o(n)。
比较两颗虚拟DOM树的差异
在用js对象表示DOM结构后,当页面状态发生变化时需要操作DOM的时候,我们可以先通过虚拟DOM计算出新的DOM树和旧的DOM树之间的最小修改量,然后最后一次性的将差异修改到真实DOM上。
下图是一个简单的两个虚拟DOM之间的差异
但是真实的场景下的DOM结构很复杂,我们必须借助于一个有效的DOM树比较算法。
-(1)如何比较两个DOM树
- (2)如何记录节点之间的差异
如何比较两个DOM树
计算两颗树之间的差异的常规算法复杂度是O(n的3次方),一个文档的DOM结构有上百个节点是正常的情况,这种复杂度不能应用于实际项目。所以,针对前端的具体情况,我们很少跨级别的修改DOM节点,通常是修改节点的属性、调整子节点的顺序、添加子节点等。所以我们一般对同级别节点进行比较,避免了diff算法的复杂性。对同级节点进行比较的常用方法就是深度优先遍历。在深度优先遍历过程中,每个节点都会有一个唯一的标记。在深度优先遍历的时候,每遍历到一个节点就把该节点和新的树进行对比。如果有差异就记录到一个对象里面。
如何记录节点之间的差异
由于我们对DOM采取的是同级比较,因此节点之间的差异可以归结为四种类型
- 修改节点属性:用PROPS表示
- 修改文本节点内容: 用TEXT表示
- 替换原有节点,用REPLACE表示
- 调整子节点,包括移动、删除、添加等,用RECODER表示
对于节点之间的差异,可以很方便的使用上述四种方式进行记录,比如当旧节点被替换的时候:
{type:REPLACE,node newNode}
当旧节点的属性修改时
{type:PROPS,props newProps}
每个节点都有一个编号,如果对应的节点有变化,那么就把相应变化的类别记录下来即可。最后将这个差异对真实DOM做最小化的修改。