• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

读书笔记- 浅出react - react diff策略

武飞扬头像
yanling.zhang
帮助1

1.diff策略:

react将虚拟dom转换为真实dom的最少操作的过程称为调和。diff算法就是调和的具体实现。

-diff策略

  1. web ui中dom节点跨层级的移动操作特别少,可以忽略不计。
  2. 拥有相同类的2个组件将会生成相似的树形结构,拥有不同类的2个组件将会生成不同的树形结构。
  3. 对于同一层级的一组子节点,他们可以通过唯一的id进行区分。
    基于以上的策略,react分别对tree diff,component diff,element diff 进行算法的优化。

你可能会有疑问:如果出现跨层级的移动操作,diff会有怎样的表现呢?reac只会考虑简单的同层级节点的位置变换,而对于不同层级的节点,只会创建和删除的操作。

tree diff:react对虚拟dom树进行层级控制,只会对相同层级的dom节点进行比较,既同一父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节会被完全删除,不会进一步的比较。这样只需要对树进行一次遍历,便能完成整个对dom树的比较。
对树进行分层比较,2颗树只会对同一层次的节点进行比较。

component diff(组件间比较)

  1. 如果是同一类型的组件,按照原策略继续比较虚拟dom。
  2. 如果不是,则将改组件判断为dirty component,从而替换整个组件下的所有子节点。
    那么对于同一类型的组件,有可能虚拟dom没有发生变化,如果能够确定这一点,那么就可以节省大量的diff运算时间。因此react允许用户通过shouldComponentUpdate()来判断该组件是否需要进行diff算法分析。

element diff(节点间的比较)
当节点出于同一层级时,diff提供了3种节点的操作。分别为:insert_markup(插入), remove_existing(移动), remove_node(删除)
insert_markup(插入):新组件类型不在旧集合中,需要对新节点执行插入操作。
remove_existing(移动):旧集合中有新组件类型,且element是可更新的。
remove_node(删除):1. 旧组件类型在新集合也有,但对应的element不同,则不能直接复用和更新,需要执行删除操作。
2.旧组件不在新集合里,需要执行删除操作。

  • 传统的diff如下图
    学新通
    react发现这类的操作繁琐冗余,因为这些都是相同的节点,但是由于位置发生了变化,导致需要进行低效的删除和创建操作,其实主要对这些节点进行位置移动即可。 针对这一现象,react提出优化策略,允许开发者针对同一层级的同组子节点,添加唯一的key进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化。

  • 进行diff差异化对比后
    学新通
    进行diff差异话对比后:如果所示:通过key,发现新旧集合中的节点都是相同的节点,因此无需进行节点的删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置。此时react给出的 diff结果是:B,D不需要任何操作,A,C进行移动操作即可。

  • 那么如此高效的diff到底是如何运作的呢?我们分析一下:
    首先,对新集合中的节点进行循环遍历for(name in nextChildren) ,通过唯一的key判断旧集合中是否存在相同的节点。if(prevChild=== lastChild),如果存在相同的节点则进行移动操作,但是在移动前需要将【当前节点在旧集合中的位置】与【lastIndex】进行比较, 如果 满足【当前节点在旧集合中的位置】< 【lastIndex】,那么就行移动操作,否则不移动位置。这是一种顺序优化的手段。
    lastIndex:一直在更新,表示访问过的节点在旧集合中最右边的位置(即最大的位置)。如果新集合中当前访问的节点比lastIndex大,说明当前访问的节点在旧集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,
    因此不用添加到差异队列中,即不执行移动操作。只有当访问的节点比lastIndex小的时候,才不要进行移动操作。

学新通

下面文字描述上图diff差异对比的过程:
第一步:
从新集合中取的B,然后判断旧集合中是否存在相同的节点B,,此时发现存在节点首先,对新集合中的节点进行循环遍历for(name in nextChildren) ,通过唯一的key判断旧集合中是否存在相同的节点。if(prevChild=== lastChild),如果存在相同的节点则进行移动操作,但是在移动前需要将
【当前节点在旧集合中的位置】与【lastIndex】进行比较, 如果 满足【当前节点在旧集合中的位置】< 【lastIndex】,那么就行移动操作,否则不移动位置。这是一种顺序优化的手段。
lastIndex一直在更新,表示访问过的节点在旧集合中最右边的位置(即最大的位置)。如果新集合中当前访问的节点比lastIndex大,说明当前访问的节点在旧集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,
因此不用添加到差异队列中,即不执行移动操作。只有当访问的节点比lastIndex小的时候,才不要进行移动操作。

学新通

下面文字描述上图diff差异对比的过程:

第一步:
从新集合中取得 B,然后判断旧集合中是否存在相同节点 B,此时发现存在节点 B,接着通过对比节点位置判断是否进行移动操作。B 在旧集合中的位置 B._mountIndex = 1,此时 lastIndex = 0,不满足 child._mountIndex < lastIndex 的条件,因此不对 B 进行移动
操作。更新 lastIndex = Math.max(prevChild.mountIndex, lastIndex),其中 prevChild. mountIndex 表示B在旧集合中的位置,则lastIndex = 1,并将B的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中 B._mountIndex = 0,nextIndex 进入下一个节点的判断。

第二步:
从新集合中取得 A,然后判断旧集合中是否存在相同节点 A,此时发现存在节点 A,接着通过对比节点位置判断是否进行移动操作。A 在旧集合中的位置 A._mountIndex = 0,此时 lastIndex = 1,满足 child._mountIndex < lastIndex 的条件,因此对 A 进行移动操作enqueueMove(this, child._mountIndex, toIndex),其中 toIndex 其实就是 nextIndex,表示 A 需要移动到的位置。更新lastIndex=Math.max(prevChild._mountIndex, lastIndex), 则lastIndex = 1,并将 A 的位置更新为新集合中的位prevChild._mountIndex = nextIndex,此时新集合中 A._mountIndex = 1,nextIndex 进入下一个节点的判断。

第三步:
从新集合中取得 D,然后判断旧集合中是否存在相同节点 D,此时发现存在节点 D,接着通过对比节点位置判断是否进行移动操作。D 在旧集合中的位置 D._mountIndex = 3,此时 lastIndex = 1,不满足 child._mountIndex < lastIndex 的条件,因此不对 D 进行移动操作。更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 D 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中 D._mountIndex = 2,nextIndex 进入下一个节点的判断。

第四步:

从新集合中取得 C,然后判断旧集合中是否存在相同节点 C,此时发现存在节点 C,接着通过对比节点位置判断是否进行移动操作。C 在旧集合中的位置 C._mountIndex = 2,此时 lastIndex = 3,满足 child._mountIndex < lastIndex的条件,因此对 C 进行移动操作enqueueMove(this, child._mountIndex, toIndex)。更新lastIndex = Math.max(prevChild. _mountIndex, lastIndex),则 lastIndex = 3,并将 C 的位置更新为新集合中的位置prevChild._mountIndex = nextIndex,此时新集合中 A._mountIndex = 3,nextIndex 进入下一个节点的判断。由于 C 已经是最后一个节点,因此 diff 操作到此完成。

上面主要分析新旧集合中存在相同节点但位置不同时,对节点进行位置移动的情况。如果新 集合中有新加入的节点且旧集合存在需要删除的节点,那么 diff 又是如何对比运作的呢?

模拟diff移动过程:
学新通

以下图为例进行介绍:

学新通

  1. 从新集合中取得B,然后判断旧集合中存在是否相同节点 B,可以发现存在节点 B。由于
    B 在旧集合中的位置 B._mountIndex = 1,此时 lastIndex = 0,因此不对 B 进行移动操作。
    更新lastIndex = 1,并将 B 的位置更新为新集合中的位置 B._mountIndex = 0,nextIndex
    进入下一个节点的判断。
  2. 从新集合中取得 E,然后判断旧集合中是否存在相同节点 E,可以发现不存在,此时可以
    创建新节点 E。更新 lastIndex = 1,并将 E 的位置更新为新集合中的位置,nextIndex
    进入下一个节点的判断。
  3. 从新集合中取得 C,然后判断旧集合中是否存在相同节点 C,此时可以发现存在节点 C。
    由于 C 在旧集合中的位置 C._mountIndex = 2,lastIndex = 1,此时 C._mountIndex >
    lastIndex,因此不对 C 进行移动操作。更新 lastIndex = 2,并将 C 的位置更新为新集
    合中的位置,nextIndex 进入下一个节点的判断。
    4. 从新集合中取得 A,然后判断旧集合中是否存在相同节点 A,此时发现存在节点 A。由于
    A 在旧集合中的位置 A._mountIndex = 0,lastIndex = 2,此时 A._mountIndex < lastIndex,
    因此对 A 进行移动操作。更新 lastIndex = 2,并将 A 的位置更新为新集合中的位置,
    nextIndex 进入下一个节点的判断。
  4. 当完成新集合中所有节点的差异化对比后,还需要对旧集合进行循环遍历,判断是否存
    在新集合中没有但旧集合中仍存在的节点,此时发现存在这样的节点 D,因此删除节点 D,
    到此 diff 操作全部完成。

diff不足之处:
当然,diff 还存在些许不足与待优化的地方。如图 3-24 所示,若新集合的节点更新为 D、A、 B、C,与旧集合相比只有 D 节点移动,而 A、B、C 仍然保持原有的顺序,理论上 diff 应该只需对 D 执行移动操作,然而由于 D 在旧集合中的位置是最大的,导致其他节点的 _mountIndex < lastIndex,造成 D 没有执行移动操作,而是 A、B、C 全部移动到 D 节点后面的现象。

学新通
模拟diff创建,移动,删除 diff过程:

学新通

2.react path方法

通过前面的内容,我们了解了 React 如何构建虚拟标签,执行组件生命周期,更新 state,计
算 tree diff 等,这一系列操作都还是在 Virtual DOM 中进行的。然而浏览器中并未能显示出更新
的数据,那么 React 又是如何让浏览器展示出最新的数据呢?

React Patch 实现了关键的最后一步。所谓 Patch,简而言之就是将 tree diff 计算出来的 DOM 差异队列更新到真实的 DOM 节点上,最终让浏览器能够渲染出更新的数据。可以这么说,如果 没有 Patch,那么 React 之前基于 Virtual DOM 做再多性能优化的操作都是徒劳,因为浏览器并不 认识 Virtual DOM。虽然 Patch 方法如此重要,但它的实现却非常简洁明了,主要是通过遍历差 异队列实现的。遍历差异队列时,通过更新类型进行相应的操作,包括:新节点的插入、已有节 点的移动和移除等。

这里为什么可以直接依次插入节点呢?原因就是在 diff 阶段添加差异节点到差异队列时,本
身就是有序添加。也就是说,新增节点(包括 move 和 insert)在队列里的顺序就是最终真实 DOM
的顺序,因此可以直接依次根据 index 去插入节点。而且,React 并不是计算出一个差异就去执
行一次 Patch,而是计算出全部差异并放入差异队列后,再一次性地去执行 Patch 方法完成真实
DOM 的更新。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgcagfh
系列文章
更多 icon
同类精品
更多 icon
继续加载