大家好,我是心锁,一枚23届准毕业生。
如果读者阅读过我其他几篇React相关的文章,就知道这次我是来填坑的了
原因是,写了两篇解读react-hook的文章后我发现——并不是每位同学都清楚React的架构,包括我在内也只是综合不同技术文章与阅读部分源码有一个了解,但是调试时真正沉淀成文章的还没有。
所以这篇文章来啦~文章基于2022年8月9月的React源码进行调试及阅读,将以通俗的形式揭秘React
阅读本文,成本与收益如下
阅读耗时:26min+
全文字数:1.1w+
全文字符:5.5w+
预期收益:通明境 · React架构
本文适合有阅读React源码计划的初学者或者正在阅读React源码的工程师,我们一起形成头脑风暴。
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,也就是如原生
这类原生标签会保存节点名称elementType:保存组件类型和type大部分情况是一样的,但是也有不一样的情况,比如LazyComponent
stateNode:保存Fiber对应的真实DOM节点
ref: 和key一样属于base字段
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节点这四个属性,它们的含义分别是
这样子一来,对于我们这里的组件,就构成了如图的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>
);
}
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节点上存储的这些结构的作用。
pendingProps,从FiberNode的构造函数看,是mixed(可传入)进来的
也就是说,这部分props可以在Fiber间传递,主要用于更新/创造新Fiber节点时用来传递props
memoizedProps
和pendingProps
的区别是什么呢?
我们知道,props代表一个Function的参数,当props变化时Function也会再次执行。
一般来讲,memoizedProps
会在整个渲染流程结尾部分被更新,存储FiberNode的props。
而pendingProps
一般在渲染开始时,作为新的Props出现
举个更便于理解的例子,在如图的beginWork
阶段,会对比新的props和旧的props来确定是否更新,此时比较的就是workInProgress.pendingProps
和current.memoizedProps
上一篇我们讲useEffect
有讲到,updateQueue
以如图的形式存储useEffect
运行时生成的各个effect
lastEffect以环形链的形式存储了单个节点的所有effect。
(当然,这里指的当然只是函数式组件)
在useState
章节,我们也有讲过memoizedState
,memoizedState
存储了我们调用hook时产生的hook
对象,目前已知除了useContext不会有hook对象产生并挂载,其他hook都会挂载到这里。
hook之间以.next
相连形成单向链表。
而hook调用时产生的不管是effect(useEffect)还是state(useState),都是存储在
hook.memoizedState
,体现在Fiber节点上,其实是存储在hook.memoizedState.memoizedState
,注意不要混淆。
以下是调试代码
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
相连,同时显然,这是一条单向链表
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。
至于具体如何保存,实际上是使用了二进制的特性,举几个例子
温习一下
&运算符
的规则:只有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
温习一下
|运算符
的规则:只有0&0=0,其他情况为1
上边unknownFlags的例子我们不难发现,react利用了|运算符
的特性来存储flag
const unknownFlags = Placement|Update; //此时=0b000000000000000000000110
这样的好处是快,判断是否包含的时候,直接使用& 运算符
,在有限的操作依据面前,使用二进制完全可以兜住所有情况。
~运算符会把每一位取反,即1->0,0->1
在React中,~运算符同样是常用操作
那么作用是什么呢?其实也很容易从函数上下文分析出来,对于图中这个例子,react通过~运算符
与&运算符
的结合,从flags中删除了Placement
这个flag。
通过unknownFlags & Placement
判断unknownFlags
是否包含Placement
通过unknownFlags |= Placement
将Placement
合并进unknownFlags
中
通过unknownFlags &= ~Placement
将Placement
从unknownFlags
中删去
关于有哪些flags,我们可以翻阅到
ReactFiberFlags.js
,这里会有详细flags的记载
我们曾说过,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
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
...
this.lanes = NoLanes;
this.childLanes = NoLanes;
...
}
和lane有关的变量统一和调度优先级有关,暂时不涉及(因为还没看)
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在配合。
我们上边有讲到FiberNode.memoizedState
,我们知道这里保存的是mountWorkInProgressHook
时产生的hook对象
{
memoizedState: 0,
baseState: 0,
baseQueue: null,
queue: ???,
next:null
}
那么hook的各个项指什么?
其实很好理解,baseState对应上一次的state(effect),memoizedState为最新的state(effect),总之就是hook保存基本数据的地方。
而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的结构,我们逐一讲解
其中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;
}
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) ); }
pending 属性存储排队中的状态变更规则,单向环形链表结构。
在源码中,每一个规则以Update
的结构连接
export type Update<S, A> = {|
lane: Lane,
action: A,
hasEagerState: boolean,
eagerState: S | null,
next: Update<S, A>,
|};
那么我们知道了
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;
}
}
值得注意的是,baseQueue的结构来自queue.pending而不是queue
(baseQueue被赋值queue.pending)
其余的大抵是没啥好说的,baseQueue在调试中的体现我暂时并没有遇到,推测需要有比较大量的更新。
本章我们讲述React的渲染流程,将覆盖React的render
阶段与commit
阶段的概念与流程概览,不会非常深入,争取留存印象。
我们已经预先知道可以将React的渲染分成render
阶段和commit
阶段,也知道render
阶段的关键函数是beginWork
和completeWork
,commit
阶段的关键函数则是commitRoot
。
在这个基础上,我们从调用堆栈中可以找到这两个阶段的起始节点。
我们在beginWork中打上断点,然后可以回溯调用堆栈找到出发点。
从图中,我们可以知道renderRoot触发于performConcurrentWorkOnRoot
除此之外,在performSyncWorkOnRoot
中也可以走入renderRoot
它们会根据情况走到renderRootConcurrent
或者renderRootSync
,这里即是render阶段的开始点
那么我们得到第一个关键节点:
- render阶段开始于
renderRootConcurrent
或renderRootSync
我们知道,render阶段的尾巴是completeWork
,commit阶段的起步是commitRoot
,我们尝试在这completeWork
方法中断点,然后单步调试到commitRoot
。
上图是我debug出来的结果,completeWork
与commitRoot
之间的最近公共函数节点是performSyncWorkOnRoot/performConcurrentWorkOnRoot
。
那么我们知道,commitRoot
即是commit阶段的起点。
那么我们得到两个关键信息:
- commit阶段开始于
commitRoot
- render阶段和commit阶段通过
performSyncWorkOnRoot/performConcurrentWorkOnRoot
联动
renderRootConcurrent
或renderRootSync
commitRoot
performSyncWorkOnRoot/performConcurrentWorkOnRoot
联动正常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方法。
找到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中的待执行任务全部执行
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阶段要负责的了
commit阶段,除了会处理一下和hook
相关的事情之外,最主要做了就是负责把beginWork阶段记录的flags在真实DOM树上进行操作。
总结来说:
useEffect\useInsertionEffect\useLayoutEffect
相关的hook,处理class组件相关的生命周期钩子这里是延续状态更新流程的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;
...
}
那么此处引出了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:
...
}
}
看到这里,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;
...
}
...
}
不满足更新条件的话,会根据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算法需要开单章
Fiber
节点进行diff比较,将比较的结果生成新Fiber
节点当然,不管走到哪里,workInProgress都会得到一个child FIber
不管是reconcileChildFibers
还是mountChildFibers
,都是通过调用ChildReconciler这个函数来运行的。
而在整个ChildReconciler中,我们会经常性看到如图一样的操作。
这便引出了操作依据一说,react用Fiber.flags
并以二进制的形式存贮了对于每个Fiber的操作依据,这种方式比数组更高效,可以方便地使用位运算发为Fiber.flags
增删不同的操作依据。
点击这里可以查看所有的操作类型
标记这个知识点,下次再说
我们持续执行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;
}
...
}
}
update时,无需生成新的DOM节点,所以此时要处理props,在updateHostComponent
中,第二部分会调用prepareUpdate->diffProperties
获得一个updatePayload挂载在workInProgress.updateQueue
上
具体会处理哪些props,我们深入到diffProperties
就可以找到这一块的逻辑
OK,那么我们回到上边所说的updatePayload
,调试发现updatePayload
是一个数组,数据结构体现为一个偶数为key,奇数为value的数组:
到了这一步,update流程最后会走入markUpdate
,至此。completeWork的update逻辑完毕
我们此时来看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阶段了。
首先我们要知道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阶段可以分为三个阶段,分别代表
当然,在这些流程之外,commit阶段还会处理useEffect
这类需要在commit阶段执行的hook。
在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
的副作用触发了组件渲染,在这种情况下会再走一次状态更新流程(当然这期间有优化)
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
,发现这里只有两个简单的内容
clearContainer(root.containerInfo)
那么我们对BeforeMutation阶段进行小结,现在我们知道React在BeforeMutation主要做了两件事
focus
、blur
逻辑getSnapshotBeforeUpdate
生命周期钩子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,发现会执行以下内容:
useInsertionEffect
,会包含destory
和create
两个阶段Deletions:删除节点
Update,more
Hydrating :SSR相关,由于博主目前为止没有实践过SSR,所以不说。
Ref:safelyDetachRef
ContentReset
Visibility
…
打住,有点多了!我们只关注Update
,Deletions
,Placement
,并且只关注HostComponent
关于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还会把这个属性也更新上去,在我这篇文章中有这个属性的应用
我们只说HostComponent
的逻辑,只有真实节点会走到这里,另外两个tagHostRoot
,HostPortal
,相比HostComponent只是缺少了ContextReset
的内容。
(如果其他类型的tag走到commitPlacement是会报错的)
那么这里其实主要就是三步:
获取Fiber节点存在HostFiber的父节点,并最终获得真实DOM
获取Fiber节点的兄弟真实DOM节点
insertOrAppendPlacementNodeIntoContainer,将节点插入或添加到父容器中
走Placement完毕,可以很明显看到页面渲染
(appendChildToContainer函数涉及真实DOM的插入/添加操作)
deletions是在beginWork的diff过程中获得的
componentWillUnmount
生命周期钩子,从页面移除Fiber节点
对应DOM节点
进入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
更多内容其实都非常好理解,我推荐直接动手看。
当然,在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函数)
不管在面试还是在生活中,都曾有人问我为什么要看React源码。
我刚开始是因为对于hook的架构感兴趣而去看的,而现在随着阅读逐渐深入,我发现阅读react源码一方面给了我比较强的成就感,这也是我可以坚持下来的原因。另一方面,我们真的会在阅读中体会到某些思想上的高明。
比如,二进制flags、useEffect形成的环形更新链条
阅完本文,期待你对React18的Fiber架构有了更新的认识,也理解了React状态更新的全流程,更期望你可以将学到的东西真实应用在自己的生活、工作中,我认为这才是读源码最重要的。
那么这里留几个关于React的问题,默想3分钟,把收获沉淀在脑海中。
Hi~你好,再次认识一下,我是心锁,致力于前端开发的软件开发工程师。
这是我第一篇单字符数破5w,字数破1w的文章,耗时一个月零四天。
所以非常期待你的点赞、收藏、分享~
后续呢,我会进行必要的切割,分多文方便阅读,同时补充更多细节,所以非常期待你的关注。
- github.com/GrinZero 这是我的github,我会在上边更新脑子里突然蹦出来的主意,欢迎你的follow,后续也会把react解读更新上去。
(部分项目成果集合图)
- juejin.cn/user/164528… 这是我的掘金个人主页,期待你的关注。