浅谈React中的diff

### 简介  

diff算法在React中处于主导地位,是React V-dom和渲染的性能保证,这也是React最有魅力、最吸引人的地方。  

React一个很大一个的设计有点就是将diff和V-dom的完美结合,而高效的diff算法可以让用户更加自由的刷新页面,让开发者也能远离原生dom操作,更加开心的撸代码。    

但总所周知,普适diff的复杂度对于大量dom对比会出现严重的性能问题,React团队对diff的优化可以让React能够在服务端渲染,到底React的diff做了什么优化呢?本文来简单探讨一下!



### React的diff策略
1. 策略一:忽略Web UI中DOM节点跨层级移动;
2. 策略二:拥有相同类型的两个组件产生的DOM结构也是相似的,不同类型的两个组件产生的DOM结构则不近相同
3. 策略三:对于同一层级的一组子节点,通过分配唯一唯一id进行区分(key值)
在Web UI的场景下,基于以上三个点,React对tree diff、component diff、element diff进行优化,将普适diff的复杂度降低到一个数量级,保证了整体UI界面的构建性能!


### 三个优化


> tree diff

基于策略一,React的做法是把dom tree分层级,对于两个dom tree只比较同一层次的节点,忽略Dom中节点跨层级移动操作,只对同一个父节点下的所有的子节点进行比较。如果对比发现该父节点不存在则直接删除该节点下所有子节点,不会做进一步比较,这样只需要对dom tree进行一次遍历就完成了两个tree的比较。  
==那么对于跨层级的dom操作是怎么进行处理的呢?==下面通过一个图例进行阐述  





两个tree进行对比,右边的新tree发现A节点已经没有了,则会直接销毁A以及下面的子节点B、C;在D节点上面发现多了一个A节点,则会重新创建一个新的A节点以及相应的子节点。  
具体的操作顺序:create A → create B → creact C → delete A。


==优化点注意:==  


保证稳定dom结构有利于提升性能,不建议频繁真正的移除或者添加节点


> component diff

React应用是基于组件构建的,对于组件的比较优化侧重于以下几点:  
    1. 同一类型组件遵从tree diff比较v-dom树
    2. 不通类型组件,先将该组件归类为dirty component,替换下整个组件下的所有子节点
    3. 同一类型组件Virtual Dom没有变化,React允许开发者使用shouldComponentUpdate()来判断该组件是否进行diff,运用得当可以节省diff计算时间,提升性能





如上图,当组件D → 组件G时,diff判断为不同类型的组件,虽然它们的结构相似甚至一样,diff仍然不会比较二者结构,会直接销毁D及其子节点,然后新建一个G相关的子tree,这显然会影响性能,官方虽然认定这种情况极少出现,但是开发中的这种现象造成的影响是非常大的。


==优化点注意:==  

对于同一类型组件合理使用shouldComponentUpdate(),应该避免结构相同类型不同的组件

> element diff

对于同一层级的element节点,diff提供了以下3种节点操作:  
    1. INSERT_MARKUP 插入节点:对全新节点执行节点插入操作
    2. MOVE_EXISING 移动节点:组件新集合中有组件旧集合中的类型,且element可更新,即组件调用了receiveComponent,这时可以复用之前的dom,执行dom移动操作
    3. REMOVE_NODE 移除节点:此时有两种情况:组件新集合中有组件旧集合中的类型,但对应的element不可更新、旧组建不在新集合里面,这两种情况需要执行节点删除操作


**key值diff中重要性**  
  



一般diff在比较集合[A,B,C,D]和[B,A,D,C]的时候会进行全部对比,即按对应位置逐个比较,发现每个位置对应的元素都有所更新,则把旧集合全部移除,替换成新的集合,如上图,但是这样的操作在React中显然是复杂、低效、影响性能的操作,因为新集合中所有的元素都可以进行复用,无需删除重新创建,耗费性能和内存,只需要移动元素位置即可。   

React对这一现象做出了一个高效的策略:允许开发者对同一层级的同组子节点添加唯一key值进行区分。意义就是代码上的一小步,性能上的一大步,甚至是翻天覆地的变化!



==重点来了,React通过key是如何进行element管理的呢?为何如此高效?==  


算法改进:  

React会先进行新集合遍历,for(name in nextChildren),通过key值判断两个对比集合中是否存在相同的节点,即if(prevChild === nextChild),如何为true则进行移动操作,在此之前,需要执行被移动节点在新旧(child._mountIndex)集合中的位置比较,if(child._mountIndex < lastIndex)为true时进行移动,否则不执行该操作,这实际上是一种顺序优化,lastIndex是不断更新的,表示访问过的节点在集合中的最右的位置。若当前访问节点在旧集合中的位置比lastIndex大,即靠右,说明它不会影响其他元素的位置,因此不用添加到差异队列中,不执行移动操作,反之则进行移动操作。



下图示例:  





* nextIndex = 0,lastIndex = 0,从新集合中获取B,在旧集合中发现相同节点B,旧集合中:B._mountIndex = 1,child._mountIndex < lastIndex ==> false,不执行移动操作,更新lastIndex = Math.max(prevChild._mountIndex, lastIndex), prevChild._mountIndex === B._mountIndex ==> true,更新B在新集合中的位置:prevChild._mountIndex = nextIndex,在新集合中:B._mountIndex = 0,nextIndex++,进行下一个节点判断。
* nextIndex = 1,lastIndex = 1,从新集合中获取A,在旧集合中发现相同节点A,旧集合中:A._mountIndex = 0,child._mountIndex < lastIndex ==> true,对A进行移动操作enqueueMove(this, child._mountIndex, toIndex),toIndex是A要被移动到的位置,更新lastIndex = Math.max(prevChild._mountIndex, lastIndex),更新A在新集合中的位置prevChild._mountIndex = nextIndex,在新集合中:A._mountIndex = 1,nextIndex++,进行下一个节点判断。
* nextIndex = 2,lastIndex = 1,从新集合中获取D,在旧集合中发现相同节点D,旧集合中:D._mountIndex = 3,child._mountIndex < lastIndex ==> false,不执行移动操作,更新lastIndex = Math.max(prevChild._mountIndex, lastIndex), prevChild._mountIndex === D._mountIndex ==> true,更新D在新集合中的位置:prevChild._mountIndex = nextIndex,在新集合中:D._mountIndex = 2,nextIndex++,进行下一个节点判断。
* nextIndex = 3,lastIndex = 3,从新集合中获取C,在旧集合中发现相同节点C,旧集合中:C._mountIndex = 2,child._mountIndex < lastIndex ==> true,对C进行移动操作enqueueMove(this, child._mountIndex, toIndex),toIndex是C要被移动到的位置,更新lastIndex = Math.max(prevChild._mountIndex, lastIndex),更新C在新集合中的位置prevChild._mountIndex = nextIndex,在新集合中:A._mountIndex = 3,nextIndex++,进行下一个节点判断。
* 由于是最后一个节点,diff操作完成

==那么,除了有可复用节点,新集合当有新插入节点,旧集合有需要删除的节点呢?==  
下图示例:  





对于这种情况,React则是执行以下步骤:  
* nextIndex = 0,lastIndex = 0,从新集合中获取B,在旧集合中发现相同节点B,旧集合中:B._mountIndex = 1,child._mountIndex < lastIndex ==> false,不执行移动操作,更新lastIndex = 1,更新B在新集合中的位置,nextIndex++,进行下一个节点判断。
* nextIndex = 1,lastIndex = 1,从新集合中获取E,在旧集合中没有发现相同节点E,nextIndex++进入下一个节点判断。
* nextIndex = 2,lastIndex = 1,从新集合中获取C,在旧集合中发现相同节点C,旧集合中:C._mountIndex = 2,child._mountIndex < lastIndex ==> false,不对C进行移动操作,更新lastIndex = 2,更新C在新集合的位置,nextIndex++,进行下一个节点判断。
* nextIndex = 3,lastIndex = 2,从新集合中获取A,在旧集合中发现相同节点A,旧集合中:A._mountIndex = 0,child._mountIndex < lastIndex ==> true,对A进行移动操作,更新lastIndex = 2,更新A在新集合中的位置,nextIndex++进入下一个节点判断。
* 当完成新集合所有节点中的差异对比后,对旧集合进行遍历,判读旧集合中是否存在新集合中不存在的节点,此时发现D节点符合判断,执行删除D节点的操作,diff操作完成。


### 优化后diff的不足


世上没有百分之百完美算法,React的diff也有自己的不足之处,比如新旧集合元素全部可以复用,只是新集合中将旧集合最后一个元素放到了第一个位置,短板就会出现
下图示例:





按照上述顺序优化,则旧集合中D的位置是最大的,最少的操作只是将D移动到第一位就可以了,实际上diff操作会移动D之前的三个节点到对应的位置,这种情况会影响渲染的性能。


==优化建议==
  
在开发过程中,同层级的节点添加唯一key值可以极大提升性能,尽量减少将最后一个节点移动到列表首部的操作,当节点达到一定的数量以后或者操作过于频繁,在一定程度上会影响React的渲染性能。比如大量节点拖拽排序的问题。


总之,React为我们提供优秀的diff算法,使我们能够在实际开发中happy的撸代码,但也不是说可以“随意”去构建我们的应用,根据diff的特点,在具体场景中取长补短,规避一些算法上面的短板也是有利于提升应用整体的性能。




参考资料:  
* 《深入React技术栈》陈屹 ——3.5章节
  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值