[通明境 · React架构]通俗地讲React,优雅地理解React

  • [通明境 · React架构]通俗地讲React,优雅地理解React已关闭评论
  • 200 次浏览
  • A+
所属分类:Web前端
摘要

大家好,我是心锁,一枚23届准毕业生。如果读者阅读过我其他几篇React相关的文章,就知道这次我是来填坑的了


1 前言

大家好,我是心锁,一枚23届准毕业生。

如果读者阅读过我其他几篇React相关的文章,就知道这次我是来填坑的了

原因是,写了两篇解读react-hook的文章后我发现——并不是每位同学都清楚React的架构,包括我在内也只是综合不同技术文章与阅读部分源码有一个了解,但是调试时真正沉淀成文章的还没有。

[通明境 · React架构]通俗地讲React,优雅地理解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等

    [通明境 · React架构]通俗地讲React,优雅地理解React

  • key:更新key会强制更新Fiber节点

  • type:保存组件本身。准确来说,对于函数组件保存函数本身,对于类组件保存类本身,对于HostComponent,也就是如原生<div></div>这类原生标签会保存节点名称

  • elementType:保存组件类型和type大部分情况是一样的,但是也有不一样的情况,比如LazyComponent

  • stateNode:保存Fiber对应的真实DOM节点

  • ref: 和key一样属于base字段

    [通明境 · React架构]通俗地讲React,优雅地理解React

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>   ); }  

[通明境 · React架构]通俗地讲React,优雅地理解React

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(可传入)进来的

[通明境 · React架构]通俗地讲React,优雅地理解React

也就是说,这部分props可以在Fiber间传递,主要用于更新/创造新Fiber节点时用来传递props

2.3.2 memoizedProps

memoizedPropspendingProps的区别是什么呢?

我们知道,props代表一个Function的参数,当props变化时Function也会再次执行。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

一般来讲,memoizedProps会在整个渲染流程结尾部分被更新,存储FiberNode的props。

pendingProps一般在渲染开始时,作为新的Props出现

[通明境 · React架构]通俗地讲React,优雅地理解React

举个更便于理解的例子,在如图的beginWork阶段,会对比新的props和旧的props来确定是否更新,此时比较的就是workInProgress.pendingPropscurrent.memoizedProps

[通明境 · React架构]通俗地讲React,优雅地理解React

2.3.3 updateQueue

上一篇我们讲useEffect有讲到,updateQueue以如图的形式存储useEffect运行时生成的各个effect

[通明境 · React架构]通俗地讲React,优雅地理解React

lastEffect以环形链的形式存储了单个节点的所有effect。

(当然,这里指的当然只是函数式组件)

2.3.4 memoizedState

useState章节,我们也有讲过memoizedStatememoizedState存储了我们调用hook时产生的hook对象,目前已知除了useContext不会有hook对象产生并挂载,其他hook都会挂载到这里。

[通明境 · React架构]通俗地讲React,优雅地理解React

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获取到的上下文

[通明境 · React架构]通俗地讲React,优雅地理解React

从调试结果看,多个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。

[通明境 · React架构]通俗地讲React,优雅地理解React

至于具体如何保存,实际上是使用了二进制的特性,举几个例子

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架构]通俗地讲React,优雅地理解React

那么作用是什么呢?其实也很容易从函数上下文分析出来,对于图中这个例子,react通过~运算符&运算符的结合,从flags中删除了Placement这个flag。

2.4.4 小总结:React中常见的操作

  • 通过unknownFlags & Placement判断unknownFlags是否包含Placement

  • 通过unknownFlags |= PlacementPlacement合并进unknownFlags

  • 通过unknownFlags &= ~PlacementPlacementunknownFlags中删去

关于有哪些flags,我们可以翻阅到ReactFiberFlags.js,这里会有详细flags的记载

[通明境 · React架构]通俗地讲React,优雅地理解React

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已经在构建

[通明境 · React架构]通俗地讲React,优雅地理解React

(图误,在renderWithHooks才对)

而当我们再次渲染,在renderWithHooks断点,就可以观察到workInProgress.alternate==current

[通明境 · React架构]通俗地讲React,优雅地理解React

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在配合。

[通明境 · React架构]通俗地讲React,优雅地理解React

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保存基本数据的地方。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

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可能不好理解,我们可以从代码中理解,且看这里

[通明境 · React架构]通俗地讲React,优雅地理解React

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触发===异步触发)。

[通明境 · React架构]通俗地讲React,优雅地理解React

那这里可以看到,我们可以从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;   } } 

[通明境 · React架构]通俗地讲React,优雅地理解React

3.3 baseQueue

值得注意的是,baseQueue的结构来自queue.pending而不是queue

[通明境 · React架构]通俗地讲React,优雅地理解React

(baseQueue被赋值queue.pending)

其余的大抵是没啥好说的,baseQueue在调试中的体现我暂时并没有遇到,推测需要有比较大量的更新。

4 React架构

本章我们讲述React的渲染流程,将覆盖React的render阶段与commit阶段的概念与流程概览,不会非常深入,争取留存印象。

4.1 React渲染关键节点

我们已经预先知道可以将React的渲染分成render阶段和commit阶段,也知道render阶段的关键函数是beginWorkcompleteWorkcommit阶段的关键函数则是commitRoot

在这个基础上,我们从调用堆栈中可以找到这两个阶段的起始节点。

  • render阶段

我们在beginWork中打上断点,然后可以回溯调用堆栈找到出发点。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

从图中,我们可以知道renderRoot触发于performConcurrentWorkOnRoot

[通明境 · React架构]通俗地讲React,优雅地理解React

除此之外,在performSyncWorkOnRoot中也可以走入renderRoot

[通明境 · React架构]通俗地讲React,优雅地理解React

它们会根据情况走到renderRootConcurrent或者renderRootSync,这里即是render阶段的开始点

那么我们得到第一个关键节点:

  • render阶段开始于renderRootConcurrentrenderRootSync
  • commit阶段

我们知道,render阶段的尾巴是completeWork,commit阶段的起步是commitRoot,我们尝试在这completeWork方法中断点,然后单步调试到commitRoot

[通明境 · React架构]通俗地讲React,优雅地理解React

上图是我debug出来的结果,completeWorkcommitRoot之间的最近公共函数节点是performSyncWorkOnRoot/performConcurrentWorkOnRoot

那么我们知道,commitRoot即是commit阶段的起点。

那么我们得到两个关键信息:

  • commit阶段开始于commitRoot
  • render阶段和commit阶段通过performSyncWorkOnRoot/performConcurrentWorkOnRoot联动

4.1.1 小总结

  • render阶段开始于renderRootConcurrentrenderRootSync
  • 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; } 

[通明境 · React架构]通俗地讲React,优雅地理解React

寻找root节点是一个向上不断寻找root节点的过程,在这个过程中react还会持续调用detectUpdateOnUnmountedFiber检查是否调用了过期的更新函数。

[通明境 · React架构]通俗地讲React,优雅地理解React

什么是过期的更新函数?举个例子,通过useRef保存了setState方法,但是随着组件更新ref中的setState方法并没有更新,此时由于setState方法本质上是通过.bind的形式报存了函数及参数fiber节点,此时就会存在调用了一个已卸载组件的过期的setState方法。

4.2.2 调度同步/异步更新

找到root节点之后,那么就要进入render流程,这就存在一个问题。

我们上边说了,render阶段的触发函数是performSyncWorkOnRootperformConcurrentWorkOnRoot,那么如何判断应该进入同步更新还是异步更新呢?

这就要走到ensureRootIsScheduledensureRootIsScheduled会通过判断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))的过程。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

值得注意的是,同步调度这里还更复杂,react一方面需要考虑是否是严格模式做不同的callback

[通明境 · React架构]通俗地讲React,优雅地理解React

(ensureRootIsScheduled是一个很重要的函数,会Scheduled一起讲会比较好)

另一方面还调度了flushSynCallbacks,这个函数做的事情很简单,就是把syncQueue中的待执行任务全部执行

[通明境 · React架构]通俗地讲React,优雅地理解React [通明境 · React架构]通俗地讲React,优雅地理解React

4.2.3 render阶段

render阶段分成了两个阶段,我们在状态更新流程中不讲细节,只讲明基本作用,细节请看后边的单章

经历了调度更新,会来到render阶段,render阶段做了两件事。

  • beginWork阶段。在这个阶段react做的事情是从root递归到子叶,每次beginWork会对Fiber节点进行新建/复用逻辑,然后通过reconcileChildrenchild 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。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

我们提炼一下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.  

    [通明境 · React架构]通俗地讲React,优雅地理解React

  • 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);       } 		...   } 

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

根据我们在【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

[通明境 · React架构]通俗地讲React,优雅地理解React

不管是reconcileChildFibers还是mountChildFibers,都是通过调用ChildReconciler这个函数来运行的。

[通明境 · React架构]通俗地讲React,优雅地理解React

而在整个ChildReconciler中,我们会经常性看到如图一样的操作。

[通明境 · React架构]通俗地讲React,优雅地理解React

这便引出了操作依据一说,react用Fiber.flags并以二进制的形式存贮了对于每个Fiber的操作依据,这种方式比数组更高效,可以方便地使用位运算发为Fiber.flags增删不同的操作依据。

[通明境 · React架构]通俗地讲React,优雅地理解React

点击这里可以查看所有的操作类型

#3 diff算法*

标记这个知识点,下次再说

4.3.2 completeWork

我们持续执行workLoop,会发现workInProgressrootFiber持续深入到了我的调试代码中的最底层(一个div),此时就到了render阶段的第二个阶段completeWork

function performUnitOfWork(unitOfWork) {   ...    if (next === null) {     // 进入completeWork     completeUnitOfWork(unitOfWork);   } else {     ...   }    ... } 

那么此时进入completeUnitOfWork,这里的核心逻辑是completeWork从子节点不断访问workInProgress.return向上循环执行beginWork,如果遇到兄弟子节点,则会将workInProgress指向兄弟节点并返回至performUnitOfWork。重新执行beginWork到completeWork的整个render阶段。

[通明境 · React架构]通俗地讲React,优雅地理解React

那么completeWork做了什么?这里是completeWork的基本逻辑框架(我把bubbleProperties提出来方便理解每个completeWork都会执行这前后两条语句),做了popTreeContextbubbleProperties

function completeWork(current, workInProgress, renderLanes) {   popTreeContext(workInProgress);    switch (workInProgress.tag) {     case FunctionComponent:       ...     case HostComponent:       ...     ...   }   bubbleProperties(workInProgress); } 

popTreeContext是和上边beginWork相关的内容,这里的目的是使得正在进行的工作不处于堆栈顶部。对应pushContext的阶段一般在beginWork的swtich中进入的函数中都可以找到

[通明境 · React架构]通俗地讲React,优雅地理解React

bubbleProperties的核心逻辑我也提了出来,可以看到这里是做了一个层遍历,遍历了completedWorkFiber的所有child,将它们的return赋值为completedWorkFiber。同时,这里也涉及了subtreeFlags的计算,会将子节点的操作依据冒泡到父节点。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

而关于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的逻辑,代表原生组件。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

下边是我提炼出来的核心逻辑,这里同样会区分updatemount

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

[通明境 · React架构]通俗地讲React,优雅地理解React

[通明境 · React架构]通俗地讲React,优雅地理解React

具体会处理哪些props,我们深入到diffProperties就可以找到这一块的逻辑

[通明境 · React架构]通俗地讲React,优雅地理解React

[通明境 · React架构]通俗地讲React,优雅地理解React

OK,那么我们回到上边所说的updatePayload,调试发现updatePayload是一个数组,数据结构体现为一个偶数为key,奇数为value的数组:

[通明境 · React架构]通俗地讲React,优雅地理解React

到了这一步,update流程最后会走入markUpdate,至此。completeWork的update逻辑完毕

[通明境 · React架构]通俗地讲React,优雅地理解React

#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

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

那么接着,就是commit阶段了。

4.4 commit阶段

首先我们要知道commit阶段的职责是什么。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

这样的话,我们又要强调一下双缓存树了,workInProgress树是一颗在内存中构建的DOM树,current树则是页面正在渲染的DOM树。

在此基础上,render阶段已经完成了内存中构建下一状态的workInProgress,那么此时commit阶段正应该做将current树与workInProgress树调换的工作。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

而调换工作中,由于render阶段的真实DOM并没有更新,只是做了标记,此时会需要commit阶段负责把这些更新根据不同的操作标记在真实DOM上操作。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

commit阶段开始于commitRoot,往下就是调用commitRootImpl,我们会着重分析commitRootImpl

[通明境 · React架构]通俗地讲React,优雅地理解React

[通明境 · React架构]通俗地讲React,优雅地理解React

首先看入参,可以看到commitRootImpl的入参有四个,其中root为最基本的参数,传入的是已准备就绪的workInProgressRootFiber

function commitRootImpl(   root: FiberRoot,   recoverableErrors: null | Array<CapturedValue<mixed>>,   transitions: Array<Transition> | null,   renderPriorityLevel: EventPriority, ) 

[通明境 · React架构]通俗地讲React,优雅地理解React

我们认为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的副作用触发了组件渲染,在这种情况下会再走一次状态更新流程(当然这期间有优化)

[通明境 · React架构]通俗地讲React,优雅地理解React

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

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

而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)

[通明境 · React架构]通俗地讲React,优雅地理解React

[通明境 · React架构]通俗地讲React,优雅地理解React

# 小结

那么我们对BeforeMutation阶段进行小结,现在我们知道React在BeforeMutation主要做了两件事

  • 处理真实DOM增删后的 focusblur逻辑
  • 调用ClassComponent的getSnapshotBeforeUpdate生命周期钩子

4.4.3 Mutation

commit第二阶段,我们会进入commitMutationEffects->commitMutationEffectsOnFiber

  if (subtreeHasEffects || rootHasEffect) {     ...     commitMutationEffects(root, finishedWork, lanes);     ...   } else {     // No effects.     root.current = finishedWork;   } 

[通明境 · React架构]通俗地讲React,优雅地理解React

commitMutationEffectsOnFiber是一个368行的函数,它会根据Fiber.tagFiber.flags走不同的Mutation逻辑

[通明境 · React架构]通俗地讲React,优雅地理解React

目前来说,除了ScopeComponent外的所有Component类型都会执行

recursivelyTraverseMutationEffects(root, finishedWork); commitReconciliationEffects(finishedWork); 

所以我们首先走入recursivelyTraverseMutationEffects,可以看到recursivelyTraverseMutationEffects主要分成两部分。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

上边的部分负责从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); } 

[通明境 · React架构]通俗地讲React,优雅地理解React

我通览这部分涉及的flags,发现会执行以下内容:

  • Update->Insertion:执行React18推出的新hook,useInsertionEffect,会包含destorycreate两个阶段

[通明境 · React架构]通俗地讲React,优雅地理解React

  • Update->Layout:执行useLayoutEffect上一次执行残留的destory函数

    [通明境 · React架构]通俗地讲React,优雅地理解React

  • Placement:

[通明境 · React架构]通俗地讲React,优雅地理解React

  • Deletions:删除节点

  • Update,more

    [通明境 · React架构]通俗地讲React,优雅地理解React

  • Hydrating :SSR相关,由于博主目前为止没有实践过SSR,所以不说。

  • Ref:safelyDetachRef

  • ContentReset

  • Visibility

    ...

打住,有点多了!我们只关注UpdateDeletionsPlacement,并且只关注HostComponent

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

#1 Update

关于FunctionComponent的Update,做的事情其实就在上方前亮点

而对于HostComponent,react 会执行这些内容:

[通明境 · React架构]通俗地讲React,优雅地理解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架构]通俗地讲React,优雅地理解React

[通明境 · React架构]通俗地讲React,优雅地理解React

(我们其实遇到过类似的函数⬆️)

react还会把这个属性也更新上去,在我这篇文章中有这个属性的应用

[通明境 · React架构]通俗地讲React,优雅地理解React

#2 Placement

我们只说HostComponent的逻辑,只有真实节点会走到这里,另外两个tagHostRootHostPortal,相比HostComponent只是缺少了ContextReset的内容。

[通明境 · React架构]通俗地讲React,优雅地理解React

(如果其他类型的tag走到commitPlacement是会报错的)

那么这里其实主要就是三步:

  • 获取Fiber节点存在HostFiber的父节点,并最终获得真实DOM

    [通明境 · React架构]通俗地讲React,优雅地理解React

    [通明境 · React架构]通俗地讲React,优雅地理解React

  • 获取Fiber节点的兄弟真实DOM节点

  • insertOrAppendPlacementNodeIntoContainer,将节点插入或添加到父容器中

[通明境 · React架构]通俗地讲React,优雅地理解React

[通明境 · React架构]通俗地讲React,优雅地理解React

走Placement完毕,可以很明显看到页面渲染

[通明境 · React架构]通俗地讲React,优雅地理解React

(appendChildToContainer函数涉及真实DOM的插入/添加操作)

#3 Deletion

deletions是在beginWork的diff过程中获得的

  • 调用被删除节点的componentWillUnmount生命周期钩子,从页面移除Fiber节点对应DOM节点

[通明境 · React架构]通俗地讲React,优雅地理解React

  • 安全解绑ref

[通明境 · React架构]通俗地讲React,优雅地理解React

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会按照我们熟悉的流程做递归

[通明境 · React架构]通俗地讲React,优雅地理解React

[通明境 · React架构]通俗地讲React,优雅地理解React

(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阶段算已经完成了。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

但是React的渲染却不能算完成,正如我一开始读源码的初衷是为了知道,我在useEffect里调用了更新,这个执行时机和触发渲染原理是什么情况。

到了这里我会明白,由于我们上述的各种effect、生命周期狗子,此时完全可能再次触发更新。

而react也会很自然地走进一个新的render+commit的过程,先将触发更新的内容更新后再继续原本未更新的。

[通明境 &#183; React架构]通俗地讲React,优雅地理解React

对于React来讲,会在flushWork执行完毕后才真正进入空闲。但是这就是后话了

[通明境 · React架构]通俗地讲React,优雅地理解React

(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解读更新上去。

[通明境 · React架构]通俗地讲React,优雅地理解React

(部分项目成果集合图)