时隔2年,从新看React源码,不少之前不理解的内容如今都懂了。本文将用实际案例结合相关React源码,集中讨论React Diff原理。使用当前最新React版本:16.13.1
。html
另外,今年将写一个“搞懂React源码系列”,把React最核心内容用最通俗易懂地方式讲清楚。2020年搞懂React源码系列:react
- React Diff原理
- React 调度原理
- 搭建阅读React源码环境-支持全部版本断点调试
- React Hooks原理
欢迎Star和订阅个人博客。git
在讨论Diff算法前,有必要先介绍React Fiber,由于React源码中各类实现都是基于Fiber,包括Diff算法。固然,熟悉React Fiber的朋友可跳过Fiber介绍。github
Fiber并不复杂,但若是要全面理解,仍是得花好一段时间。本文主题是diff原理,因此这里仅简单介绍下Fiber。算法
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' } }
)。架构
React源码中,关于diff要从reconcileChildren(...)
提及。ide
总流程:函数
流程图中, 显示源码中用到的函数名,省略复杂参数。“新内容”即被比较的新内容,它多是三种类型:
咱们先以新内容为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
。左侧调试面板可看到对应变量的类型。
此处:
workInProgress
: 父级Fibercurrent.child
: 处于比较中的旧内容对应fibernextChildren
: 即处于比较中的新内容, 为React元素,其类型为对象。在Diff时,比较中的旧内容为Fiber,而比较中的新内容为React元素、文本或数组。其实从这一步已经能够看出,React官网的diff算法说明和实际代码是实现差异较大。
由于新内容为对象,因此继续执行reconcileSingleElement(...)
和placeSingleChild(...)
。
咱们先看placeSingleChild(...)
:
placeSingleChild(...)
的做用很简单,给differ后的Fiber添加反作用标签:Placement(替换),代表在以后须要将旧Fiber对应的DOM元素进行替换。
继续看 reconcileSingleElement(...)
:
此处正式开始diff(比较),child为旧内容fiber,element为新内容,它们的元素类型不一样。
由于类型不一样,React将“删除”旧内容fiber以及其全部相邻Fiber(即给这些fiber添加反作用标签 Deletion(删除)), 并基于新内容生成新的Fiber。而后将新的Fiber设置为父Fiber的child。
到此,一个新内容为React元素的且新旧内容的元素类型不一样的Diff过程已经完成。
那若是新旧内容的元素类型相同呢?
编写相似案例,咱们能够获得结果
userFiber(...)
:
userFiber(...)
的主要做用是基于旧内容fiber和新内容的属性(props)克隆生成一个新内容fiber,这也是所谓的fiber复用。
因此当新旧内容的元素类容相同,React会复用旧内容fiber,结合新内容属性,生成一个新的fiber。一样,将新的fiber设置位父fiber的child。
新内容为React元素的diff流程总结:
当新内容为文本时,逻辑与新内容为React元素时相似:
使用案例:
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') )
若新内容为数组,需reconcileChildrenArray(...)
:
for循环遍历新内容数组,伪代码(用于理解):
for ( let i = 0, oldFiber; i < newArray.length; ) { ... i++ oldFiber = oldFiber.sibling }
遍历每一个新内容数组元素时:
updateSlot(...)
:
由于newChild
的类型为object
, 因此:
updateElement(...)
:
updateElement(...)
与reconcileSingleElement(...)
核心逻辑一致:
同理,updateTextNode(...)
:
updateTextNode(...)
与reconcileSingleTextNode(...)
核心逻辑一致:
HostText
,则基于新内容文本建立新的fiberHostText
, 则克隆旧fiber,结合新内容文本生成新的fiber在本案例中,新内容数组for循环完成后:
由于新旧内容数组的长度一致,因此直接返回第一个新的fiber。而后同上,React将新的fiber设为父fiber的child。
不过若新内容数组长度与旧内容fiber及其相邻fiber的总个数不一致,React如何处理?
编写相似案例。
若新内容数组长度更短:
React将删除多余的旧内容fiber的相邻fiber。
若新内容数组长度更长:
React将遍历多余的新内容数组元素,基于新内容数组元素建立的新的fiber,并添加反作用标签 Placement(替换)。
新内容为数组时的diff流程总结:
经过React源码研究diff算法时,仅调试分析相关代码,能比较容易的得出答案。
Diff的三种状况:
Diff时若比较结果相同,则复用旧内容Fiber,结合新内容生成新Fiber;若不一样,仅经过新内容建立新fiber。
而后给旧内容fiber添加反作用替换标签,或者给旧内容fiber及其全部相邻元素添加反作用删除标签。
最后将新的(第一个)fiber设为父fiber的child。
感谢你花时间阅读这篇文章。若是你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!
欢迎经过微信(扫描下方二维码)或Github订阅个人博客。