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

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 的更新。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值