diff算法在React中处于主导地位,是React V-dom和渲染的性能保证,这也是React最有魅力、最吸引人的地方。
React一个很大一个的设计有点就是将diff和V-dom的完美结合,而高效的diff算法可让用户更加自由的刷新页面,让开发者也能远离原生dom操做,更加开心的撸代码。
但总所周知,普适diff的复杂度对于大量dom对比会出现严重的性能问题,React团队对diff的优化可让React可以在服务端渲染,到底React的diff作了什么优化呢?本文来简单探讨一下!react
tree diff算法
基于策略一,React的作法是把dom tree分层级,对于两个dom tree只比较同一层次的节点,忽略Dom中节点跨层级移动操做,只对同一个父节点下的全部的子节点进行比较。若是对比发现该父节点不存在则直接删除该节点下全部子节点,不会作进一步比较,这样只须要对dom tree进行一次遍历就完成了两个tree的比较。
==那么对于跨层级的dom操做是怎么进行处理的呢?==下面经过一个图例进行阐述app
两个tree进行对比,右边的新tree发现A节点已经没有了,则会直接销毁A以及下面的子节点B、C;在D节点上面发现多了一个A节点,则会从新建立一个新的A节点以及相应的子节点。
具体的操做顺序:create A → create B → creact C → delete A。dom
优化建议
性能
保证稳定dom结构有利于提高性能,不建议频繁真正的移除或者添加节点
复制代码
component diff优化
React应用是基于组件构建的,对于组件的比较优化侧重于如下几点:
1. 同一类型组件听从tree diff比较v-dom树 2. 不通类型组件,先将该组件归类为dirty component,替换下整个组件下的全部子节点 3. 同一类型组件Virtual Dom没有变化,React容许开发者使用shouldComponentUpdate()来判断该组件是否进行diff,运用得当能够节省diff计算时间,提高性能this
如上图,当组件D → 组件G时,diff判断为不一样类型的组件,虽然它们的结构类似甚至同样,diff仍然不会比较两者结构,会直接销毁D及其子节点,而后新建一个G相关的子tree,这显然会影响性能,官方虽然认定这种状况极少出现,可是开发中的这种现象形成的影响是很是大的。spa
优化建议
设计
对于同一类型组件合理使用shouldComponentUpdate(),应该避免结构相同类型不一样的组件
复制代码
element diffcode
对于同一层级的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++,进行下一个节点判断。
==那么,除了有可复用节点,新集合当有新插入节点,旧集合有须要删除的节点呢?==
下图示例:
对于这种状况,React则是执行如下步骤:
世上没有百分之百完美算法,React的diff也有本身的不足之处,好比新旧集合元素所有能够复用,只是新集合中将旧集合最后一个元素放到了第一个位置,短板就会出现 下图示例:
按照上述顺序优化,则旧集合中D的位置是最大的,最少的操做只是将D移动到第一位就能够了,实际上diff操做会移动D以前的三个节点到对应的位置,这种状况会影响渲染的性能。
优化建议
在开发过程当中,同层级的节点添加惟一key值能够极大提高性能,尽可能减小将最后一个节点移动到列表首部的操做,当节点达到必定的数量之后或者操做过于频繁,在必定程度上会影响React的渲染性能。好比大量节点拖拽排序的问题。
复制代码
总之,React为咱们提供优秀的diff算法,使咱们可以在实际开发中happy的撸代码,但也不是说能够“随意”去构建咱们的应用,根据diff的特色,在具体场景中取长补短,规避一些算法上面的短板也是有利于提高应用总体的性能。
参考资料: