react for循环_搞懂React源码系列-React Diff原理

138cb98585c10c242878c520c1b0fc24.png

时隔2年,重新看React源码,很多以前不理解的内容现在都懂了。本文将用实际案例结合相关React源码,集中讨论React Diff原理。使用当前最新React版本:16.13.1

另外,今年将写一个“搞懂React源码系列”,把React 最核心内容用最通俗易懂地方式讲清楚。2020年搞懂React源码系列:
* React Diff原理
* React 调度原理
* 搭建阅读React源码环境-支持所有版本断点调试
* React Hooks原理
欢迎Star和订阅我的博客。

在讨论Diff算法前,有必要先介绍React Fiber,因为React源码中各种实现都是基于Fiber,包括Diff算法。当然,熟悉React Fiber的朋友可跳过Fiber介绍。

Fiber简介

Fiber并不复杂,但如果要全面理解,还是得花好一段时间。本文主题是diff原理,所以这里仅简单介绍下Fiber。

fa6a82a453854dd9bad9b4ccccd16cd2.png

Fiber是一个抽象的节点对象,每个对象可能有子Fiber(child)和相邻Fiber(child)和父Fiber(return),React使用链表的形式将所有Fiber节点连接,形成链表树。

Fiber还有副作用标签(effectTag),比如替换Placement(替换)和Deletion(删除),用于之后更新DOM。

值得注意的是,React diff中,除了fiber,还用到了基础的React元素对象(如: 将<div>foo</div>编译后生成的对象: { type: 'div', props: { children: 'foo' } } )。

Diff 过程

React源码中,关于diff要从reconcileChildren(...)说起。

总流程:

1b5180bd2e9113e6a64dde3d86a84b75.png

流程图中, 显示源码中用到的函数名,省略复杂参数。“新内容”即被比较的新内容,它可能是三种类型:

  • 对象: React元素
  • 字符串或数字: 文本
  • 数组:数组元素可能是React元素或文本

新内容为React元素

我们先以新内容为React元素为例,全面的调试一遍代码,将之后会重复用到的方法在此步骤中讲解,同时以一张流程图作为总结。

案例:

function SingleElementDifferentTypeChildA() { return <h1>A</h1> }

function SingleElementDifferentTypeChildB() { return <h2>B</h2> }

function SingleElementDifferentType() {

 const [ showingA, setShowingA ] = useState( true ) 

 useEffect( () => {

  setTimeout( () => setShowingA( false ), 1000 )

 } )

 return showingA ? <SingleElementDifferentTypeChildA/> : <SingleElementDifferentTypeChildB/>

}

ReactDOM.render( <SingleElementDifferentType/>, document.getElementById('container') )

从第一步reconcileChildren(...)开始调试代码,无需关注与diff不相关的内容,比如renderExpirationTime。左侧调试面板可看到对应变量的类型。

8d3e6835fb437faafd246e318dca687f.png

此处:

  • workInProgress: 父级Fiber
  • current.child: 处于比较中的旧内容对应fiber
  • nextChildren: 即处于比较中的新内容, 为React元素,其类型为对象。

在Diff时,比较中的旧内容为Fiber,而比较中的新内容为React元素、文本或数组。其实从这一步已经可以看出,React官网的diff算法说明和实际代码是实现差别较大。

f38d10466b0d1f3011e078e5424e9548.png

因为新内容为对象,所以继续执行reconcileSingleElement(...)placeSingleChild(...)

我们先看placeSingleChild(...)

5080da213cf7ca64e5b0f118932c46ab.png

placeSingleChild(...)的作用很简单,给differ后的Fiber添加副作用标签:Placement(替换),表明在之后需要将旧Fiber对应的DOM元素进行替换。

继续看 reconcileSingleElement(...):

ee495cd6b3bdf71d0e8bb47e95c1acd2.png

此处正式开始diff(比较),child为旧内容fiber,element为新内容,它们的元素类型不同。

46b747e00e1625d5245a184e445111eb.png

f182220486dbc3a80536ced37298dd15.png

因为类型不同,React将“删除”旧内容fiber以及其所有相邻Fiber(即给这些fiber添加副作用标签 Deletion(删除)), 并基于新内容生成新的Fiber。然后将新的Fiber设置为父Fiber的child。

到此,一个新内容为React元素的且新旧内容的元素类型不同的Diff过程已经完成。

那如果新旧内容的元素类型相同呢?

编写类似案例,我们可以得到结果

d78e76ad5a695fa1a4dcaf96b83a4ed7.png

userFiber(...)

56bc138f5eeedd486dcb235b14dd9392.png

userFiber(...)的主要作用是基于旧内容fiber和新内容的属性(props)克隆生成一个新内容fiber,这也是所谓的fiber复用。

所以当新旧内容的元素类容相同,React会复用旧内容fiber,结合新内容属性,生成一个新的fiber。同样,将新的fiber设置位父fiber的child。

新内容为React元素的diff流程总结:

18446e9d9e7d633919f591243bbc0e71.png

新内容为文本

当新内容为文本时,逻辑与新内容为React元素时类似:

eaaab8610c038c17f4a897238c9516e1.png

新内容为数组

使用案例:

function ArrayComponent() {

  const [ showingA, setShowingA ] = useState( true ) 

  useEffect( () => {

   setTimeout( () => setShowingA( false ), 1000 )

  } )

  return showingA ? <div>

​    <span>A</span>

​    <span>B</span>

  </div> : <div>

​    <span>C</span>

​    D

  </div>

}

ReactDOM.render( <ArrayComponent/>, document.getElementById('container') )

cea7fa95b47b56bc10314fdb5a3346e5.png

若新内容为数组,需reconcileChildrenArray(...):

a8dd86ef888252170689ac3826db315f.png

for循环遍历新内容数组,伪代码(用于理解):

for ( let i = 0, oldFiber; i < newArray.length; ) {

  ...

  i++

  oldFiber = oldFiber.sibling
}

遍历每个新内容数组元素时:

8af38cd948d6f53f3fdee9310eee6f23.png

updateSlot(...):

ded8d58b2b72856febb70129c852a608.png

因为newChild的类型为object, 所以:

ca63fc4296905facf3d26a8d361c6fc3.png

updateElement(...):

dddeb90c9e9fe53258975a9e0864e580.png

updateElement(...)reconcileSingleElement(...)核心逻辑一致:

  • 若新旧内容元素类型一致,则克隆旧fiber,结合新内容生成新的fiber
  • 若不一致,则基于新内容创建新的fiber。

同理,updateTextNode(...)

86cdbc0a43d2b5fb65e42744e329ac91.png

updateTextNode(...)reconcileSingleTextNode(...)核心逻辑一致:

  • 若旧内容fiber的标签不是HostText,则基于新内容文本创建新的fiber
  • 若是HostText, 则克隆旧fiber,结合新内容文本生成新的fiber

在本案例中,新内容数组for循环完成后:

d1995474a6b4835f2f9f2cfadb0a35cb.png

因为新旧内容数组的长度一致,所以直接返回第一个新的fiber。然后同上,React将新的fiber设为父fiber的child。

不过若新内容数组长度与旧内容fiber及其相邻fiber的总个数不一致,React如何处理?

编写类似案例。

若新内容数组长度更短:

96640cb681b8c80a66c78cb54d6665b7.png

React将删除多余的旧内容fiber的相邻fiber。

若新内容数组长度更长:

11c6de7089717af8d8f34393888876e7.png

React将遍历多余的新内容数组元素,基于新内容数组元素创建的新的fiber,并添加副作用标签 Placement(替换)。

新内容为数组时的diff流程总结:

a676f1434f9b025e0ef08656d37a4bea.png

总结

通过React源码研究diff算法时,仅调试分析相关代码,能比较容易的得出答案。

Diff的三种情况:

  1. 新内容为React元素
  2. 新内容为文本
  3. 新内容为数组

Diff时若比较结果相同,则复用旧内容Fiber,结合新内容生成新Fiber;若不同,仅通过新内容创建新fiber。

然后给旧内容fiber添加副作用替换标签,或者给旧内容fiber及其所有相邻元素添加副作用删除标签。

最后将新的(第一个)fiber设为父fiber的child。

参考资料

  • The how and why on React’s usage of linked list in Fiber to walk the component’s tree: https://medium.com/react-in-d...
  • [译]深入React fiber架构及源码: https://zhuanlan.zhihu.com/p/...
  • Inside Fiber: in-depth overview of the new reconciliation algorithm in React: https://medium.com/react-in-d...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值