React的diff算法详解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React的diff算法详解相关的知识,希望对你有一定的参考价值。

参考技术A

一、什么是diff算法?

为了增强用户体验,React从版本16开始将 同步更新 重构成了 可中断的异步更新 ,即采用了新的Reconciler(协调器,用于找出变化的组件),而新的Reconciler中采用了fiber架构。fiber架构的原理在此不再详细解释,我们目前只需要知道fiber节点可以保存dom信息,fiber节点构成的树叫fiber树,而更新dom是要用到‘双缓存技术’,即比较旧的fiber树与此次要渲染的jsx对象,返回新的fiber树进行渲染。 在旧fiber树与jsx对象比较时,决定哪些节点要复用的过程,就是diff算法

由于diff本身也会带来性能消耗,为了降低算法复杂度,React对diff做了 三个预设限制

更新后

如果没有key会走第二条限制,有了key,react就可以判断div和p节点是存在的,可以复用,只需要交换顺序。

diff算法会根据不同的jsx对象执行不同的处理函数,根据jsx对象的不同,我们可以分为两类

1.JSX对象(之后都用newChildren表示)的类型为object、number、string,代表同级只有一个节点
2. newChildren的类型为Array,代表同级有多个节点。

二、单节点diff

对于单节点diff,用一个流程图就可以解释

更新后

由于 key的默认值为null ,所以更新前与更新后满足key相同且元素类型不同,那么我们要删除更新前的三个div节点,新增p节点

三、多节点diff

对于多节点diff, 我们要 遍历newChildren和oldFiber 进行比较。由于React团队发现dom节点一般有更新,增加,删除这三种操作,而更新更为频繁,所以他们设置更新的优先级高于增加删除。基于以上原因,在多节点diff算法的实现中有两层遍历, 第一层遍历处理更新的节点,第二层遍历处理更新以外的节点

第一层遍历

遍历newChildren与oldFiber, 判断节点是否可复用,如果可以复用,则继续遍历。
如果不能复用,分为两种情况:

第二层遍历

第二层遍历从第一层遍历的结束位开始
第一层遍历结束后有4种结果

首先我们要判断newChildren中遍历到的节点,在oldFiber中是否存在,基于此,React将oldFiber中的节点以key-oldfiber 键值对的形式存在Map中,只需要newChildren的key,就可以判断oldFiber中有没有相应的节点。

如果oldFiber中没有相应的节点,则将newChildren生成的fiber打上placement标记

如果有相应的节点,将它的索引记为oldIndex,与上一次可复用节点在oldFiber的索引位置lastPlacedIndex比较,如果每次可复用的节点在上一次可复用右边就说明位置没有变化 ,即

oldIndex >=lastPlacedIndex, 说明相对位置没有变化 ,那么令lastPlacedIndex=oldIndex
oldIndex<lastPlacedIndex, 代表本节点需要向右移动
例如:

参考文档
React技术揭秘 (iamkasong.com)

React源码揭秘:Diff算法详解

编者按:本文作者奇舞团前端开发工程师苏畅。

代码参照React 16.13.1

什么是Diff

在前两篇文章中我们分别介绍了 React 的首屏渲染流程1和组件更新流程2,其中

  • 首屏渲染会渲染一整棵 DOM 树

  • 组件更新会根据变化的状态局部更新 DOM 树

那么 React 如何知道哪些 DOM 节点需要被更新呢?

在上一篇文章这里3我们讲到,在render阶段的beginWork函数中,会将上次更新产生的 Fiber 节点与本次更新的 JSX 对象(对应ClassComponentthis.render方法返回值,或者FunctionComponent执行的返回值)进行比较。根据比较的结果生成workInProgress Fiber,即本次更新的 Fiber 节点。

用通俗的语言讲

React 将上次更新的结果与本次更新的值比较,只将变化的部分体现在 DOM 上

这个比较的过程,就是 Diff。本篇文章主要讲解Rect Diff 算法4的内部实现,对 Diff 的简单讲解请参考React 文档5

Diff的瓶颈以及React如何应对

由于 Diff 操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量。

如果在 React 中使用了该算法,那么展示 1000个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。

为了降低算法复杂度,React的diff会预设三个限制:

  1. 只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。

  2. 两个不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。

  3. 开发者可以通过 key属性 来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:

// 更新前

<div>

    <p key="ka">ka</p>

    <h3 key="song">song</h3>

</div>


// 更新后

<div>

    <h3 key="song">song</h3>

    <p key="ka">ka</p>

</div>

如果没有key,React会认为div的第一个子节点由p变为h3,第二个子节点由h3变为p。这符合限制2的设定,会销毁并新建。

但是当我们用key指明了节点前后对应关系后,React知道key === "ka"p在更新后还存在,所以DOM节点可以复用,只是需要交换下顺序。

这就是React为了应对算法性能瓶颈做出的三条限制。

Diff是如何实现的

接下来我们看看Diff的具体实现。我们从Diff的入口函数reconcileChildFibers出发,接着再看看不同类型的Diff是如何实现的。

Diff函数入口函数简介

让我们稍稍看下Diff的入口函数,不要被代码长度吓到喽

以上是关于React的diff算法详解的主要内容,如果未能解决你的问题,请参考以下文章

详解 React 16 的 Diff 策略

vuediff 算法详解

React生命周期详解

vue3.0 diff算法详解(超详细)

详解vue的diff算法

Vue中diff算法详解