React中的diff算法

diff作为Virtual DOM的加速器,其算法上的改进优化是React整个界面渲染的基础和性能保障。传统的diff算法,计算一棵树形结构转换成另一颗树形结构的最少操作是通过循环递归对节点进行依次对比,这种方式效率低下,算法复杂度达到O(n^3),其中n是树中节点的总数。而React diff将O(n^3)复杂度的问题转换成O(n)复杂度的问题。

  1. diff策略
  • 策略一: Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
  • 策略二:拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  • 策略三:对于同一层级的一组子节点,它们可以通过唯一id进行区分。

基于以上策略,React分别对tree diff、component diff以及element diff进行算法优化。

1.1 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。
由此可发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以A为根节点的整个树被重新创建。这是一种影响React性能的操作,因此官方不建议进行DOM节点跨层级的操作。

1.2 component diff

React应用是基于组件构建的,对于组件的比较优化侧重于以下几点:

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


    component diff

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

1.3 element diff

对于同一层级的element节点,diff提供了以下3种节点操作:

  • INSERT_MARKUP 插入节点:对全新节点执行节点插入操作
  • MOVE_EXISING 移动节点:组件新集合中有组件旧集合中的类型,且element可更新,即组件调用了receiveComponent,这时可以复用之前的dom,执行dom移动操作
  • REMOVE_NODE 移除节点:此时有两种情况:组件新集合中有组件旧集合中的类型,但对应的element不可更新、旧组建不在新集合里面,这两种情况需要执行节点删除操作
    element 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大,即靠右,说明它不会影响其他元素的位置,因此不用添加到差异队列中,不执行移动操作,反之则进行移动操作。
    image.png

    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操作完成
    那么,除了有可复用节点,新集合当有新插入节点,旧集合有需要删除的节点呢?
    image.png

    对于这种情况,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的不足
image.png

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

优化建议

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容