一、VirtualDOM
1、概念
VirtualDOM一直是React最著名,最神秘的特点。也是其高效迅速的核心。
virtualDOM其实很好理解:FaceBook的工程师认为传统的框架,频繁的操作DOM代价很大。性能和速度很低,想找一种东西替代DOM,但又能有DOM的嵌套结构和属性。于是FaceBook工程师们创造出virtualDOM。本质上virtualDOM就是一个json对象,用来描述真实DOM应有的属性和子集。拥有和DOM一样的嵌套关系形成的virtualDOM树。
一个真实节点有很多属性,但是很多属性是绘制DOM相关的(比如offset,scrollTop这种,节点渲染之后添加的跟节点样式相关的属性)。virtualDOM对这些不常见属性暂不考虑(实际上这些绘制DOM的属性都在react-dom中),抽出重要的属性,以及嵌套关系记录下来(高效),放到内存中(迅速),这样减少了很多用不到的属性。 当virtualDOM树生成出来之后,只要按照特地的方法解析(JSX),就会快速的生成出DOM了。
本质上来说,virtualDOM只是一个简单的JS对象,并且最少包含tag、props和children三个属性。不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。它们分别是标签名(tag)、属性(props)和子元素对象(children)。
{
tag: "div",
props: {},
children: [
"Hello World",
{
tag: "ul",
props: {},
children: [{
tag: "li",
props: {
id: 1,
class: "li-1"
},
children: ["第", 1]
}]
}
]
}
生成对应的dom为
Hello World
-
第1
2、为什么需要virtualDOM
virtualDOM最大的特点是将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。如 React 就借助virtualDOM实现了服务端渲染、浏览器渲染和移动端渲染等功能。
此外,在进行页面更新的时候,借助virtualDOM,DOM 元素的改变可以在内存中进行比较,再结合框架的事务机制将多次比较的结果合并后一次性更新到页面,从而有效地减少页面渲染的次数,提高渲染效率。
二、diff算法
1、作用
Diff算法的作用是用来计算出 Virtual DOM 中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面。
2、传统diff算法
通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3) ,n是树的节点数,这个有多可怕呢?——如果要展示1000个节点,得执行上亿次比较。。即便是CPU快能执行30亿条命令,也很难在一秒内计算出差异。
3、diff策略
React用 三大策略 将O(n^3)复杂度 转化为 O(n)复杂度
策略一(tree diff):
Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
策略二(component diff):
拥有相同类的两个组件 生成相似的树形结构,
拥有不同类的两个组件 生成不同的树形结构。
策略三(element diff):
对于同一层级的一组子节点,通过唯一id区分。
(1)tree diff
(1)、React通过updateDepth对Virtual DOM树进行层级控制。
(2)、对树分层比较,两棵树 只对同一层次节点 进行比较。如果该节点不存在时,则该节点及其子节点会被完全删除,不会再进一步比较。
(3)、只需遍历一次,就能完成整棵DOM树的比较。
注:diff只简单考虑同层级的节点位置变换,如果是跨层级的话,只有创建节点和删除节点的操作。
(2)component diff
React对不同的组件间的比较,有三种策略
(1)、同一类型的两个组件,按原策略(层级比较)继续比较Virtual DOM树即可。
(2)、同一类型的两个组件,组件A变化为组件B时,可能Virtual DOM没有任何变化,如果知道这点(变换的过程中,Virtual DOM没有改变),可节省大量计算时间,所以 用户 可以通过 shouldComponentUpdate() 来判断是否需要判断计算。
(3)、不同类型的组件,将一个(将被改变的)组件判断为dirty component(脏组件),从而替换 整个组件的所有节点。
注意:如果组件D和组件G的结构相似,但是 React判断是不同类型的组件,则不会比较其结构,而是删除 组件D及其子节点,创建组件G及其子节点。
(3)element diff
当节点处于同一层级时,diff提供三种节点操作:删除、插入、移动。
插入:组件 C 不在集合(A,B)中,需要插入
删除:(1)组件 D 在集合(A,B,D)中,但 D的节点已经更改,不能复用和更新,所以需要删除 旧的 D ,再创建新的。
(2)组件 D 之前在 集合(A,B,D)中,但集合变成新的集合(A,B)了,D 就需要被删除。
移动:组件D已经在集合(A,B,C,D)里了,且集合更新时,D没有发生更新,只是位置改变,如新集合(A,D,B,C),D在第二个,无须像传统diff,让旧集合的第二个B和新集合的第二个D 比较,并且删除第二个位置的B,再在第二个位置插入D,而是 (对同一层级的同组子节点) 添加唯一key进行区分,移动即可。