React Diff 算法

0 前言

React的VirtualDOM可谓是前端领域中革命性的创新,而高效的Diff算法又极大的优化了状态的对比,不同的状态对应的就是不同的UI界面。本文将简要介绍Diff算法中的精华。

1 Diff算法

1.1背景

"树"是一种常见的数据结构,对比"树"的结构在计算机相关相关领域有广泛的应用。具体到前端开发而言,Web的UI界面就是由DOM树构成,DOM作为一种有序树结构,当DOM树结构发生变化时,浏览器会重新解析渲染DOM树。造成有序树的节点变化的基本操作方式有三种:重标记(relabel),删除(delete)和插入(insert),如下图自上而下所示。

重标记(relabel L1 to L2)
重标记
删除节点(delete node L2)
删除节点
插入节点(insert L2 to L1)
插入节点

1.2 标准的Diff算法

标准的Tree-Diff算法,使用了严谨的代价函数(Cost Function),适用于很多需要树结构对比的场景,如:计算生物学、图像分析、结构化文本数据库等。然而其算法的时间复杂度为O(n^3),在浏览器环境中,要达到每次更新都可以整体刷新界面的目的,这样高的算法复杂度会有性能问题。

2 React Diff算法(reconciliation algorithm)

React开发团队结合Web界面的特点对标准的Diff算法做出了优化,使得其时间复杂度降低为O(n)。这一算法,即reconciliation algorithm,常见中文翻译为“协调算法”,个人更倾向于“权衡算法”这一更形象的翻译,因为该算法所实现的就是权衡利弊之后的简化。这种算法的基本假设如下:

  • Web UI中DOM节点跨层级的移动操作较少,可以忽略。
  • 两个相同组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
  • 对于同一层次的一组相同类型的子节点,它们可以通过唯一的key进行区分。

2.1 单一节点对比

为了对比两个树结构,需要先能够比较两个节点,在React中就是Virtual DOM树(以下简称“VD树”)的节点,而VD树节点的不同又分两种情况:

  • 节点类型不同
  • 节点类型相同但属性(props)不同

2.1.1 节点类型不同

当VD树中同一节点位置前后节点类型不同时,React直接删除旧的节点,然后创建并插入新的节点,使用的节点基本操作方法为删除和插入。

renderA: <OldNode />
renderB: <NewNode />
=> [removeNode <OldNode />], [insertNode <NewNode />]

在React组建的生命周期方法中,老节点OldNode的componentWillUnmount()方法最先触发,之后新节点NewNode的componentWillMount()和componentDidMount()方法依次触发。

2.1.2 节点类型相同但属性不同

节点类型相同但属性不同的情况下,React会对VD数节点的属性进行重设,从而实现节点的转换,使用的节点基本操作方法为重标记。因为React组件的实例保持不变,该组件的state也会保持不变,组件的componentWillReceiveProps()、componentWillUpdate()、render()方法依次触发。

renderA: <Node className="old" />
renderB: <Node className="new" />
=> [replaceAttribute className "new"]

VD树节点的style属性稍有不同,因为传入的是对象而不是字符串,React会只更新改变了的样式属性。

如果开发者能够明确节点是否有变化,则可以通过shouldComponentUpdate(nextProp, nextState)方法来决定是否进行Diff运算。

2.2 逐层进行节点对比(分层求异)

React只会对同一层级(下图中相同颜色方框内)的DOM节点进行比较,即同一个父节点下的所有子节点。如果发现已经不存在的节点,则该节点及其子节点会被完全删除掉,不再作进一步的比较。这样只需要对树进行一次遍历,便能完成整个DOM树的比较。这一方法正是基于前一假设,即不同类型的组件产生不同的VD树结构。


逐层进行节点对比

例如,在下图的VD树结构转换过程中,看似应该是把整个A节点从根节点插入到D节点中,最直观的操作方式应该是:

Root.remove(A).find(D).append(A)

然而,React的“权衡算法”只会简单对比同一层级的节点,不同层级的节点只有删除和插入。所以,React实际的操作方式却是直接删除节点A,至于A的子节点B和C则直接被忽略了;而当发现D节点多了一个子节点A时,则会插入一个新的节点A作为自己点。过程如下:

A.destroy();
A = new A();
A.append(new B());
A.append(new C());
D.append(A);

这样的操作看似是把简单的问题变得复杂了,在这一例子中的确如此,毕竟根据NFL定理(no free lunch theorem),任何优化都是有代价的,Reconciliation algorithm也不例外。之所以称之为“权衡算法”,也正是因为这一方法以忽略不同类型节点的子节点的对比为代价,进而大大降低了Diff算法的时间复杂度。

鉴于这一情况,为提高性能,应尽量避免进行DOM节点跨层级的操作。此外,保持稳定的DOM结构也有助于性能提升,可以通过类名和CSS来控制节点的显示隐藏。

2.3 列表节点的对比

2.1和2.2中分别介绍了React diff算法对单一节点,和不在同一层中的节点的对比方式。与前两者不同的是,一组列表节点往往有相同的类型和属性,此外,列表节点的常见操作包含插入、删除和排序。如果每个列表节点没有唯一标识,则React无法识别每一个节点,那么只能更新所有列表节点造成效率低下。

例如,要在下图的列表节点B和C中间插入节点F,如果React无法识别每一个节点,则只能依次更新所有节点,如果节点数量很大,会有明显的性能损耗。



这时,“key”这个属性就要登场了,React通过key这个属性发现更新前后相同的列表节点,因此无需进行相同节点删除和创建,只需要将旧的节点的位置进行移动。如果有旧的节点被删除或新的节点被插入,也不会造成其他节点被重新创建。
再上图的例子中,React会通过每个节点的唯一标识(key),准确找到插入新节点F的位置,同时保留其他节点。



如果列表节点能够获取到唯一的id属性,那么选择id作为key最好不过,如果没有id或较难获取,则退而求其次选用.map(item, index)方法中的索引index作为key。

3 总结

  • React Diff 通过“权衡算法”,把复杂度O(n^3)的问题转化为复杂度O(n)的问题;
  • 通过”分层求异“策略进行优化,假设不同类型的组件产生不同的DOM树结构,主动放弃了对不同父节点的子节点的对比;
  • 开发组建时,保持稳定的DOM结构、避免跨层级移动DOM节点,有助于性能提升;
  • 通过设置唯一key的策略,对列表节点进行了diff算法优化。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容