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

2022-09-14 17:00:07 浏览数 (1)

1 前言

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

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

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

所以这篇文章来啦~文章基于2022年八九月的React源码进行调试及阅读,将以通俗的形式揭秘React

阅读本文,成本与收益如下

阅读耗时:26min 全文字数:1w 全文字符:5.5w 预期收益:通明境 · React架构

本文适合有阅读React源码计划的初学者或者正在阅读React源码的工程师,我们一起形成头脑风暴。

2 认识Fiber节点

2.1 Fiber节点基础部分

代码语言:html复制
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等

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c00b82a199474944b3ba92d9757a4725~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220828220452391" style="zoom:67%;" />

  • key:更新key会强制更新Fiber节点
  • type:保存组件本身。准确来说,对于函数组件保存函数本身,对于类组件保存类本身,对于HostComponent,也就是如原生<div></div>这类原生标签会保存节点名称
  • elementType:保存组件类型和type大部分情况是一样的,但是也有不一样的情况,比如LazyComponent
  • stateNode:保存Fiber对应的真实DOM节点
  • ref: 和key一样属于base字段

2.2 Fiber树结构实现

代码语言:html复制
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树

代码语言:html复制
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>
  );
}
image-20220828154533980image-20220828154533980

2.3 函数式组件&&Fiber

代码语言:html复制
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(可传入)进来的

image-20220829013902960image-20220829013902960

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

2.3.2 memoizedProps

memoizedPropspendingProps的区别是什么呢?

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

8E0B48BD4AA1E478A961D2C5EC0ECDDB8E0B48BD4AA1E478A961D2C5EC0ECDDB

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

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

image-20220830163001997image-20220830163001997

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

image-20220830163509519image-20220830163509519

2.3.3 updateQueue

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

image-20220830163738294image-20220830163738294

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

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

2.3.4 memoizedState

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

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/664f7225128443b1a76f8cc007004e4b~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220830165109296" style="zoom:67%;" />

hook之间以.next相连形成单向链表。

而hook调用时产生的不管是effect(useEffect)还是state(useState),都是存储在hook.memoizedState,体现在Fiber节点上,其实是存储在hook.memoizedState.memoizedState,注意不要混淆。

2.3.5 dependencies

以下是调试代码

代码语言:jsx复制
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获取到的上下文

image-20220906231735709image-20220906231735709

从调试结果看,多个context也将通过.next相连,同时显然,这是一条单向链表

2.4 操作依据

代码语言:javascript复制
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。

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8606b792b3f6496cb02bb4e5da49c11b~tplv-k3u1fbpfcp-zoom-1.image" alt="3C717BA45856AD3B9EF1887255274A8C" style="zoom:60%;" />

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

2.4.1 &运算

温习一下&运算符的规则:只有1&1=1,其他情况为0

代码语言:javascript复制
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。

之所以说是是否包含,我们可以看看下边的代码。

代码语言:javascript复制
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

代码语言:javascript复制
const unknownFlags = Placement|Update; //此时=0b000000000000000000000110

这样的好处是快,判断是否包含的时候,直接使用& 运算符,在有限的操作依据面前,使用二进制完全可以兜住所有情况。

2.4.3 ~运算

~运算符会把每一位取反,即1->0,0->1

在React中,~运算符同样是常用操作

image-20220914115934463image-20220914115934463

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

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

  • 通过unknownFlags & Placement判断unknownFlags是否包含Placement
  • 通过unknownFlags |= PlacementPlacement合并进unknownFlags
  • 通过unknownFlags &= ~PlacementPlacementunknownFlags中删去

关于有哪些flags,我们可以翻阅到ReactFiberFlags.js,这里会有详细flags的记载 <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6a91d67204694391ab55199a14c31b50~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220914112358670" style="zoom:80%;" />

2.5 双缓存树的体现

我们曾说过,React的最基本工作原理<a href="https://juejin.cn/post/7120656364703055880#heading-6">双缓存树</a>,这引申出了我们需要知道这种机制在React中的实际体现。

这需要我们找到ReactFiber.old.js

代码语言:html复制
function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
	...
  this.alternate = null;
	...
}

由此我们知道,FIberNode上会有一个属性alternate,而这个属性正是我们期望的双缓存树中,里树与外树的双向指针。

正如图所见,在初次渲染中,current===null,所以目前仍是白屏,而workInProgress已经在构建

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d6e8e4e864ea41659a4289ed276bb7a8~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220828110638994" style="zoom:67%;" />

<p align="center">(图误,在renderWithHooks才对)</p>

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

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3cc2970b4cfa467f9d6178d1c16d7ee0~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220828110948356" style="zoom:67%;" />

2.6* 优先级相关

代码语言:javascript复制
function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  ...
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
	...
}

和lane有关的变量统一和调度优先级有关,暂时不涉及(因为还没看)

2.7* React devtools Profiler

代码语言:javascript复制
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在配合。

image-20220914141423794image-20220914141423794

3 好好认识hook结构

我们上边有讲到FiberNode.memoizedState,我们知道这里保存的是mountWorkInProgressHook时产生的hook对象

代码语言:javascript复制
{
  memoizedState: 0,
  baseState: 0,
  baseQueue: null,
  queue: ???,
  next:null
}

那么hook的各个项指什么?

3.1 baseState和memoizedState

其实很好理解,baseState对应上一次的state(effect),memoizedState为最新的state(effect),总之就是hook保存基本数据的地方。

04AC316ED382266CFE0B9C1F8B358DC604AC316ED382266CFE0B9C1F8B358DC6

3.2 queue

而hook.queue则是useState、useReducer的dispatcher存储的地方。

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

image-20220907155356838image-20220907155356838
代码语言:javascript复制
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触发===异步触发)。

image-20220907160421253image-20220907160421253

那这里可以看到,我们可以从lastRenderedReducer得到eagerState

代码语言:javascript复制
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为不可变数据,每次更新都需要更新一个新值才有效

代码语言:javascript复制
if (objectIs(eagerState, currentState)) {
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return;
}

3.2.2 dispatch

dispatch 属性存储状态变更函数,对应useState、useReducer 返回值中的第二项

代码语言:javascript复制
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的结构连接

代码语言:typescript复制
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的情况下,不做重渲染。如果状态更新前后没有变化,则可以略过剩下的步骤。

代码语言:javascript复制
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;
  }
}
image-20220909230608210image-20220909230608210

3.3 baseQueue

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

image-20220910231501779image-20220910231501779

<p align="center">(baseQueue被赋值queue.pending)</p>

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

4 React架构

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

4.1 React渲染关键节点

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

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

  • render阶段

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

12B98F3540B6E694265C5D47D49495F812B98F3540B6E694265C5D47D49495F8

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

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9d4b2e22341040cab96124220c9fd736~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220911124953226" style="zoom:67%;" />

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

image-20220911174952904image-20220911174952904

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

那么我们得到第一个关键节点: render阶段开始于renderRootConcurrentrenderRootSync

  • commit阶段

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

image-20220911173640119image-20220911173640119

上图是我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节点。

代码语言:javascript复制
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);
    }
  ...
}
代码语言:javascript复制
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;
}
image-20220911230326450image-20220911230326450

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

image-20220911225903147image-20220911225903147

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

4.2.2 调度同步/异步更新

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

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

这就要走到ensureRootIsScheduledensureRootIsScheduled会通过判断newCallbackPriority === SyncLane来确定走同步render还是异步render,这里涉及调度器,暂时不讲(还没看还不会)

代码语言:javascript复制
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))的过程。

CA3CC756047F3449F8F0A001D7583135CA3CC756047F3449F8F0A001D7583135

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

image-20220911234438727image-20220911234438727

<p align="center">(ensureRootIsScheduled是一个很重要的函数,会Scheduled一起讲会比较好)</p>

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

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/33cb60a8b34d4143ba3140377269f8ba~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220912000205282" style="zoom:67%;" />

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/928050001e3e43a5902d4a9e6818b4a6~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220911234507477" style="zoom:80%;" />

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会检查浏览器是否有剩余时间片。

代码语言:javascript复制
function workLoopConcurrent() {
  // 执行工作,直到调度程序要求我们让步
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopSync() {
  // 已经超时了,因此无需检查我们是否需要让步就可以执行工作
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

workLoop做了什么呢?这就要从performUnitOfWork(workInProgress)说起,下边的代码是精简逻辑 (只剩下beginWork这部分逻辑) 过后的performUnitOfWork函数,可以看到performUnitOfWork通过beginWork创建了一个新的节点赋给workInProgress

代码语言:javascript复制
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。

3EC7BC0E6EDF9F966E3F99EB3AEAE44A3EC7BC0E6EDF9F966E3F99EB3AEAE44A

我们提炼一下beginWork的核心逻辑,会发现beginWork通过current!==null来判断是否是第一次执行,这里的逻辑是如果是第一次执行,那么Fiber没有mount,自然为null。

代码语言:javascript复制
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(),
代码语言:javascript复制

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.

代码语言:txt复制
image-20220912142905277image-20220912142905277
  • workInProgress.type !== current.type,fiber.type是否变化
代码语言:javascript复制
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复用到更新也会进入这个逻辑

代码语言:javascript复制
  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);
      }
		...
  }
3FEEA7F7D362DFF7489B5CD9372940853FEEA7F7D362DFF7489B5CD937294085

根据我们在【useState】章节的收获,不管是update还是mount都要走到reconcileChildren

代码语言:javascript复制
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

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d3ac3755d26e4721b443fa19b7e939ef~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220912161901164" style="zoom:67%;" />

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

image-20220912163436219image-20220912163436219

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

image-20220912193434317image-20220912193434317

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

image-20220912193542891image-20220912193542891

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

#3 diff算法*

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

4.3.2 completeWork

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

代码语言:javascript复制
function performUnitOfWork(unitOfWork) {
  ...

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

  ...
}

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

image-20220912180238796image-20220912180238796

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

代码语言:javascript复制
function completeWork(current, workInProgress, renderLanes) {
  popTreeContext(workInProgress);

  switch (workInProgress.tag) {
    case FunctionComponent:
      ...
    case HostComponent:
      ...
    ...
  }
  bubbleProperties(workInProgress);
}

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

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cb788d1863c74e25860e088b314619a3~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220912192449157" style="zoom:67%;" />

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

FA2E2BD1166CC5583D24B03D6E0E6B0AFA2E2BD1166CC5583D24B03D6E0E6B0A

而关于subtreeFlags的具体用处,在commit阶段,我们后边说。

代码语言:javascript复制
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的逻辑,代表原生组件。

9790B760B1E00F83BD11B87BB75D9B7A9790B760B1E00F83BD11B87BB75D9B7A

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

代码语言:javascript复制
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

image-20220912202620837image-20220912202620837
image-20220912230012226image-20220912230012226

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

image-20220912230843810image-20220912230843810

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f73bbfcbeff046f3992c0306a571ee38~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220912231013886" style="zoom:67%;" />

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

image-20220912231244691image-20220912231244691

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

image-20220912231509268image-20220912231509268
#2 mount时

我们此时来看mount时的逻辑,这里最核心的逻辑简化后其实只有几句

代码语言:javascript复制
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节点不一定有真实节点,所以要找到可以插入的真实节点)

代码语言:javascript复制
  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

768151FDD996975166D8ED800FB15F44768151FDD996975166D8ED800FB15F44

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

4.4 commit阶段

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

BF43DF9506C66549A9DE61E7BFD390C2BF43DF9506C66549A9DE61E7BFD390C2

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

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

27C9BA14FEC45B6C24BF60C8F18C84B627C9BA14FEC45B6C24BF60C8F18C84B6

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

43885F5E1F8C7FF2B3392D297C85560943885F5E1F8C7FF2B3392D297C855609

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

image-20220913001550758image-20220913001550758
image-20220913001951456image-20220913001951456

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

代码语言:javascript复制
function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  renderPriorityLevel: EventPriority,
)
image-20220913103915403image-20220913103915403

我们认为commit阶段可以分为三个阶段,分别代表

  • before mutation,在执行DOM操作前的阶段
  • mutation,执行DOM操作
  • layout,执行DOM操作之后

当然,在这些流程之外,commit阶段还会处理useEffect这类需要在commit阶段执行的hook。

4.4.1 Before commit start

在commit开始之前,即before mutation之前的代码可以从下边看见,它们具体做了什么我直接在代码中注释了,请看注释。

代码语言:html复制
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的副作用触发了组件渲染,在这种情况下会再走一次状态更新流程(当然这期间有优化)

image-20220913163639067image-20220913163639067

4.4.2 BeforeMutation

commit阶段的正式开始,在于commitBeforeMutationEffects这个函数,可以看到当react确定subtreeFlags或者root.flags上可以找到BeforeMutationMask | MutationMask | LayoutMask | PassiveMask时,会触发commit的逻辑

代码语言:javascript复制
  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

244392FC225E2177F8435874B3A49BE3244392FC225E2177F8435874B3A49BE3

而commitBeforeMutationEffects_begin做的事情是从finishedWork向下遍历fiber树,一直到遍历到某个Fiber节点不再有BeforeMutationMask标记,此时会进入commitBeforeMutationEffects_complete

代码语言:javascript复制
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

代码语言:javascript复制
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)
image-20220913171907770image-20220913171907770
image-20220913171936689image-20220913171936689
# 小结

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

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

4.4.3 Mutation

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

代码语言:javascript复制
  if (subtreeHasEffects || rootHasEffect) {
    ...
    commitMutationEffects(root, finishedWork, lanes);
    ...
  } else {
    // No effects.
    root.current = finishedWork;
  }
image-20220913173111382image-20220913173111382

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

image-20220913173553696image-20220913173553696

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

代码语言:javascript复制
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);

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

839D576FEA3CCCABF59671E8FCB3ADBA839D576FEA3CCCABF59671E8FCB3ADBA

上边的部分负责从Fiber.deletions中取出具体的deletions执行commitDeletionEffects,后边则是向下遍历节点递归执行commitMutationEffectsOnFiber

代码语言:javascript复制
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);
}
image-20220913231000339image-20220913231000339

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

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

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0b6f5ebb64cd4f4f8ba410147803408a~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220913231805283" style="zoom:80%;" />

  • Update->Layout:执行useLayoutEffect上一次执行残留的destory函数
image-20220913232322735image-20220913232322735
  • Placement:
image-20220913233647464image-20220913233647464
  • Deletions:删除节点
  • Update,more
image-20220913235058171image-20220913235058171
  • Hydrating :SSR相关,由于博主目前为止没有实践过SSR,所以不说。
  • Ref:safelyDetachRef
  • ContentReset
  • Visibility

...

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

223E85C04FF58A4406FA7DB4DC511E8D223E85C04FF58A4406FA7DB4DC511E8D
#1 Update

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

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

image-20220913235933430image-20220913235933430

这里最核心的就是commitUpdate,React会通过updateProperties将DOM属性更新到真实节点上

代码语言:javascript复制
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);
}
image-20220914000716411image-20220914000716411
image-20220914000658640image-20220914000658640

<p align="center">(我们其实遇到过类似的函数⬆️)</p>

react还会把这个属性也更新上去,在我这篇文章中有这个属性的应用 <img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9669a0bf1afe4f0c949b8072a62c3e78~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220914000841062" width="75%" />

#2 Placement

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

image-20220914001322392image-20220914001322392

<p align="center">(如果其他类型的tag走到commitPlacement是会报错的)</p>

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

  • 获取Fiber节点存在HostFiber的父节点,并最终获得真实DOM
image-20220914001717436image-20220914001717436
image-20220914002256829image-20220914002256829
  • 获取Fiber节点的兄弟真实DOM节点
  • insertOrAppendPlacementNodeIntoContainer,将节点插入或添加到父容器中
image-20220914002533784image-20220914002533784
image-20220914002638999image-20220914002638999

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

2022-09-14 00.32.262022-09-14 00.32.26

<p align="center">(appendChildToContainer函数涉及真实DOM的插入/添加操作)</p>

#3 Deletion

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

  • 调用被删除节点的componentWillUnmount生命周期钩子,从页面移除Fiber节点对应DOM节点
image-20220914003826446image-20220914003826446
  • 安全解绑ref
image-20220914003755958image-20220914003755958

4.4.4 Layout

进入layout阶段,证明DOM节点已经渲染完毕了

代码语言:javascript复制
//将current指向已经完成的workInProgress
root.current = finishedWork;

commitLayoutEffects(finishedWork, root, lanes);
代码语言:javascript复制
function commitLayoutEffects(finishedWork, root, committedLanes) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  
  var current = finishedWork.alternate;
  commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
  
  inProgressLanes = null;
  inProgressRoot = null;
}

commitLayoutEffects->commitLayoutEffectOnFiber会按照我们熟悉的流程做递归

image-20220914135909948image-20220914135909948
image-20220914135920901image-20220914135920901

<p align="center">(commitLayoutEffectOnFiber和recursivelyTraverseLayoutEffects递归调用)</p>

我们需要关注的是commitLayoutEffectOnFiber中的内容

代码语言:javascript复制
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阶段结束后仍有一些收尾工作。

代码语言:javascript复制
  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阶段算已经完成了。

AF0F30822F8D36A8C83A07F6E8777722AF0F30822F8D36A8C83A07F6E8777722

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

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

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

E1D350692AF8C9B7A98A83277B0D87C3E1D350692AF8C9B7A98A83277B0D87C3

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

image-20220914021943510image-20220914021943510

<p align="center">(flushWork函数)</p>

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

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec94c627da6549d79615dffe4db6de53~tplv-k3u1fbpfcp-zoom-1.image" alt="image-20220914145845619" width="70%;" />

<p align="center">(部分项目成果集合图)</p>

https://juejin.cn/user/1645288319627576/posts 这是我的掘金个人主页,期待你的关注。

0 人点赞