浅析React的Reconciliation(Diff)算法

阅读前提

阅读本文的前提,是先把这两篇文章看懂,因为它们已经说清楚的部分,本文不打算再赘述,本文更多的是从笔者的角度去剖析这些知识点,以便大家能够更好地理解它们。
React - Reconciliation
React 源码剖析系列 - 不可思议的 react diff

前言

前端框架诸如React/Vue之类的最大贡献之一,就是将广大的前端开发人员从繁琐的DOM操作中解放出来(即便有jQuery这样的优秀DOM操作库助力,随着应用复杂度的提升,DOM的操作依然会变得异常繁琐)。通过“数据驱动视图”的方式,让开发人员的注意力集中在数据和逻辑上,从而提高开发效率。也让前端开发逐渐摆脱“应用中负责还原终端设计稿”的附属地位,逐渐往构建独立复杂应用的方向发展。

数据驱动视图

一个典型的前端应用场景,就是当用户打开页面后,前端先调用后端接口拿到数据,再将数据应用到页面模板上,最终生成完整的页面呈现给用户。前端框架所做的事情,就是将数据存储下来,在保证相同数据一定生成相同页面的前提下,即可将前端的最终关注点由“页面”前置为“数据",最终可用数据来表达\持久化/还原应用的状态

现代框架都推崇使用组件来封装页面的一部分信息和功能,再通过不同组件的拼装来构建整个页面。以React为例,React的每个组件都有一个render方法用来输出组件当前状态下的界面信息(虚拟DOM)。而随之而来的一个问题就是,当组件的状态发生变化时,组件的render方法会返回不同的界面信息,此时该如何更新用户界面呢?

如果简单粗暴点,直接将老的DOM节点全部卸载,然后重新构建新的DOM节点再挂载上去,当然也是可以的,但是这样的做法在前端场景下的成本就太高了,因为大家都知道,DOM节点的创建和销毁都是需要耗费巨大的系统资源的。

还有另一种选择,由于组件的render方法在更新前后都会返回树状的界面描述信息,所以问题等价于如何将一棵树转换成另一棵树。传统的转换算法复杂度极高O(n3),完全不适用于前端的场景,所以我们需要寻求一种更聪明、更高效的转换算法。

React的Reconciliation(Diff)算法

为了解决这个问题,React基于前端的应用场景,设计出一种聪明的启发式Diff算法,将O(n3)复杂度的问题转换成O(n)复杂度的问题,大大提高了界面更新的效率。具体的算法设计思路和细节,请参考React 源码剖析系列 - 不可思议的 react diff相关篇幅,本文不再赘述,下面我将抛出我自己的几点疑问和相关的思考。

一、为什么要有Diff算法?

为了重用DOM节点。由于DOM节点的创建和销毁成本很高,又由于在前端的场景下,大面积的界面更新是很少的,更常见的是局部的小范围更新,所以大部分的界面在大多数时候都是保持不变的。说明我们可以只做局部更新,以避免频繁创建和销毁DOM所带来的巨大资源开销。

二、何时局部更新?何时整体切换?

我举一个常见的栗子🌰。假如我们正在手机上申请办理信用卡,那么常见的场景是:

第一步/第一个界面/表单组件:填写长长的表单提交个人信息;
第二步/第二个界面/刷脸组件:刷脸录入人脸信息。

我们假设第一个界面有部分保存已填信息到后端的功能,以便用户再次进入时,可以回填之前已经填过的用户信息。我们再假设所有操作都在同一个HTML页面上下文中完成,表单界面和刷脸界面都是用组件来进行封装,我们称之为表单组件和刷脸组件。

接下来我们模拟一下用户的使用场景,用户首先打开第一个界面,我们会先展示空表单给用户,然后等待后端接口返回用户之前已填的部分信息,那么此时第一个界面有两种状态:初始空状态和部分信息回填状态。两种状态的切换时机就是在接口数据返回前后。那么当接口数据返回时,我们有必要将之前空状态下渲染的表单节点全部卸载删除吗?当然没必要,我们只需要将对应的信息回填到表单上展示给用户即可。这就是组件在状态切换前后所引发的局部更新。

当用户填完所有信息提交之后,他将被引导至第二个界面,也就是刷脸界面。由于第二个界面与第一个界面长得完全不同,此时已没有重用DOM节点的意义和价值。那么此时我们只需要放心地卸载第一个表单组件,然后初始化并挂载第二个刷脸组件即可。此时我们不用担心由此引发的资源消耗,因为这是由于业务变化导致的功能区变化,属于必要的资源开销。

那么我来总结一下就是,每一个功能区都有它自己的生命周期,在它的生命周期以内只需要做状态变更,也就是局部更新即可。当功能区生命周期结束时,即可放心大胆地将其卸载,然后初始化并装载下一个功能区即可。这里的功能区可以是一个组件,也可以包含多个组件。

三、如何判断新老节点是否有比较的必要?

如果读者已经理解了前提中引用的两篇文章的话,那么这个问题的答案是已知的。即通过判断新老节点是否是相同类型的节点来做判断,对于html标签而言,直接判断标签名是否相同即可,而对于组件而言,也是直接判断组件的类型是否相同。组件就是一棵DOM树的描述对象标识,由于React推荐并且认为同类组件会产出相似的树形结构,所以同类组件才有深入diff比较的必要,同理不同的组件由于产出的DOM树不同,也就没有进一步比较的必要,直接做简单的删除和创建操作即可。

四、为什么要引入key属性?

先抛结论:React会为每一个虚拟DOM设置key值,要么你直接指定key,要么React用数组下标为你生成一个。具体代码如下:

// node_modules/react-dom/lib/traverseAllChildren.js
function getComponentKey(component, index) {
  if (component && typeof component === 'object' && component.key != null) {
    return KeyEscapeUtils.escape(component.key);
  }
  return index.toString(36);
}

那么React为什么要这么做呢?根据前面所言,新老节点有继续比较的价值的前提,是它们的类型要相同。那么是不是类型相同就一定有比较的价值呢?并不竟然,在一种典型的场景下就不一定,那就是子节点的位置变化。这种情况下,我们就必须显示地通过指定唯一标识key值的方式告知React这种情况的发生,否则React就只能“傻乎乎”的逐个比较了。所以实际在React中新老节点比较的方法如下:

// node_modules/react-dom/lib/shouldUpdateReactComponent.js
function shouldUpdateReactComponent(prevElement, nextElement) {
  var prevEmpty = prevElement === null || prevElement === false;
  var nextEmpty = nextElement === null || nextElement === false;
  if (prevEmpty || nextEmpty) {
    return prevEmpty === nextEmpty;
  }

  var prevType = typeof prevElement;
  var nextType = typeof nextElement;
  if (prevType === 'string' || prevType === 'number') {
    return nextType === 'string' || nextType === 'number';
  } else {
    return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key;
  }
}

而在Vue中也是类似的:

// node_modules/vue/src/core/vdom/patch.js
function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

参考资料

React - Reconciliation
React 源码剖析系列 - 不可思议的 react diff
(完整版)快速掌握虚拟DOM和diff算法【Vue】

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

推荐阅读更多精彩内容