# Render 阶段

# 流程概述

当组件更新,本质上是从 fiberRoot 开始深度调和 fiber 树。render 阶段的核心就是如何创建 Fiber Node 以及 构建 Fiber Tree。render 阶段开始于 performSyncWorkOnRootperformConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。这两个方法中会调用如下两个方法:

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

可以看到,他们唯一的区别是是否调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历。

  • workInProgress代表当前已创建的workInProgress fiber

  • performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树

Fiber Reconciler是从Stack Reconciler重构而来,通过遍历的方式实现可中断的递归,所以performUnitOfWork的工作可以分为两部分:“递”和“归”。

# “递”阶段

首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用 beginWork方法。该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。

当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。

# “归”阶段

在“归”阶段会调用 completeWork 方法 处理Fiber节点。当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。

“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。

  • 组件 A 触发 setState 或者 useState 更新视图,既然 fiber 是从 root 开始更新,那么如何找到对应的 A 并 rerender 的呢?
  • 组件类型 fiber 进行 beginWork 就一定会进行 render 吗?

# 最小更新单元 State

虽然在 React V18 引入订阅外部数据源的 useMutableSource。但在当前版本的 React 中,视图的更新基本都来源于内部 state 的改变。如果有一个组件 A ,如果想要它更新,那么场景有如下情况:

  • 组件本身改变 state 。函数 useState | useReducer ,类组件 setState | forceUpdate
  • props 改变,由组件更新带来的子组件的更新。
  • context更新,并且该组件消费了当前 context

无论是上面哪种方式,本质上都是 state 的变化。

  • props 改变来源于父级组件的 state 变化。
  • context 变化来源于 Provider 中 value 变化,而 value 一般情况下也是 state 或者是 state 衍生产物。

state 改变是在组件对应的 fiber 单位上的,之前的 fiber 章节讲到了在 React 的世界里会存在多种多样的 fiber 类型, 而开发者平时使用的组件 function Component 或者 Class Component 也是两种不同的 fiber 类型。而且 React 底层对它们的处理逻辑也不相同。

  • 比如更新类组件用的是 updateClassComponent,它做的事情是初始化时候实例化类组件,更新的话那么直接调用 render 得到新的 children
  • 更新函数组件用的是 updateFunctionComponent,里面调用 renderWithHooks 执行函数组件并依次调用 hooks

那么在整个 React 系统中,能够更新 state 的基本都在组件层面上,换句话说只有组件才能出发更新,比如 div 元素 hostComponent 类型的 fiber,它是无法独立的自我更新的,只能依赖于父类的组件更新 state ,但是在调和阶段,它也会作为一个任务单元进入到 workLoop 中 ;综上所述,可以这么理解

  • fiber是调和过程中的最小单元,每一个需要调和的 fiber 都会进入 workLoop 中。
  • 而组件是最小的更新单元,React 的更新源于数据层 state 的变化。

# 调度任务的初始化工作

以组件类型的 fiber 调和流程来理解组件更新流程。类组件在 render 阶段的一个重要作用就是产生新的 children ,也就是我们常说的 rerender。只有产生新的 children ,接下来才能深度遍历 children ,改变视图。每一个需要调和的 fiber 都要经历一个过程叫做 beginWork,在 beginWork 流程中将执行上述各种 fiber 的更新函数。

那么对于组件类型 fiber 说,进入到 workLoop 中,那么一定会 rerender 吗? 答案是否定的,解析来看几种情况。

主要看一下如下 demo :

/* 子组件2 */
function Child2(){
    return <div>子组件 2</div>
}
/* 子组件1 */
function Child1(){
    const [ num , setNumber ] = React.useState(0)
    return <div>
        子组件 {num}
        <button onClick={() => setNumber(num+1)} >按钮1</button>
     </div>
}
/* 父组件 */
export default function Index(){
    const [ num , setNumber ] = React.useState(0)
    return <div>
        <p>父组件 {num} </p>
        <Child1 />
        <Child2 />
        <button onClick={()=> setNumber(num+1)} >按钮2</button>
    </div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

场景一:如上 demo 中,当点击 Child1按钮1 的时候,Child1 会渲染,那么 Child1 自然会进入到 beginWork 流程中,那么疑问来了:

  • 问题一:父组件 Index 没有更新,会 rerender 吗?那么有会进入 beginWork 流程吗 ?
  • 问题二:Child2 会进入 beginWork流程吗 ?
  • 问题三:如果 IndexbeginWork,那么 React 从 Root fiber 开始调和的时候,是如何找到更新的事发点 Index 的呢?

场景二:在如上 demo 中,当点击 Index 中的 按钮2 的时候:

  • 问题四:Index 因为本身的 state 改变会更新,那么 Child1Child2 为什么会跟着更新。

接下来我们开始以一次更新开始,分析调和过程中 beginWork 流程。

在正式流程分析之前,先来看一下 v17 引出的新的概念,在 v16 版本,任务的优先级用 expirationTime 表示,在 v17 版本被 lane 取缔。

  • lane : 更新优先级。(在一次更新任务中,将赋予给更新的 fiber 的一个更新优先级 lane。)
  • childLaneschildren 中更新优先级。(如果当前 fiber 的 child 中有高优先级任务,那么当前 fiber 的 childLanes 等于当前优先级)。

# scheduleUpdateOnFiber

React 更新任务的起点 - scheduleUpdateOnFiber

类组件 setState 更新

// react-reconciler/src/ReactFiberClassComponent.new.js -> classComponentUpdater
enqueueSetState(inst, payload, callback){
     const fiber = getInstance(inst);       
     const lane = requestUpdateLane(fiber);
     scheduleUpdateOnFiber(fiber, lane, eventTime);
}
1
2
3
4
5
6

函数组件 useState 更新

// react-reconciler/src/ReactFiberHooks.new.js -> dispatchReducerAction
function dispatchReducerAction(fiber,queue,action){
    const lane = requestUpdateLane(fiber);
    scheduleUpdateOnFiber(fiber, lane, eventTime);
}
1
2
3
4
5

如上代码都是精简后,保留的最核心的流程。可以明确看到,无论是组件更新的本质就是:

  • 创建一个任务优先级 lane。
  • 然后进行 scheduleUpdateOnFiber。 那么这个 scheduleUpdateOnFiber 应该就是整个 React 更新任务的开始。

# scheduleUpdateOnFiber 开始更新 fiber

// react-reconciler/src/ReactFiberWorkLoop.new.js -> scheduleUpdateOnFiber
function scheduleUpdateOnFiber(fiber,lane){
    /* 递归向上标记更新优先级 */
    const root = markUpdateLaneFromFiberToRoot(fiber, lane);
    if(root === null) return null
    /* 如果当前 root 确定更新,那么会执行 ensureRootIsScheduled */
    ensureRootIsScheduled(root, eventTime);
}
1
2
3
4
5
6
7
8

scheduleUpdateOnFiber 主要做了两件事:

  • 第一个就是通过当前的更新优先级 lane ,把当前 fiber 到 rootFiber 的父级链表上的所有优先级都给更新了。
  • 如果当前 fiber 确定更新,那么会调用 ensureRootIsScheduled

# 优先级标记

那么 markUpdateLaneFromFiberToRoot 如何标记的优先级?

// react-reconciler/src/ReactFiberWorkLoop.new.js -> markUpdateLaneFromFiberToRoot
/**
 * @param {*} sourceFiber 发生 state 变化的fiber ,比如组件 A 触发了 useState ,那么组件 A 对应的 fiber 就是 sourceFiber
 * @param {*} lane        产生的更新优先级
 */
function markUpdateLaneFromFiberToRoot(sourceFiber,lane){
    /* 更新当前 fiber 上 */
    sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
    /* 更新缓存树上的 lanes */
    let alternate = sourceFiber.alternate;
    if (alternate !== null) alternate.lanes = mergeLanes(alternate.lanes, lane);
    /* 当前更新的 fiber */
    let node = sourceFiber;
    /* 找到返回父级 */
    let parent = sourceFiber.return;
    while(parent !== null){
        /* TODO: 更新 childLanes 字段 */
        parent.childLanes = mergeLanes(parent.childLanes, lane);
        if (alternate !== null) {  alternate.childLanes = mergeLanes(alternate.childLanes, lane); }
        /* 递归遍历更新 */
        node = parent;
        parent = parent.return;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

markUpdateLaneFromFiberToRoot 做的事很重要:

  • 首先会更新当前 fiber 上的更新优先级。fiber 架构采用双缓冲树,所有还要更新当前 fiber 的缓冲树 alternate 上的优先级。
  • 然后会递归向上把父级连上的 childLanes 都更新,更新成当前的任务优先级。

为什么向上递归更新父级的 childLanes ?

  • 所有的 fiber 是通过一颗 fiber 树关联到一起的,如果组件 A 发生一次更新,React 是从 root 开始深度遍历更新 fiber 树。
  • 那么更新过程中需要深度遍历整个 fiber 树吗?,当然也不是,那么只有一个组件更新,所有的 fiber 节点都调和无疑是性能上的浪费。
  • 既然要从头更新,又不想调和整个 fiber 树,那么如何找到更新的组件 A 呢?这个时候 childLanes 就派上用场了,如果 A 发生了更新,那么先向上递归更新父级链的 childLanes,接下来从 Root Fiber 向下调和的时候,发现 childLanes 等于当前更新优先级,那么说明它的 child 链上有新的更新任务,则会继续向下调和,反之退出调和流程。

Root Fiber 是通过 childLanes 逐渐向下调和找到需要更新的组件的。

render1

整个 fiber 树调和流程。

  • 第一阶段是发生更新,那么产生一个更新优先级 lane
  • 第二阶段向上标记 childLanes 过程。
  • 第三阶段是向下调和过程,有的同学会问,为什么 A 会被调和,原因是 A 和 B 是同级,如果父级元素调和,并且向下调和,那么父级的第一级子链上的 fiber 都会进入调和流程。从 fiber 关系上看,Root 先调和的是 child 指针上的 A ,然后 A 会退出向下调和,接下来才是 sibling B,接下来 B 会向下调和,通过 childLanes 找到当事人 F,然后 F 会触发 render 更新。这也就解决问题2,Child2 的调和问题。

通过上述我们知道了如何找到 F 并执行 render 的,那么还有一个问题,就是 B,E 会向下调和,如果它们是组件,那么会 render 么,答案是否定的,要记住的是调和过程并非 render 过程,调和过程有可能会触发 render 函数,也有可能只是继续向下调和,而本身不会执行 render 。

既然知道了如何去更新 childLanes ,以及更新 childLanes 的意义,我们接着向下分析流程。在 scheduleUpdateOnFiber 中,最后会调用 ensureRootIsScheduled ,那么它的作用又是什么呢?

ensureRootIsScheduled 的作用就是根据任务的类型,发起异步调度任务,在调度章节已经讲了调度流程。接下来会走调度的流程。

  • 对于 legacy sync 模式最后的更新任务是 performSyncWorkOnRoot
  • 对于 Concurrent 模式最后的更新任务是 performConcurrentWorkOnRoot

# 从 workLoop 到 beginWork

这里主要以 legacy 模式为主,performSyncWorkOnRoot :

// react-reconciler/src/ReactFiberWorkLoop.new.js -> performSyncWorkOnRoot
function performSyncWorkOnRoot(root) {
    /* render 阶段 */
    let exitStatus = renderRootSync(root, lanes);
    /* commit 阶段 */
    commitRoot(root);
    /* 如果有其他的等待中的任务,那么继续更新 */
    ensureRootIsScheduled(root, now());
}
1
2
3
4
5
6
7
8
9

调和的两大阶段 rendercommit 都在这个函数中执行。

  • renderRootSync 代表 render 阶段。
  • commitRoot 代表 commit 阶段。
  • 当 render 和 commit 阶段执行之后,如果有其他的等待中的任务,那么继续执行调度任务。

到此为止,一次更新调度任务的初始化工作完成。开始正式进入调和阶段。对前戏阶段做一下总结,流程图如下:

render2

# workLoop

renderRootSync 做了什么?

// react-reconciler/src/ReactFiberWorkLoop.new.js -> renderRootSync
function renderRootSync(root,lanes){
    workLoopSync();
    /* workLoop完毕后,证明所有节点都遍历完毕,那么重置状态,进入 commit 阶段 */
    workInProgressRoot = null;
    workInProgressRootRenderLanes = NoLanes;
}
1
2
3
4
5
6
7

renderRootSync 核心功能:

  • 执行 workLoopSync
  • workLoop 完毕后,证明所有节点都遍历完毕,那么重置状态,进入 commit 阶段。

workLoopSync 在整个 render 流程中充当的角色非常重要,可以把 workLoopSync 当作一个循环运作的加工器,每一个需要调和的 fiber 可以当作一个零件,每一个零件都需要进入加工器,如果没有待加工的零件,那么加工器才停止运转。下面就是加工器的具体实现。

// react-reconciler/src/ReactFiberWorkLoop.new.js -> workLoopSync
function workLoopSync() {
  /* 循环执行 performUnitOfWork ,一直到 workInProgress 为空 */
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
1
2
3
4
5
6
7
  • 如上只要 workInProgress 不为 null(还有需要调和的 fiber),那么 workLoopSync 会循环调用 performUnitOfWork

当 Concurrent 模式下会通过 shouldYield ,来判断有没有过期的任务,有过期任务,会中断 workLoop ,那么也就是说明了render阶段是可以被打断的。

while (workInProgress !== null && !shouldYield()) {
  performUnitOfWork(workInProgress);
}
1
2
3

# performUnitOfWork

回到 workLoopSync 流程上来,fiber 树是深度优先遍历得到的,在遍历完父节点,那么接下来就会遍历子节点。在这其中,每一个调和的 fiber 都将作为 workInProgress 进行调和更新。无论什么模式,workLoop 的执行单元都是 fiber 。而且更新单元的函数叫做 performUnitOfWork

// react-reconciler/src/ReactFiberWorkLoop.new.js -> performUnitOfWork
function performUnitOfWork(unitOfWork){
    const current = unitOfWork.alternate;
    let  next = beginWork(current, unitOfWork, subtreeRenderLanes);
    unitOfWork.memoizedProps = unitOfWork.pendingProps;
    if (next === null) {
       completeUnitOfWork(unitOfWork);
    } else {
      workInProgress = next;
    }
}
1
2
3
4
5
6
7
8
9
10
11

# beginWork

# beginWork 发生场景

假设有一个组件 fiber 链:root Fiber --child--> A组件 --child--> B组件 --child--> C组件

render3

以组件B 为参考,来看一下 React 如何调和的,那么一次更新就有可能有三种场景:

  1. 场景一:更新 A 组件,那么 A 触发更新,那么如果 B,C 没有做渲染控制处理(比如 memo PureComponent),那么更新会波动到 B , C,那么 A,B,C 都会 rerender。

render4

  1. 场景二:当更新 B 组件,那么组件 A fiber 会被标记,然后 A 会调和,但是不会 rerender;组件 B 是当事人,既会进入调和,也会 rerender;组件 C 受到父组件 B 的影响,会 rerender。

render5

  1. 场景三:当更新 C 件,那么 A,B 会进入调和流程,但是不会 rerender,C 是当事人,会调和并 rerender。

render6

如上的场景本质上都在 beginWork 中进行的,那么 beginWork 是如何处理这些逻辑的:

# beginWork 的两个阶段

// react-reconciler/src/ReactFiberBeginWork.new.js -> beginWork
/**
 * @param {*} current         current 树 fiber 
 * @param {*} workInProgress  workInProgress 树 fiber 
 * @param {*} renderLanes     当前的 render 优先级
 * @returns 
 */
function beginWork(current,workInProgress,renderLanes){
    /* -------------------第一部分-------------------- */
    // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
    // current!== null 来判断当前 fiber 是否创建过,如果第一次 mounted , 那么 current 为 null
    // 第一阶段主要针对更新的情况。如果初始化,那么直接跳过第一阶段,到第二阶段。
    if(current !== null){ 
        /* 更新流程 */
        /* current 树上上一次渲染后的 props */
        const oldProps = current.memoizedProps;
        /* workInProgress 树上这一次更新的 props  */
        const newProps = workInProgress.pendingProps;
        
        if(oldProps !== newProps ||  hasLegacyContextChanged()){
          didReceiveUpdate = true; // 当前更新是否来源于父级的更新 为 true 则来自父组件更新
        }else{
          /* props 和 context 没有发生变化,检查是否更新来自自身或者 context 改变 */
          const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes)
          if(!hasScheduledUpdateOrContext){
              didReceiveUpdate = false;
              return  attemptEarlyBailoutIfNoScheduledUpdate(current,workInProgress,renderLanes)
          }
          /* 这里省略了一些判断逻辑 */
          didReceiveUpdate = false;
        }

    }else{
      didReceiveUpdate = false
    }
    /* -------------------第二部分-------------------- */
    /* TODO: 走到这里流程会被调和 | 更新,比如函数执行会执行,类组件会执行 render 。 */
    // mount时:根据tag不同,创建不同的子Fiber节点
    switch(workInProgress.tag){
        /* 函数组件的情况 */
        case FunctionComponent: {
           return updateFunctionComponent( current, workInProgress, Component, resolvedProps, renderLanes )
        }
        /* 类组件的情况 */
        case ClassComponent:{
          return updateClassComponent(current,workInProgress,Component,resolvedProps,renderLanes)
        }
        /* 元素类型 fiber <div>, <span>  */
        case HostComponent:{
          return updateHostComponent(current, workInProgress, renderLanes)
        }
        /* 其他 fiber 情况 */ 
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

注意

除 rootFiber 以外, 组件 mount 时,由于是首次渲染,是不存在当前组件对应的 Fiber 节点在上一次更新时的 Fiber 节点,即 mount 时 current === null。组件 update时,由于之前已经 mount 过,所以 current !== null。所以我们可以通过current === null ?来区分组件是处于 mount 还是 update。

didReceiveUpdate

didReceiveUpdate :这个变量主要证明当前更新是否来源于父级的更新,那么自身并没有更新。比如更新 B 组件,那么 C 组件也会跟着更新,这个情况下 didReceiveUpdate = true。

基于此原因,beginWork的工作可以分为两部分:

  • update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child
  • mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点

# 第一阶段(update)

首先通过 current!== null 来判断当前 fiber 是否创建过,如果第一次 mounted , 那么 current 为 null,而第一阶段主要针对更新的情况。如果初始化,那么直接跳过第一阶段,**到第二阶段。**如果是更新流程。那么判断 oldProps === newProps(源码中还判断了老版本 context 是否变化),那么两者相等。一般会有以下几种情况:

情况一:还是回到上面场景上来,如果 C 组件更新,那么 B 组件被标记 ChildLanes 会进入到 beginWork 调和阶段,但是 B 组件本身 props 不会发生变化。

情况二:通过 useMemo 等方式缓存了 React element 元素,在渲染控制章节讲到过。

情况三:就是更新发生在当前组件本身,比如 B 组件发生更新,但是 B 组件的 props 并没有发生变化,所以也会走到这个流程上来。

反之如果两者不想等,证明父级 fiber 重新 rerender 导致了 props 改变,此时 didReceiveUpdate = true ,那么第一阶段完成,进入到第二阶段。

新老 props 相等的处理逻辑如下:

  1. checkScheduledUpdateOrContext
// react-reconciler/src/ReactFiberBeginWork.new.js -> checkScheduledUpdateOrContext
function checkScheduledUpdateOrContext(current,renderLanes){
    const updateLanes = current.lanes;
    /* 这种情况说明当前更新 */
    if (includesSomeLane(updateLanes, renderLanes)) {
      return true;
    }
     /* 如果该 fiber 消费了 context ,并且 context 发生了改变。 */
    if (enableLazyContextPropagation) {
      const dependencies = current.dependencies;
      if (dependencies !== null && checkIfContextChanged(dependencies)) {
        return true;
      }
    }
  return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

当新老 props 相等情况,首先会检查当前 fiber 的 lane 是否等于当前的更新优先级,如果相等,那么证明更新来源当前 fiber,比如 B 组件发生更新,那么会走这里(情况三)。当然期间也会判断是否有消费 context 并发生了变化。最后返回状态 hasScheduledUpdateOrContext 。

如果 hasScheduledUpdateOrContext 为 false,证明当前组件没有更新,也没有 context 上的变化,那么还有一种情况就是 child 可能有更新,但是当前 fiber 不需要更新(情况一)。那么会直接返回 attemptEarlyBailoutIfNoScheduledUpdate退出第二阶段

  1. attemptEarlyBailoutIfNoScheduledUpdate

attemptEarlyBailoutIfNoScheduledUpdate 这个函数会处理部分 Context 逻辑,但是最重要的是调用了 bailoutOnAlreadyFinishedWork

// react-reconciler/src/ReactFiberBeginWork.new.js -> bailoutOnAlreadyFinishedWork
function bailoutOnAlreadyFinishedWork(current,workInProgress,renderLanes){
     /* 如果 children 没有高优先级的任务,说明所有的 child 都没有更新,那么直接 返回,child 也不会被调和  */
    if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
      /* 这里做了流程简化 */
      return null 
    }
    /* 当前fiber没有更新。但是它的children 需要更新。  */
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
}
1
2
3
4
5
6
7
8
9
10
11

bailoutOnAlreadyFinishedWork 流程非常重要。它主要做了两件事:

  • 首先通过 includesSomeLane 判断 childLanes 是否是高优先级任务,如果不是,那么所有子孙 fiber 都不需要调和 ,那么直接返回 null,child 也不会被调和。
  • 如果 childLanes 优先级高,那么证明 child 需要被调和,但是当前组件不需要,所以会克隆一下 children,返回 children ,那么本身不会 rerender

# 第二阶段(mount)

第二阶段就是更新 fiber,比如是函数组件,就会调用 updateFunctionComponent,类组件就调用 updateClassComponent,然后进行 rerender 了。

对于我们常见的组件类型,如(FunctionComponent/ClassComponent/HostComponent),最终会进入reconcileChildren方法。

  • 对于mount的组件,他会创建新的子Fiber节点
  • 对于update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 对于mount的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 对于update的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

从代码可以看出,和beginWork一样,他也是通过current === null ?区分mountupdate。不论走哪个逻辑,最终他会生成新的子Fiber节点并赋值给workInProgress.child,作为本次beginWork 返回值,并作为下次performUnitOfWork执行时workInProgress的传参。

# 流程总结

接下来以上述中的组件B为例子,在强化一下更新流程。

场景一:当更新 A 时候,那么 A 组件的 fiber 会进入调和流程,会执行 render 形成新的组件 B 对应的 element 元素,接下来调和 B ,因为 B 的 newProps 不等于 oldProps,所以会 didReceiveUpdate = true ,然后更新组件,也会触发 render。(这里都是默认没有渲染控制的场景,比如 memo PureComponent 等 )。

render7

场景二:当更新 B 时候,那么 A 组件会标记 childLanes,所以 A 会被调和,但是不会 render,然后到了主角 B ,B 由于新老 props 相等,所以会 checkScheduledUpdateOrContext 流程,判断 lane 等于 renderLanes ,检查到 lane 等于 renderLane,所以会执行更新,触发 render。 C 组件也就跟着更新。

render8

场景三:当更新 C 时候,那么 A 和 B 组件会标记 childLanes,所以 A 和 B 会被调和,但是不会更新,然后到 C ,C 会走正常流程。

render9

场景四:还有一种情况,什么时候 B 会跳出调和流程呢。

render10

# beginWork 流程图

render11

组件更新和调和过程。rerender 一定会调和,但是调和并不一定 rerender,也有可能找到待更新的子元素。