- A+
1 前言
大家好,我是心锁,一枚23届准毕业生。
如果读者阅读过我其他几篇React相关的文章,就知道这次我是来填坑的了
原因是,写了两篇解读react-hook的文章后我发现——并不是每位同学都清楚React的架构,包括我在内也只是综合不同技术文章与阅读部分源码有一个了解,但是调试时真正沉淀成文章的还没有。
所以这篇文章来啦~文章基于2022年八九月的React源码进行调试及阅读,将以通俗的形式揭秘React
阅读本文,成本与收益如下
阅读耗时:26min+
全文字数:1w+
全文字符:5.5w+
预期收益:通明境 · React架构
本文适合有阅读React源码计划的初学者或者正在阅读React源码的工程师,我们一起形成头脑风暴。
2 认识Fiber节点
2.1 Fiber节点基础部分
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // Instance this.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null; ... this.ref = null; ... }
Fiber节点本身存储了一些最基本的数据,其中包括如上六项构成Instance
,它们分别代表
-
tag:Fiber节点对应组件的类型,包括了Funtion、Class等
-
key:更新key会强制更新Fiber节点
-
type:保存组件本身。准确来说,对于函数组件保存函数本身,对于类组件保存类本身,对于HostComponent,也就是如原生<div></div>这类原生标签会保存节点名称
-
elementType:保存组件类型和type大部分情况是一样的,但是也有不一样的情况,比如
LazyComponent
-
stateNode:保存Fiber对应的真实DOM节点
-
ref: 和key一样属于base字段
2.2 Fiber树结构实现
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { ... // Fiber this.return = null; this.child = null; this.sibling = null; this.index = 0; ... }
我们看到Fiber节点这四个属性,它们的含义分别是
- return:指向父节点Fiber
- child:指向子节点Fiber
- sibling:指向右边的兄弟节点Fiber
这样子一来,对于我们这里的组件,就构成了如图的Fiber树
const CountButton = () => { const [count, setCount] = useState(0); const handleClick = () => { setCount(v => v + 1); }; useEffect(() => { console.log('Hello Mount Effect'); return () => { console.log('Hello Unmount Effect'); }; }, []); useEffect(() => { console.log('Hello count Effect'); }, [count]); return ( <> <div>Render by state</div> <div>{count}</div> <button onClick={handleClick}>Add Count</button> </> ); }; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <CountButton/> </header> </div> ); }
2.3 函数式组件&&Fiber
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { ... this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null; ... }
从源码上看,React为hook足足腾出了五个属性专门处理在函数式组件中使用hook的场景。
这些个玩意儿气其实我们在前边的hook章节也或多或少有了解过,这里专门讲述Fiber节点上存储的这些结构的作用。
2.3.1 pendingProps
pendingProps,从FiberNode的构造函数看,是mixed(可传入)进来的
也就是说,这部分props可以在Fiber间传递,主要用于更新/创造新Fiber节点时用来传递props
2.3.2 memoizedProps
memoizedProps
和pendingProps
的区别是什么呢?
我们知道,props代表一个Function的参数,当props变化时Function也会再次执行。
一般来讲,memoizedProps
会在整个渲染流程结尾部分被更新,存储FiberNode的props。
而pendingProps
一般在渲染开始时,作为新的Props出现
举个更便于理解的例子,在如图的beginWork
阶段,会对比新的props和旧的props来确定是否更新,此时比较的就是workInProgress.pendingProps
和current.memoizedProps
2.3.3 updateQueue
上一篇我们讲useEffect
有讲到,updateQueue
以如图的形式存储useEffect
运行时生成的各个effect
lastEffect以环形链的形式存储了单个节点的所有effect。
(当然,这里指的当然只是函数式组件)
2.3.4 memoizedState
在useState
章节,我们也有讲过memoizedState
,memoizedState
存储了我们调用hook时产生的hook
对象,目前已知除了useContext不会有hook对象产生并挂载,其他hook都会挂载到这里。
hook之间以.next
相连形成单向链表。
而hook调用时产生的不管是effect(useEffect)还是state(useState),都是存储在
hook.memoizedState
,体现在Fiber节点上,其实是存储在hook.memoizedState.memoizedState
,注意不要混淆。
2.3.5 dependencies
以下是调试代码
const BaseContext = createContext(1); const BaseContextDemo = () => { const {base} = useContext(BaseContext); return <div>{base}</div>; }; const CountButton = () => { const [count, setCount] = useState(0); const handleClick = () => { setCount(v => v + 1); }; useEffect(() => { console.log('Hello Mount Effect'); return () => { console.log('Hello Unmount Effect'); }; }, []); useEffect(() => { console.log('Hello count Effect'); }, [count]); const ref = useRef(); const [base, setBase] = useState(null); const initValue = { base, setBase, }; return ( <BaseContext.Provider value={initValue}> <div ref={ref}> <div>Render by state</div> <div>{count}</div> <button onClick={handleClick}>Add Count</button> <button onClick={() => setBase(i => ++i)}>Add Base</button> <BaseContextDemo /> </div> </BaseContext.Provider> ); };
在还没有发出的useContext
原理中,会记载useContext的实现原理,剧透就是FiberNode.dependencies
这个属性记载了组件中通过useContext
获取到的上下文
从调试结果看,多个context也将通过.next
相连,同时显然,这是一条单向链表
2.4 操作依据
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { ... // Effects this.flags = NoFlags; this.subtreeFlags = NoFlags; this.deletions = null; ... }
我们看到这三个属性
-
deletions:待删除的子节点,render阶段diff算法如果检测到Fiber的子节点应该被删除就会保存到这里。
-
flags/subtreeFlags:都是二进制形式,分别代表
Fiber
节点本身的保存的操作依据与Fiber
节点的子树的操作依据。
flags是React中很重要的一环,具体作用是通过二进制在每个Fiber节点保存其本身与子节点的flags。
至于具体如何保存,实际上是使用了二进制的特性,举几个例子
2.4.1 &运算
温习一下
&运算符
的规则:只有1&1=1,其他情况为0
const NoFlags = /* */ 0b000000000000000000000000; const PerformedWork = /* */ 0b000000000000000000000001; const Placement = /* */ 0b000000000000000000000010; const Update = /* */ 0b000000000000000000000100; const unknownFlags=Placement; Boolean(unknownFlags & Placement) // true Boolean(unknownFlags & Update) //false
React中会用一个未知的flags & 一个flag,此时是在判断未知的flags中是否包含flag。
之所以说是是否包含,我们可以看看下边的代码。
const NoFlags = /* */ 0b000000000000000000000000; const PerformedWork = /* */ 0b000000000000000000000001; const Placement = /* */ 0b000000000000000000000010; const Update = /* */ 0b000000000000000000000100; const unknownFlags = Placement|Update; //此时=0b000000000000000000000110 Boolean(unknownFlags & Placement) // true Boolean(unknownFlags & Update) //true
2.4.2 |运算
温习一下
|运算符
的规则:只有0&0=0,其他情况为1
上边unknownFlags的例子我们不难发现,react利用了|运算符
的特性来存储flag
const unknownFlags = Placement|Update; //此时=0b000000000000000000000110
这样的好处是快,判断是否包含的时候,直接使用& 运算符
,在有限的操作依据面前,使用二进制完全可以兜住所有情况。
2.4.3 ~运算
~运算符会把每一位取反,即1->0,0->1
在React中,~运算符同样是常用操作
那么作用是什么呢?其实也很容易从函数上下文分析出来,对于图中这个例子,react通过~运算符
与&运算符
的结合,从flags中删除了Placement
这个flag。
2.4.4 小总结:React中常见的操作
-
通过
unknownFlags & Placement
判断unknownFlags
是否包含Placement
-
通过
unknownFlags |= Placement
将Placement
合并进unknownFlags
中 -
通过
unknownFlags &= ~Placement
将Placement
从unknownFlags
中删去
关于有哪些flags,我们可以翻阅到
ReactFiberFlags.js
,这里会有详细flags的记载
2.5 双缓存树的体现
我们曾说过,React的最基本工作原理双缓存树,这引申出了我们需要知道这种机制在React中的实际体现。
这需要我们找到ReactFiber.old.js
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { ... this.alternate = null; ... }
由此我们知道,FIberNode上会有一个属性alternate
,而这个属性正是我们期望的双缓存树中,里树与外树的双向指针。
正如图所见,在初次渲染中,current===null
,所以目前仍是白屏,而workInProgress
已经在构建
(图误,在renderWithHooks才对)
而当我们再次渲染,在renderWithHooks
断点,就可以观察到workInProgress.alternate==current
2.6* 优先级相关
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { ... this.lanes = NoLanes; this.childLanes = NoLanes; ... }
和lane有关的变量统一和调度优先级有关,暂时不涉及(因为还没看)
2.7* React devtools Profiler
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { ... if (enableProfilerTimer) { this.actualDuration = Number.NaN; this.actualStartTime = Number.NaN; this.selfBaseDuration = Number.NaN; this.treeBaseDuration = Number.NaN; this.actualDuration = 0; this.actualStartTime = -1; this.selfBaseDuration = 0; this.treeBaseDuration = 0; } ... }
React并不只是react
,react仓库里包含了其他工程,其中就包含了我们的react profiler工具,在使用了profiler工具的情况下,react fiber会记录一些运行时间,其实很多带有Profiler
的判断语句都是和Profiler在配合。
3 好好认识hook结构
我们上边有讲到FiberNode.memoizedState
,我们知道这里保存的是mountWorkInProgressHook
时产生的hook对象
{ memoizedState: 0, baseState: 0, baseQueue: null, queue: ???, next:null }
那么hook的各个项指什么?
3.1 baseState和memoizedState
其实很好理解,baseState对应上一次的state(effect),memoizedState为最新的state(effect),总之就是hook保存基本数据的地方。
3.2 queue
而hook.queue则是useState、useReducer
的dispatcher存储的地方。
var queue:UpdateQueue = { pending: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: reducer, lastRenderedState: initialState }; hook.queue = queue; var dispatch = queue.dispatch = dispatchReducerAction.bind(null, currentlyRenderingFiber$1, queue);
对于queue的结构,我们逐一讲解
3.2.1 lastRenderedState & lastRenderedReducer
- queue.lastRenderedState属性存储上一个 state
- queue.lastRenderedReducer 属性存储 reducer 内部状态变更逻辑
其中queue.lastRenderedReduce
可能不好理解,我们可以从代码中理解,且看这里
function basicStateReducer(state, action) { // $FlowFixMe: Flow doesn't like mixed types return typeof action === 'function' ? action(state) : action; } function mountState(initialState) { ... hook.memoizedState = hook.baseState = initialState; var queue = { pending: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: initialState }; ... }
这是dispatchSetState
中的一段逻辑,处理的正是我们下边将讲述的,「不在渲染中」的处理阶段(onClick触发===异步触发)。
那这里可以看到,我们可以从lastRenderedReducer
得到eagerState
var currentState = queue.lastRenderedState; var eagerState = lastRenderedReducer(currentState, action); // Stash the eagerly computed state, and the reducer used to compute // it, on the update object. If the reducer hasn't changed by the // time we enter the render phase, then the eager state can be used // without calling the reducer again.
eagerState是什么? 实际上这里是通过lastRenderedReducer快速获得了最近一次的state。
react会通过objectIs(eagerState,currentState)
来确定是否不进行更新,这也是为什么我们更新state的时候要注意state为不可变数据,每次更新都需要更新一个新值才有效
if (objectIs(eagerState, currentState)) { enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update); return; }
3.2.2 dispatch
dispatch 属性存储状态变更函数,对应useState、useReducer 返回值中的第二项
function mountState(initialState) { var hook = mountWorkInProgressHook(); if (typeof initialState === 'function') { initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; var queue = { pending: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: initialState }; hook.queue = queue; var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue); return [hook.memoizedState, dispatch]; }
值得注意的就是dispatch会通过.bind事先注入currentlyRenderingFiber$1, queue
两个参数,此间通过bind绑定的currentlyRenderingFiber$1
,作用是判断这个更新是在fiber的render阶段还是异步触发。
这也给了我们一个判断fiber在render阶段的条件
function isRenderPhaseUpdate(fiber: Fiber) { const alternate = fiber.alternate; return ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber) ); }
3.2.3 pending
pending 属性存储排队中的状态变更规则,单向环形链表结构。
在源码中,每一个规则以Update
的结构连接
export type Update<S, A> = {| lane: Lane, action: A, hasEagerState: boolean, eagerState: S | null, next: Update<S, A>, |};
那么我们知道了
- eagerState 缓存上一个状态(React称之为急迫的状态)
- action 代表状态变更的规则,可以是本次要被修改的值,也可以是函数
- hasEagerState 则是记录是否执行过优化逻辑
eagerState在所有源码中只在这里使用,根据React源码,这里的优化指的是React会在eagerState===currentState的情况下,不做重渲染。如果状态更新前后没有变化,则可以略过剩下的步骤。
try { var currentState = queue.lastRenderedState; var eagerState = lastRenderedReducer(currentState, action); update.hasEagerState = true; update.eagerState = eagerState; if (objectIs(eagerState, currentState)) { enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update); return; } } catch (error) { } finally { { ReactCurrentDispatcher$1.current = prevDispatcher; } }
3.3 baseQueue
值得注意的是,baseQueue的结构来自queue.pending而不是queue
(baseQueue被赋值queue.pending)
其余的大抵是没啥好说的,baseQueue在调试中的体现我暂时并没有遇到,推测需要有比较大量的更新。
4 React架构
本章我们讲述React的渲染流程,将覆盖React的render
阶段与commit
阶段的概念与流程概览,不会非常深入,争取留存印象。
4.1 React渲染关键节点
我们已经预先知道可以将React的渲染分成render
阶段和commit
阶段,也知道render
阶段的关键函数是beginWork
和completeWork
,commit
阶段的关键函数则是commitRoot
。
在这个基础上,我们从调用堆栈中可以找到这两个阶段的起始节点。
- render阶段
我们在beginWork中打上断点,然后可以回溯调用堆栈找到出发点。
从图中,我们可以知道renderRoot触发于performConcurrentWorkOnRoot
除此之外,在performSyncWorkOnRoot
中也可以走入renderRoot
它们会根据情况走到renderRootConcurrent
或者renderRootSync
,这里即是render阶段的开始点
那么我们得到第一个关键节点:
- render阶段开始于
renderRootConcurrent
或renderRootSync
- commit阶段
我们知道,render阶段的尾巴是completeWork
,commit阶段的起步是commitRoot
,我们尝试在这completeWork
方法中断点,然后单步调试到commitRoot
。
上图是我debug出来的结果,completeWork
与commitRoot
之间的最近公共函数节点是performSyncWorkOnRoot/performConcurrentWorkOnRoot
。
那么我们知道,commitRoot
即是commit阶段的起点。
那么我们得到两个关键信息:
- commit阶段开始于
commitRoot
- render阶段和commit阶段通过
performSyncWorkOnRoot/performConcurrentWorkOnRoot
联动
4.1.1 小总结
- render阶段开始于
renderRootConcurrent
或renderRootSync
- commit阶段开始于
commitRoot
- render阶段和commit阶段通过
performSyncWorkOnRoot/performConcurrentWorkOnRoot
联动
4.2 状态更新流程
4.2.1 找到root节点
正常render的第一步,是找到当前Fiber的root节点。
以useState造成的渲染举例,React会通过enqueueConcurrentHookUpdate->getRootForUpdatedFiber
找到当前节点的root节点。
function dispatchSetState(fiber, queue, action) { ... var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { var eventTime = requestEventTime(); scheduleUpdateOnFiber(root, fiber, lane, eventTime); entangleTransitionUpdate(root, queue, lane); } ... }
function getRootForUpdatedFiber(sourceFiber) { ... detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber); var node = sourceFiber; var parent = node.return; while (parent !== null) { detectUpdateOnUnmountedFiber(sourceFiber, node); node = parent; parent = node.return; } return node.tag === HostRoot ? node.stateNode : null; }
寻找root节点是一个向上不断寻找root节点的过程,在这个过程中react还会持续调用detectUpdateOnUnmountedFiber
检查是否调用了过期的更新函数。
什么是过期的更新函数?举个例子,通过useRef保存了setState方法,但是随着组件更新ref中的setState方法并没有更新,此时由于setState方法本质上是通过.bind的形式报存了函数及参数fiber节点,此时就会存在调用了一个已卸载组件的过期的setState方法。
4.2.2 调度同步/异步更新
找到root节点之后,那么就要进入render流程
,这就存在一个问题。
我们上边说了,render
阶段的触发函数是performSyncWorkOnRoot
或performConcurrentWorkOnRoot
,那么如何判断应该进入同步更新还是异步更新呢?
这就要走到ensureRootIsScheduled
,ensureRootIsScheduled
会通过判断newCallbackPriority === SyncLane
来确定走同步render还是异步render,这里涉及调度器,暂时不讲(还没看还不会)
function ensureRootIsScheduled(root, currentTime) { ... var newCallbackNode; if (newCallbackPriority === SyncLane) { // Special case: Sync React callbacks are scheduled on a special // internal queue if (root.tag === LegacyRoot) { ... scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root)); } else { scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); } ... newCallbackNode = null; } else { var schedulerPriorityLevel; ... newCallbackNode = scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root)); } root.callbackPriority = newCallbackPriority; root.callbackNode = newCallbackNode; }
那么可以看到,这里会有一个scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))
或者scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root))
的过程。
值得注意的是,同步调度这里还更复杂,react一方面需要考虑是否是严格模式做不同的callback
(ensureRootIsScheduled是一个很重要的函数,会Scheduled一起讲会比较好)
另一方面还调度了flushSynCallbacks
,这个函数做的事情很简单,就是把syncQueue中的待执行任务全部执行
4.2.3 render阶段
render阶段分成了两个阶段,我们在状态更新流程中不讲细节,只讲明基本作用,细节请看后边的单章
经历了调度更新,会来到render阶段,render阶段做了两件事。
beginWork
阶段。在这个阶段react做的事情是从root递归到子叶,每次beginWork
会对Fiber
节点进行新建/复用逻辑,然后通过reconcileChildren
将child Fiber
挂载到workInProgress.child
并在child Fiber
上记录flags,最终遍历整个Fiber树completeWork
阶段。在这个阶段,是从子叶不断向上遍历到父亲Fiber节点的过程,这个过程中,completeWork
会把workInProgress Tree
上的真实DOM挂载/更新上去。
那么总结来说,beginWork
负责虚拟DOM节点Fiber Node
的维护与flag记录,completeWork负责真实DOM节点在Fiber Node
的映射工作。
当然,这些操作只涉及节点维护,真正渲染到页面上就是commit阶段要负责的了
4.2.4 commit阶段
commit阶段,除了会处理一下和hook
相关的事情之外,最主要做了就是负责把beginWork阶段记录的flags在真实DOM树上进行操作。
总结来说:
- 处理和
useEffectuseInsertionEffectuseLayoutEffect
相关的hook,处理class组件相关的生命周期钩子 - 基于flags做真实DOM树操作,包括增删改,以及输入框类型节点的focus、blur等问题
- 清理一些全局变量,并确保进入下一次调度
4.3 render阶段
这里是延续状态更新流程的render阶段。
我们在状态更新第一步就拿到了root节点,经过调度更新后会进入render阶段。
此时我们有两种走法,一种是通过renderRootSync
来到workLoopSync
,另一种则是通过renderRootConcurrent
走到workLoopConcurrent
,这两者的区别是workLoopConcurrent
会检查浏览器是否有剩余时间片。
function workLoopConcurrent() { // 执行工作,直到调度程序要求我们让步 while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); } } function workLoopSync() { // 已经超时了,因此无需检查我们是否需要让步就可以执行工作 while (workInProgress !== null) { performUnitOfWork(workInProgress); } }
workLoop做了什么呢?这就要从performUnitOfWork(workInProgress)
说起,下边的代码是精简逻辑 (只剩下beginWork这部分逻辑) 过后的performUnitOfWork
函数,可以看到performUnitOfWork
通过beginWork
创建了一个新的节点赋给workInProgress
。
function performUnitOfWork(unitOfWork) { var current = unitOfWork.alternate; // currentFiber setCurrentFiber(unitOfWork); // 会将全局current变量设定为workInProgressFiber var next = beginWork$1(current, unitOfWork, renderLanes$1); // currentFiber resetCurrentFiber(); // 重置current变量为null unitOfWork.memoizedProps = unitOfWork.pendingProps; workInProgress = next; ... }
4.3.1 beginWork
那么此处引出了render阶段中最重要的两个函数之一beginWork
,beginWork正如上边所说,这个函数的职责是返回一个Fiber节点,这个节点可以复用currentFiber
也可以创建一个新的。
我们其实在【useState原理】章节中有见过beginWork,当时我们强调了双缓存机制,这次我们可以更细地了解一下beginWork。
我们提炼一下beginWork的核心逻辑,会发现beginWork
通过current!==null
来判断是否是第一次执行,这里的逻辑是如果是第一次执行,那么Fiber没有mount,自然为null。
function beginWork(current, workInProgress, renderLanes) { ... if (current !== null) { var oldProps = current.memoizedProps; var newProps = workInProgress.pendingProps; if (oldProps !== newProps || hasContextChanged() || ( workInProgress.type !== current.type )) { didReceiveUpdate = true; } else { var hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes); if (!hasScheduledUpdateOrContext && (workInProgress.flags & DidCapture) === NoFlags) { // 没有待更新的updates或者上下文信息,复用上次的Fiber节点 didReceiveUpdate = false; return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes); } ... } } else { didReceiveUpdate = false; ... } workInProgress.lanes = NoLanes; switch (workInProgress.tag) { ... case FunctionComponent: ... case HostComponent: ... } }
#1 update复用逻辑
看到这里,react在update的逻辑中,根据三个条件来判断是否复用上一次的FIber
-
oldProps !== newProps,代表
props
是否变化 -
hasContextChanged(),
var didPerformWorkStackCursor = createCursor(false); // Keep track of the previous context object that was on the stack. // We use this to get access to the parent context after we have already // pushed the next context provider, and now need to merge their contexts.
-
workInProgress.type !== current.type,
fiber.type
是否变化
function beginWork(current, workInProgress, renderLanes) { ... if (current !== null) { var oldProps = current.memoizedProps; var newProps = workInProgress.pendingProps; if (oldProps !== newProps || hasContextChanged() || ( workInProgress.type !== current.type )) { didReceiveUpdate = true; } else { //此处是复用的逻辑 ... } } else { didReceiveUpdate = false; ... } ... }
#2 mount/update新建逻辑
不满足更新条件的话,会根据workInProgress.tag
新建不同类型的Fiber节点。对于不进行Fiber复用到更新也会进入这个逻辑
switch (workInProgress.tag) { case IndeterminateComponent: { return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes); } case LazyComponent: { var elementType = workInProgress.elementType; return mountLazyComponent(current, workInProgress, elementType, renderLanes); } case FunctionComponent: { var Component = workInProgress.type; var unresolvedProps = workInProgress.pendingProps; var resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps); return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderLanes); } case ClassComponent: { var _Component = workInProgress.type; var _unresolvedProps = workInProgress.pendingProps; var _resolvedProps = workInProgress.elementType === _Component ? _unresolvedProps : resolveDefaultProps(_Component, _unresolvedProps); return updateClassComponent(current, workInProgress, _Component, _resolvedProps, renderLanes); } ... }
根据我们在【useState】章节的收获,不管是update还是mount都要走到reconcileChildren
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) { if (current === null) { // mount时 workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes); } else { // update时 workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes); } }
这里做的事情描述起来是比较好办的,不过详细起来就涉及diff算法需要开单章
- mount时,创建新的Child Fiber节点
- update时,将当前组件与该组件在上次更新时对应的
Fiber
节点进行diff比较,将比较的结果生成新Fiber
节点
当然,不管走到哪里,workInProgress都会得到一个child FIber
不管是reconcileChildFibers
还是mountChildFibers
,都是通过调用ChildReconciler这个函数来运行的。
而在整个ChildReconciler中,我们会经常性看到如图一样的操作。
这便引出了操作依据一说,react用Fiber.flags
并以二进制的形式存贮了对于每个Fiber的操作依据,这种方式比数组更高效,可以方便地使用位运算发为Fiber.flags
增删不同的操作依据。
点击这里可以查看所有的操作类型
#3 diff算法*
标记这个知识点,下次再说
4.3.2 completeWork
我们持续执行workLoop,会发现workInProgress
从rootFiber
持续深入到了我的调试代码中的最底层(一个div),此时就到了render阶段的第二个阶段completeWork
。
function performUnitOfWork(unitOfWork) { ... if (next === null) { // 进入completeWork completeUnitOfWork(unitOfWork); } else { ... } ... }
那么此时进入completeUnitOfWork
,这里的核心逻辑是completeWork从子节点不断访问workInProgress.return
向上循环执行beginWork
,如果遇到兄弟子节点,则会将workInProgress指向兄弟节点并返回至performUnitOfWork
。重新执行beginWork到completeWork的整个render阶段。
那么completeWork做了什么?这里是completeWork的基本逻辑框架(我把bubbleProperties提出来方便理解每个completeWork
都会执行这前后两条语句),做了popTreeContext
和bubbleProperties
。
function completeWork(current, workInProgress, renderLanes) { popTreeContext(workInProgress); switch (workInProgress.tag) { case FunctionComponent: ... case HostComponent: ... ... } bubbleProperties(workInProgress); }
popTreeContext是和上边beginWork相关的内容,这里的目的是使得正在进行的工作不处于堆栈顶部。对应pushContext的阶段一般在beginWork的swtich中进入的函数中都可以找到
而bubbleProperties
的核心逻辑我也提了出来,可以看到这里是做了一个层遍历,遍历了completedWorkFiber
的所有child,将它们的return赋值为completedWorkFiber
。同时,这里也涉及了subtreeFlags
的计算,会将子节点的操作依据冒泡到父节点。
而关于subtreeFlags
的具体用处,在commit阶段,我们后边说。
function bubbleProperties(){ ... var newChildLanes = NoLanes; var subtreeFlags = NoFlags; { var _child = completedWork.child; while (_child !== null) { newChildLanes = mergeLanes(newChildLanes, mergeLanes(_child.lanes, _child.childLanes)); subtreeFlags |= _child.subtreeFlags; subtreeFlags |= _child.flags; _child.return = completedWork; _child = _child.sibling; } } completedWork.subtreeFlags |= subtreeFlags; } ... }
后续的话,会根据workInProgress.tag
来走不同的逻辑,我们这里主要说HostComponent的逻辑,代表原生组件。
下边是我提炼出来的核心逻辑,这里同样会区分update
和mount
。
function completeWork(current, workInProgress, renderLanes) { popTreeContext(workInProgress); switch (workInProgress.tag) { ... case HostComponent:{ popHostContext(workInProgress); var type = workInProgress.type; if (current !== null && workInProgress.stateNode != null) { updateHostComponent$1(current, workInProgress, type, newProps); ... } else { ... var currentHostContext = getHostContext(); var rootContainerInstance = getRootHostContainer(); var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress); appendAllChildren(instance, workInProgress, false, false); workInProgress.stateNode = instance; ... } bubbleProperties(workInProgress); return null; } ... } }
#1 update时
update时,无需生成新的DOM节点,所以此时要处理props,在updateHostComponent
中,第二部分会调用prepareUpdate->diffProperties
获得一个updatePayload挂载在workInProgress.updateQueue
上
具体会处理哪些props,我们深入到diffProperties
就可以找到这一块的逻辑
OK,那么我们回到上边所说的updatePayload
,调试发现updatePayload
是一个数组,数据结构体现为一个偶数为key,奇数为value的数组:
到了这一步,update流程最后会走入markUpdate
,至此。completeWork的update逻辑完毕
#2 mount时
我们此时来看mount时的逻辑,这里最核心的逻辑简化后其实只有几句
function completeWork(current, workInProgress, renderLanes) { popTreeContext(workInProgress); ... var currentHostContext = getHostContext(); var rootContainerInstance = getRootHostContainer(); // 获得root真实DOM var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);// 创建Fiber对应的真实DOM appendAllChildren(instance, workInProgress, false, false);//将创建的真实dom插入workInProgressFiber workInProgress.stateNode = instance; ... bubbleProperties(workInProgress); }
我们关注appendAllChildren
,这里的逻辑是将新建的instance作为真实节点parent,将其插入到workInProgressFiber的真实节点中(因为一个Fiber节点不一定有真实节点,所以要找到可以插入的真实节点)
appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) { // We only have the top Fiber that was created but we need recurse down its // children to find all the terminal nodes. var node = workInProgress.child; while (node !== null) { if (node.tag === HostComponent || node.tag === HostText) { appendInitialChild(parent, node.stateNode); } else if (node.tag === HostPortal) ; else if (node.child !== null) { node.child.return = node; node = node.child; continue; } if (node === workInProgress) { return; } while (node.sibling === null) { if (node.return === null || node.return === workInProgress) { return; } node = node.return; } node.sibling.return = node.return; node = node.sibling; } };
那么这里实际做的就是把真实DOM挂载到workInProgressFiber
上,又由于我们上边说了,complateWork是一个从子节点向上遍历的过程,那么遍历完毕的时候,我们就得到了一颗构建好的workInProgress Tree
那么接着,就是commit阶段了。
4.4 commit阶段
首先我们要知道commit阶段的职责是什么。
这样的话,我们又要强调一下双缓存树了,workInProgress
树是一颗在内存中构建的DOM树,current
树则是页面正在渲染的DOM树。
在此基础上,render阶段已经完成了内存中构建下一状态的workInProgress
,那么此时commit阶段正应该做将current
树与workInProgress
树调换的工作。
而调换工作中,由于render阶段的真实DOM并没有更新,只是做了标记,此时会需要commit阶段负责把这些更新根据不同的操作标记在真实DOM上操作。
commit阶段开始于commitRoot
,往下就是调用commitRootImpl
,我们会着重分析commitRootImpl
首先看入参,可以看到commitRootImpl
的入参有四个,其中root
为最基本的参数,传入的是已准备就绪的workInProgressRootFiber
。
function commitRootImpl( root: FiberRoot, recoverableErrors: null | Array<CapturedValue<mixed>>, transitions: Array<Transition> | null, renderPriorityLevel: EventPriority, )
我们认为commit阶段可以分为三个阶段,分别代表
- before mutation,在执行DOM操作前的阶段
- mutation,执行DOM操作
- layout,执行DOM操作之后
当然,在这些流程之外,commit阶段还会处理useEffect
这类需要在commit阶段执行的hook。
4.4.1 Before commit start
在commit开始之前,即before mutation之前的代码可以从下边看见,它们具体做了什么我直接在代码中注释了,请看注释。
function commitRootImpl( root: FiberRoot, recoverableErrors: null | Array<CapturedValue<mixed>>, transitions: Array<Transition> | null, renderPriorityLevel: EventPriority, ) { do { // 这里会调度未执行完的useEffect,之所以上下各有一处,一方面是和React优先级有关,一方面也和因为调度`useEffect`等hook时重新进入了render阶段重新进入到commit阶段有关。 flushPassiveEffects(); } while (rootWithPendingPassiveEffects !== null); ... // 和flags类似的二进制 if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { throw new Error('Should not already be working.'); } // finishedWork是已经处理好的workInProgressRootFiber const finishedWork = root.finishedWork; const lanes = root.finishedLanes; ... if (finishedWork === null) { return null; } //重置待commit的rootFiber,重置commit优先级 root.finishedWork = null; root.finishedLanes = NoLanes; ... // commitRoot总是同步完成 // 所以在这里清除Scheduler绑定的回调函数等变量允许绑定新的函数 root.callbackNode = null; root.callbackPriority = NoLane; //一些优先级的计算 let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes); const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes(); remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes); markRootFinished(root, remainingLanes); if (root === workInProgressRoot) { // 完成后,重置全局变量 workInProgressRoot = null; workInProgress = null; workInProgressRootRenderLanes = NoLanes; } // 当finishedWork中存在PassiveMask标记时,调度useEffect if ( (finishedWork.subtreeFlags & PassiveMask) !== NoFlags || (finishedWork.flags & PassiveMask) !== NoFlags ) { if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; pendingPassiveEffectsRemainingLanes = remainingLanes; pendingPassiveTransitions = transitions; scheduleCallback(NormalSchedulerPriority, () => { // 这里会调度useEffect的运行,详情请看【useEffect】篇 flushPassiveEffects(); return null; }); } } ... }
这里有一点值得注意的是,伴随着flushPassiveEffects
的调用,在堆栈中完全可能形成多次commit
,这是来源于useEffect
的副作用触发了组件渲染,在这种情况下会再走一次状态更新流程(当然这期间有优化)
4.4.2 BeforeMutation
commit阶段的正式开始,在于commitBeforeMutationEffects
这个函数,可以看到当react确定subtreeFlags或者root.flags上可以找到BeforeMutationMask | MutationMask | LayoutMask | PassiveMask
时,会触发commit的逻辑
var subtreeHasEffects = (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags; var rootHasEffect = (finishedWork.flags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags; if (subtreeHasEffects || rootHasEffect) { ... var shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(root, finishedWork); ... } else { // No effects. root.current = finishedWork; }
那么我们首先来看commitBeforeMutationEffects
,那么可以看到commitBeforeMutationEffects紧接着调用了commitBeforeMutationEffects_begin
。
而commitBeforeMutationEffects_begin做的事情是从finishedWork
向下遍历fiber树,一直到遍历到某个Fiber节点不再有BeforeMutationMask
标记,此时会进入commitBeforeMutationEffects_complete
。
function commitBeforeMutationEffects(root, firstChild) { // 处理焦点相关的逻辑,处理原因是因为真实DOM的增删导致可能出现的焦点变化 focusedInstanceHandle = prepareForCommit(root.containerInfo); // nextEffect是一个全局变量,firstChild对应上方传参`finishedWork` nextEffect = firstChild; commitBeforeMutationEffects_begin(); // 处理Blur相关的逻辑 var shouldFire = shouldFireAfterActiveInstanceBlur; shouldFireAfterActiveInstanceBlur = false; focusedInstanceHandle = null; return shouldFire; } function commitBeforeMutationEffects_begin() { while (nextEffect !== null) { var fiber = nextEffect; var child = fiber.child; if ((fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && child !== null) { child.return = fiber; nextEffect = child; } else { commitBeforeMutationEffects_complete(); } } }
而commitBeforeMutationEffects_complete
同样是做了一次遍历,这次的过程则是不断向上返回,调用过程中不断执行commitBeforeMutationEffectsOnFiber
。
function commitBeforeMutationEffects_complete() { while (nextEffect !== null) { var fiber = nextEffect; setCurrentFiber(fiber); try { commitBeforeMutationEffectsOnFiber(fiber); } catch (error) { captureCommitPhaseError(fiber, fiber.return, error); } resetCurrentFiber(); var sibling = fiber.sibling; if (sibling !== null) { // 注意这里,发现了嘛,和completeWork非常相似的逻辑对吧 sibling.return = fiber.return; nextEffect = sibling; return; } nextEffect = fiber.return; } }
继续到commitBeforeMutationEffectsOnFiber
,发现这里只有两个简单的内容
- 一个是对于ClassComponent会调用getSnapshotBeforeUpdate
- 另一个则是会HostRoot进行
clearContainer(root.containerInfo)
# 小结
那么我们对BeforeMutation阶段进行小结,现在我们知道React在BeforeMutation主要做了两件事
- 处理真实DOM增删后的
focus
、blur
逻辑 - 调用ClassComponent的
getSnapshotBeforeUpdate
生命周期钩子
4.4.3 Mutation
commit第二阶段,我们会进入commitMutationEffects
->commitMutationEffectsOnFiber
if (subtreeHasEffects || rootHasEffect) { ... commitMutationEffects(root, finishedWork, lanes); ... } else { // No effects. root.current = finishedWork; }
commitMutationEffectsOnFiber
是一个368行的函数,它会根据Fiber.tag
和Fiber.flags
走不同的Mutation逻辑
目前来说,除了ScopeComponent
外的所有Component类型都会执行
recursivelyTraverseMutationEffects(root, finishedWork); commitReconciliationEffects(finishedWork);
所以我们首先走入recursivelyTraverseMutationEffects
,可以看到recursivelyTraverseMutationEffects
主要分成两部分。
上边的部分负责从Fiber.deletions
中取出具体的deletions
执行commitDeletionEffects
,后边则是向下遍历节点递归执行commitMutationEffectsOnFiber
。
function recursivelyTraverseMutationEffects(root, parentFiber, lanes) { // Deletions effects can be scheduled on any fiber type. They need to happen // before the children effects hae fired. var deletions = parentFiber.deletions; if (deletions !== null) { for (var i = 0; i < deletions.length; i++) { var childToDelete = deletions[i]; try { commitDeletionEffects(root, parentFiber, childToDelete); } catch (error) { captureCommitPhaseError(childToDelete, parentFiber, error); } } } var prevDebugFiber = getCurrentFiber(); if (parentFiber.subtreeFlags & MutationMask) { var child = parentFiber.child; while (child !== null) { setCurrentFiber(child); commitMutationEffectsOnFiber(child, root); child = child.sibling; } } setCurrentFiber(prevDebugFiber); }
我通览这部分涉及的flags,发现会执行以下内容:
- Update->Insertion:执行React18推出的新hook,
useInsertionEffect
,会包含destory
和create
两个阶段
-
Update->Layout:执行
useLayoutEffect
上一次执行残留的destory
函数 -
Placement:
-
Deletions:删除节点
-
Update,more
-
Hydrating :SSR相关,由于博主目前为止没有实践过SSR,所以不说。
-
Ref:safelyDetachRef
-
ContentReset
-
Visibility
...
打住,有点多了!我们只关注Update
,Deletions
,Placement
,并且只关注HostComponent
#1 Update
关于FunctionComponent的Update,做的事情其实就在上方前亮点
而对于HostComponent,react 会执行这些内容:
这里最核心的就是commitUpdate
,React会通过updateProperties
将DOM属性更新到真实节点上
function commitUpdate(domElement, updatePayload, type, oldProps, newProps, internalInstanceHandle) { // Apply the diff to the DOM node. updateProperties(domElement, updatePayload, type, oldProps, newProps); // Update the props handle so that we know which props are the ones with // with current event handlers. updateFiberProps(domElement, newProps); }
(我们其实遇到过类似的函数⬆️)
react还会把这个属性也更新上去,在我这篇文章中有这个属性的应用
#2 Placement
我们只说HostComponent
的逻辑,只有真实节点会走到这里,另外两个tagHostRoot
,HostPortal
,相比HostComponent只是缺少了ContextReset
的内容。
(如果其他类型的tag走到commitPlacement是会报错的)
那么这里其实主要就是三步:
-
获取Fiber节点存在HostFiber的父节点,并最终获得真实DOM
-
获取Fiber节点的兄弟真实DOM节点
-
insertOrAppendPlacementNodeIntoContainer,将节点插入或添加到父容器中
走Placement完毕,可以很明显看到页面渲染
(appendChildToContainer函数涉及真实DOM的插入/添加操作)
#3 Deletion
deletions是在beginWork的diff过程中获得的
- 调用被删除节点的
componentWillUnmount
生命周期钩子,从页面移除Fiber节点
对应DOM节点
- 安全解绑ref
4.4.4 Layout
进入layout阶段,证明DOM节点已经渲染完毕了
//将current指向已经完成的workInProgress root.current = finishedWork; commitLayoutEffects(finishedWork, root, lanes);
function commitLayoutEffects(finishedWork, root, committedLanes) { inProgressLanes = committedLanes; inProgressRoot = root; var current = finishedWork.alternate; commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes); inProgressLanes = null; inProgressRoot = null; }
commitLayoutEffects->commitLayoutEffectOnFiber
会按照我们熟悉的流程做递归
(commitLayoutEffectOnFiber和recursivelyTraverseLayoutEffects递归调用)
我们需要关注的是commitLayoutEffectOnFiber
中的内容
function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork, committedLanes) { // When updating this function, also update reappearLayoutEffects, which does // most of the same things when an offscreen tree goes from hidden -> visible. var flags = finishedWork.flags; switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: { recursivelyTraverseLayoutEffects(finishedRoot, finishedWork, committedLanes); //调度useLayoutEffect的create if (flags & Update) { commitHookLayoutEffects(finishedWork, Layout | HasEffect); } break; } case ClassComponent: { recursivelyTraverseLayoutEffects(finishedRoot, finishedWork, committedLanes); //调度componentDidUpdate、componentDidMount等class组件的生命周期钩子 if (flags & Update) { commitClassLayoutLifecycles(finishedWork, current); } if (flags & Callback) { commitClassCallbacks(finishedWork); } //用真实DOM更新ref if (flags & Ref) { safelyAttachRef(finishedWork, finishedWork.return); } break; } ... case HostComponent: { recursivelyTraverseLayoutEffects(finishedRoot, finishedWork, committedLanes); // 这里会调度组件的docus、img的src标签 if (current === null && flags & Update) { commitHostComponentMount(finishedWork); } //用真实DOM更新ref if (flags & Ref) { safelyAttachRef(finishedWork, finishedWork.return); } break; } ... } }
此时React会做一些收尾的工作,正如我在给文章收尾一样,内容是比较少(水)的。
-
调度
useLayoutEffect
的开始阶段 -
调度componentDidUpdate、componentDidMount等class组件的生命周期钩子
-
真实dom上的focus处理、img标签的src处理
-
AttachRef,获取真实DOM,更新
ref
更多内容其实都非常好理解,我推荐直接动手看。
4.4.5 After commit end
当然,在layout阶段结束后仍有一些收尾工作。
var rootDidHavePassiveEffects = rootDoesHavePassiveEffects; //上边执行useEffect时会标记rootDoesHavePassiveEffects=true //这里会对相关内容进行清除 if (rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = false; rootWithPendingPassiveEffects = root; pendingPassiveEffectsLanes = lanes; } else { releaseRootPooledCache(root, remainingLanes); } ... //和react-refresh-runtime相关的模块 onCommitRoot(finishedWork.stateNode, renderPriorityLevel); ... // 确保root有一个新的调度,我想找机会试试把这句话注释 ensureRootIsScheduled(root, now()); // 一些错误处理 if (recoverableErrors !== null) { var onRecoverableError = root.onRecoverableError; for (var i = 0; i < recoverableErrors.length; i++) { var recoverableError = recoverableErrors[i]; var componentStack = recoverableError.stack; var digest = recoverableError.digest; onRecoverableError(recoverableError.value, { componentStack: componentStack, digest: digest }); } } if (hasUncaughtError) { hasUncaughtError = false; var error$1 = firstUncaughtError; firstUncaughtError = null; throw error$1; } // React注释:请再次阅读,因为被动效果可能会更新它 if (includesSomeLane(pendingPassiveEffectsLanes, SyncLane) && root.tag !== LegacyRoot) { flushPassiveEffects(); } // 无限重渲染的计数 remainingLanes = root.pendingLanes; if (includesSomeLane(remainingLanes, SyncLane)) { if (root === rootWithNestedUpdates) { nestedUpdateCount++; } else { nestedUpdateCount = 0; rootWithNestedUpdates = root; } } else { nestedUpdateCount = 0; } // If layout work was scheduled, flush it now. // 执行一些同步任务,这样无需等待在下一次循环的时候进行,这里可以参考ensureRootIsScheduled flushSyncCallbacks(); return null;
那么至此,commit阶段算已经完成了。
但是React的渲染却不能算完成,正如我一开始读源码的初衷是为了知道,我在useEffect里调用了更新,这个执行时机和触发渲染原理是什么情况。
到了这里我会明白,由于我们上述的各种effect、生命周期狗子,此时完全可能再次触发更新。
而react也会很自然地走进一个新的render+commit的过程,先将触发更新的内容更新后再继续原本未更新的。
对于React来讲,会在flushWork执行完毕后才真正进入空闲。但是这就是后话了
(flushWork函数)
5 总结
不管在面试还是在生活中,都曾有人问我为什么要看React源码。
我刚开始是因为对于hook的架构感兴趣而去看的,而现在随着阅读逐渐深入,我发现阅读react源码一方面给了我比较强的成就感,这也是我可以坚持下来的原因。另一方面,我们真的会在阅读中体会到某些思想上的高明。
比如,二进制flags、useEffect形成的环形更新链条
阅完本文,期待你对React18的Fiber架构有了更新的认识,也理解了React状态更新的全流程,更期望你可以将学到的东西真实应用在自己的生活、工作中,我认为这才是读源码最重要的。
那么这里留几个关于React的问题,默想3分钟,把收获沉淀在脑海中。
- 总结一下beginWork和completeWork的工作内容
- useLayoutEffect在什么时机执行
- react是在什么时候、怎么存储、怎么应用操作依据的?
6 尾声
Hi~你好,再次认识一下,我是心锁,致力于前端开发的软件开发工程师。
这是我第一篇单字符数破5w,字数破1w的文章,耗时一个月零四天。
所以非常期待你的点赞、收藏、分析~
后续呢,我会进行必要的切割,分多文方便阅读,同时补充更多细节,所以非常期待你的关注。
- https://github.com/GrinZero 这是我的github,我会在上边更新脑子里突然蹦出来的主意,欢迎你的follow,后续也会把react解读更新上去。
(部分项目成果集合图)
- https://juejin.cn/user/1645288319627576/posts 这是我的掘金个人主页,期待你的关注。