react中,render之后返回一个虚拟Dom树,通过对比前后两次render产生的虚拟Dom树,决定如何修改页面上真实Dom树,那么这个对比的过程使用的就是diff算法。
-
web ui 中跨层级的移动非常少,基本都是同层级的移动。
-
拥有相同类型的两个组件产生的DOM结构也是相似的,不同类型的两个组件产生的DOM结构则不近相同。
-
对于同层级的组件,通过唯一的key标记区分。
基于上述条件,react的diff算法采用同层级对比的方式,分别对tree diff、component diff 以及 element diff 进行算法优化。
tree diff
component diff
react组件的对比策略:
1)如果不是同一类型组件,直接标记为dirty component,替换整个组件以及子组件。
2)如果是同一类型组件,比较state和props,记录下变化,再比较children,同样进行
component diff,记录下变化patch,之后再某个时间点统一更新。
3)对于同一类型的组件,有可能其Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
element diff
element diff是整个diff算法的核心。
通过源码可以看到,nextChildren和prevChildren分别表示新的节点和旧的节点,他们以对象的形式存储ReactDOMComponent。
for in 循环遍历新集合nextChildren,通过key查找旧的集合中是否存在新的集合中的元素,如果如果不存在,prevChild即为undefined,如果存在相同节点,也即prevChild === nextChild,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较。这里比较lastIndex目的是为了优化不必要的移动。具体如下:遍历nextChildren过程中,当前节点在preChildren中的位置,如果大于旧的节点中已经访问过节点的最大位置lastIndex,则表示该节点在旧的节点中的位置本身就靠后,因此不需要移动,反之需要移动。
最后,还需要遍历一遍旧的节点,删去preChildren中存在,但是nextchildren中不存在的节点。
补充:
key作为元素的标记,在涉及到数组的动态变更,例如数组新增元素、删除元素或者重新排序等,使用index作为key会导致展示错误的数据,例如:
代码块
{this.state.data.map((v,idx) => <Item key={idx} v={v} />)}
// 开始时:['a','b','c']=>
<ul>
<li key="0">a <input type="text"/></li>
<li key="1">b <input type="text"/></li>
<li key="2">c <input type="text"/></li>
</ul>
// 数组重排 -> ['c','b','a'] =>
<ul>
<li key="0">c <input type="text"/></li>
<li key="1">b <input type="text"/></li>
<li key="2">a <input type="text"/></li>
</ul>
dom进行diff,新老版的都有key=0的组件,react认为同一个组件,则只可能更新组件;
同时应该避免使用Math.random()去作为key:key应该是稳定的,可预测的和独特的。不稳定的key(如由其生成的key Math.random())将导致许多组件实例和DOM节点被不必要地重新创建,这可能导致性能下降和子组件中的丢失状态。可以尝试使用一个全局变量保存当前的key,让key递增。