最近在回顾之前看过的React源码解析,改正了之前很多错误的理解。本文主要分享对Reconcile
和Renderer
部分的认识,可对应类式组件的生命周期进行思考。
1、React设计理念
这部分属于前置知识,可以重点关注Fiber架构的实现原理与工作原理部分:Fiber节点中存储的某些参数的含义,如stateNode、tag、type
;双缓存Fiber树等
2、React架构(省略Scheduler部分)
React架构的整个执行过程可以分为两个阶段(可对应ClassComponent
生命周期的两个阶段):render
阶段和commit
阶段,其中render
阶段由协调器Reconcile
完成,commit
阶段由渲染器Renderer
实现。前者主要进行Fiber树的计算(异步,可中断的),后者则根据effectList
完成DOM操作及页面的渲染(同步,不可中断)。总体上即根据页面新的JSX在内存中构建workInProgress Fiber
,在此过程中可复用current Fiber
。
(1)render阶段
先看一下render
阶段,通过可中断的递归实现,其中“递”进行深度优先遍历,目的是为每个WorkInProgress Fiber
节点创建子节点并进行连接,到子节点进行“归”,子节点有兄弟节点则兄弟“递”,否则父“归”。
1️⃣递
这个过程中主要调用beginWork()
函数,该函数会首先根据current
指针是否为null
判断在挂载阶段还是更新阶段。挂载阶段需要根据tag
(classComponent、FunctionComponent、HostComponent
。。。)来创建子Fiber节点并返回给workInProgress.children
。更新阶段则需要根据当前current
节点和workInProgress
节点的props
和type
及优先级判断是否节点可复用,不可复用最终会通过diff
算法复用current
节点,并为子Fiber
节点增加EffectTag
(Placement、Deletion、Update
...)字段来保存commit
阶段要执行的DOM操作,对于函数式组件,若使用了useEffect
和useLayoutEffect
也需要增加EffectTag
(HasEffect、NoEffect
...)可复用还需要检查子树是否需要更新,同样的生成的子节点进行连接后,子节点作为下一轮“递”的输入。
1️⃣归
这个过程中主要调用compeleteWork()
函数,会根据tag区别对待,这里以HostComponent
为例。首先会判断是挂载阶段还是更新阶段,前者Fiber节点中没有对应的DOM对象即stateNode
,所以会为其生成DOM节点并将子孙DOM插入,若update
阶段stateNode
为null
则进行同样操作,否则更新updateQueue
(保存如className、onClick、style
等)并将子孙DOM插入。最后需要将有effectTag
的Fiber挂在EffectList
中,后续commit
阶段就不需要再次遍历Fiber树寻找更新,该链表可在rootFiber
的firstEffect
中读取。
(2)commit阶段
整个commit
阶段主要分为三部分:DOM操作前before mutation
,DOM操作mutation
,DOM操作后layout
,每个部分都会遍历effectList
数组。
1️⃣before mutation
这个步骤主要作三件事:DOM的blur
及autoFocus
处理逻辑、对于类式组件,执行生命周期函数getSnapshotBeforeUpdate()
、对于类式组件使用useEffect()
的,在scheduleCallback()
中调度flushPassiveEffects()
,在layout
阶段之后将effectList
赋值给rootWithPendingPassiveEffects
,并执行flushPassiveEffects()
,flushPassiveEffects()
会遍历rootWithPendingPassiveEffects
,即执行useEffect()
的回调函数。
2️⃣mutation
这个过程会执行所有useLayoutEffect()
的销毁函数,通过遍历effectList
并根据effectTag
对DOM进行不同的增、删、改(细节见图)操作,更新ref等
3️⃣layout
这个阶段DOM操作已经完成,对于类式组件会执行生命周期函数类如componentDidMount()
、componentDidUpdate()
;对于函数式组件,会调用useLayoutEffect()
的回调函数,调度useEffect()
的销毁、回调函数,赋值ref等。
由于JS的同步执行阻塞了主线程,所以此时JS已经可以获取到新的DOM
,但是浏览器对新的DOM
并没有完成渲染。useLayoutEffect
和useEffect
的执行时机有所不同,虽然它们的回调都在layout
阶段被处理,但具体的执行顺序和浏览器渲染的时机是关键。useLayoutEffect
的回调是同步执行(立即执行)的,在浏览器绘制之前执行,而useEffect
的回调是异步的,React会调度useEffect
的回调,但实际执行是在浏览器完成绘制之后,这会导致在useLayoutEffect
中进行的DOM操作可以立即反映到屏幕上,而useEffect
可能会有闪烁,因为浏览器可能已经绘制过一次了。
// React 提交阶段伪代码
function commitRoot() {
// 1. 执行 useLayoutEffect 的销毁函数
commitBeforeMutationEffects();
// 2. 更新 DOM
commitMutationEffects();
// 3. 执行 useLayoutEffect 的回调
commitLayoutEffects(); // 🔴 同步执行
// 4. 浏览器绘制(Paint)
requestPaint();
// 5. 调度 useEffect 的回调
scheduleEffectCallbacks(); // ⚡ 异步执行(微任务)
}
React的渲染流程分为渲染阶段和提交阶段,提交阶段又包括pre-commit
、commit
和post-``commit
,而useLayoutEffect
在commit
阶段同步执行,此时DOM已经更新但浏览器还未重绘,而useEffect
则是在post-commit
阶段异步执行,此时浏览器已经完成绘制。
参考文献:react.iamkasong.com