React进阶笔记【9_ReactDOM.render渲染链路】

2022/4/6 ReactJS

# 逻辑分层

scheduleUpdateOnFiber 方法的作用是发起调度任务,再由 ReactDOM.render 触发当前场景下首屏渲染,也就是 performSyncWorkOnRoot 方法,它开启了 render 阶段,最后 commitRoot 方法则是真实 DOM 渲染过程,即 commit 阶段。

scheduleUpdateOnFibercommitRoot 为界,可以分为三个阶段:初始化阶段、render阶段、commit阶段。

# 初始化阶段

该阶段完成了 Fiber 树中基本实体的创建。

首先是 legacyRenderSubtreeIntoContainer 方法,在 ReactDOM.render 中这样调用了它:

return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
1

legacyRenderSubtreeIntoContainer 的关键逻辑如下:

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
  // container 对应的是我们传入的真实 DOM 对象
  var root = container._reactRootContainer;
  // 初始化 fiberRoot 对象
  var fiberRoot;

  // DOM 对象本身不存在 _reactRootContainer 属性,因此 root 为空
  if (!root) {
    // 若 root 为空,则初始化 _reactRootContainer,并将其值赋值给 root
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);

    // legacyCreateRootFromDOMContainer 创建出的对象会有一个 _internalRoot 属性,将其赋值给 fiberRoot
    fiberRoot = root._internalRoot;

    // 这里处理的是 ReactDOM.render 入参中的回调函数,了解即可
    if (typeof callback === 'function') {
      var originalCallback = callback;
      callback = function () {
        var instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }

    // 进入 unbatchedUpdates 方法
    unbatchedUpdates(function () {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });

  } else {
    // 逻辑处理的是非首次渲染的情况(即更新),其逻辑除了跳过了初始化工作,与楼上基本一致
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      var _originalCallback = callback;
      callback = function () {
        var instance = getPublicRootInstance(fiberRoot);
        _originalCallback.call(instance);
      };
    }
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}
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

在该过程中,有一个 fiberRoot 对象,下面列出来 rootfiberRoot

可以看出, root 对象(container._reactRootContainer)上有一个 _internalRoot 属性,它就是 fiberRootfiberRoot 的本质是一个 FiberRootNode 对象,其中包含一个 current 属性:

可以得出,current 对象是一个 FiberNode 实例,而 FiberNode 正是 Fiber 节点的对象类型。current 对象是一个 Fiber 节点,它还是 Fiber 树的头部节点。

fiberRoot 关联的是真实 DOM节点,而 current 关联的是虚拟 DOM 的根节点,这两个节点是后续整棵 Fiber 树构建的起点。

后续,fiberRoot 将和 ReactDOM.render 方法的其他参数,一起被传入 updateContainer 方法,从而形成一个回调。该回调就是 unbatchedUpdates 的入参:

// unbatchedUpdates
function unbatchedUpdates(fn, a) {
  // 这里是对上下文的处理,不必纠结
  var prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;
  
  try {
    // 重点在这里,直接调用了传入的回调函数 fn,对应当前链路中的 updateContainer 方法
    return fn(a);
  } finally {
    // finally 逻辑里是对回调队列的处理,此处不用太关注
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// updateContainer
function updateContainer(element, container, parentComponent, callback) {
  // ......
  // 这是一个 event 相关的入参,此处不必关注
  var eventTime = requestEventTime();
  // ......
  // 这是一个比较关键的入参,lane 表示优先级
  var lane = requestUpdateLane(current$1);

  // 结合 lane(优先级)信息,创建 update 对象,一个 update 对象意味着一个更新
  var update = createUpdate(eventTime, lane); 
  // update 的 payload 对应的是一个 React 元素
  update.payload = {
    element: element
  };
  // 处理 callback,这个 callback 其实就是我们调用 ReactDOM.render 时传入的 callback
  callback = callback === undefined ? null : callback;

  if (callback !== null) {
    if (typeof callback !== 'function') {
      error('render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback);
    }
    update.callback = callback;
  }
  
  // 将 update 入队
  enqueueUpdate(current$1, update);
  // 调度 fiberRoot 
  scheduleUpdateOnFiber(current$1, lane, eventTime);
  // 返回当前节点(fiberRoot)的优先级
  return lane;
}
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

updateContainer 大致做了三件事:

  1. 请求当前 Fiber 节点的 lane(优先级)
  2. 结合 lane(优先级),创建当前 Fiber 节点的 update 对象,并将其入队
  3. 调度当前节点 (rootFiber)

scheduleUpdateOnFiber 函数的任务是调度当前节点的更新,该函数会处理一系列与优先级、打断操作相关的逻辑。下一步会走到 performSyncWorkOnRoot 上,该方法的意思是 “执行根节点的同步任务”,也就是说即将走的是一个同步过程,所以在整个渲染链路过程中,Schedule(调度)没有存在感。

前面提到,performSyncWorkOnRoot 是开启 render 阶段的,render 阶段的任务就是完成 Fiber 树的构建,它是整个渲染链路最核心的一环。在异步渲染的模式下,render 阶段应该是一个可打断的异步过程。

到目前为止,ReactDOM.render 触发的首次渲染竟然是个同步过程!而 Fiber 的异步渲染并没有体现出来!

同步的ReactDOM.render,异步的 ReactDOM.createRoot

在 React 16 之后,React 有以下 3 种启动方式:

  • legacy 模式

    ReactDOM.render(<App />, rootNode),这是当前 React App 的方式(同步渲染链路)

  • blocking 模式

    ReactDOM.createBlockingRoot(rootNode).render(<App />),目前正在实验中,作为迁移到 concurrent 模式的第一个步骤

  • concurrent 模式

    ReactDOM.createRoot(rootNode).render(<App />),会作为 React 的默认开发模式(异步渲染)

legacyconcurrent 主要的区别在于 schedueUpdateOnFiber 的判断里:

if(lane === SyncLane){
  // ...
  performSyncWorkOnRoot(root)
}else{
  // ...
}
1
2
3
4
5
6

在异步渲染模式下,请求到的 lane 不再是 SyncLane(同步优先级),所以不会再走到 performSyncWorkOnRoot 里,而是会走到 else 里。其中 React 通过 fiber.mode 来获取当前是哪种渲染模式。

所以,不同的渲染模式的差异,其实是 mode 属性的差异,该属性决定了一个工作流是同步执行的,还是分片异步执行的。

Fiber 架构的设计确实是为了 Concurrent 而存在的,它在 React 中并不能和异步渲染画等号,它是一种同时兼容了同步渲染和异步渲染的设计。

# render 阶段

render 阶段是整个渲染链路最核心的一环,“找不同” 的过程就是在 render 阶段发生的。

React 15 下的调和过程是一个递归的过程,而 Fiber 架构下的调和过程,虽然没有依赖递归,但在 ReactDOM.render 触发的同步模式下,它仍然是一个深度优先搜索的过程。在这个过程中,beginWork 将创建新的 Fiber 节点,而 completeWork 负责将 Fiber 节点映射为 DOM 节点。

上面提到,performSyncWorkOnRoot 是 render 阶段的起点,而它的关键是调用了 renderRootSync 方法,紧随其后的是 prepareFreshStack,它的作用是重置一个新的堆栈环境,其中最为关键的是对 createWorkInProgress 的调用:

// 这里入参中的 current 传入的是现有数结构中的 rootFiber 对象
function createWorkInProgress(current, pendingProps){
  var workInProgress = current.alternate;
  
  // ReactDOM.render 触发的首屏渲染将进入这个逻辑
  if(workInProgress === null){
    // 这个点要注意,workInProgress 是 createFiber 方法的返回值
    workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
    workInProgress.ekementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
    
    // 这里也要注意,workInProgress 的 alternate 将指向 current
    workInProgress.alternate = current;
    
    // 还有这里,current 的 alternate 将反过来指向 workInProgress
    current.alternate = workInProgress;
  } else {
    // ..
  }
  
  // ...
  
  // 返回 workInProgress 节点
  return workInProgress;
}
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

current 就是下图:

简单整理一下:

  1. createWorkInProgress 将调用 createFiberworkInProgresscreateFiber 方法的返回值
  2. workInProgressalternate 将指向 current
  3. currentalternate 将反过来指向 workInProgress

下面看下 createFiber 是什么样的:

var createFiber = function(tag, pendingProps, key, mode){
  return new FiberNode(tag, pendingProps, key, mode);
}
1
2
3

可以看出来 createFiber 创建了一个 FiberNode 实例,而 FiberNode 就是 Fiber 节点的类型,所以,workInProgress 就是一个 Fiber 节点。再通过 workInProgress 的参数可以看出来,workInProgress 节点其实就是 current 节点的副本(即 rootFiber)。到此整棵树的结构如下图:

之后会进入 workLoopSync 的逻辑:

function workLoopSync(){
  // 若 workInProgress 不为空
  while(workInProgress !== null){
    // 针对它执行 performUnitOfWork 方法
    performUnitOfWork(workInProgress);
  }
}

// performUnitOfWork 将触发对 beginWork 的调用,进而实现对新 Fiber 节点的创建
1
2
3
4
5
6
7
8
9

通过循环调用 performUnitOfWork 来触发 beginWork,新的 Fiber 节点就会被不断地创建。当 workInProgress 为空时,说明就没有新的节点可以被创建了,也就是说完成了整颗 Fiber 树的创建。

到此为止,一棵 current 树,一棵 workInProgress 树,两棵树到底有什么区别?这个问题就需要去看 beginWorkcompleteWork 都做了什么。

# beginWork

function beginWork(current, workInProgress,renderLanes){
  // ...
  
  // current 节点不为空的情况下,会加一个判断,看看是否有更新逻辑要处理
  if(current !== null){
    // 获取新旧 props
    var oldProps = current.memorizedProps;
    var newProps = workInProgress.pendingProps;
    
    // 若 props 更新或者上下文改变,则认为需要 “接受更新”
    if(oldProps !== newProps || hasContextChanged() || workInProgress,type !== current.type){
      // 打个更新标记
      didReceiveUpdate = true;
    } else if(xxx){
      // 不需要更新的情况 A
      return A
    } else {
      if(需要更新的情况 B){
        didReceiveUpdate = true;
      } else {
        // 不需要更新的其他情况,首次渲染就会执行到这一行逻辑
        didReceiveUpdate = false;
      } 
    }
  } else {
    didReceiveUpdate = false;
  }
  
  // ...
  
  // 这里的 switch 是 beginWork 中的核心逻辑
  switch(workInProgress.tag){
  	// ...
      
    // 这里省略掉大量形如 “case: xxx” 的逻辑
      
    // 根节点将进入这个逻辑
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes)
    
    // DOM 标签对应的节点将进入这个逻辑
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes)
    
    // 文本节点将进入这个逻辑
    case HostText:
      return updateHostText(current, workInProgress)
    // ...
  }
}
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

上面的代码做个小总结:

  • beginWork 的入参是一对用 alternate 连接起来的 workInProgresscurrent 节点
  • beginWork 的核心逻辑是根据 fiber 节点(workInProgress) 的 tag 属性的不同调用不同的节点创建函数

switch 里的一堆 case ,它们都会通过调用 reconcileChildren 方法,来生成当前节点的子节点:

function reconcileChildren(current, workInProgress, nextChildren, renderLanes){
  // 判断 current 是否为 null
  if(current === null){
    // 若 current 为 null,则进入 mountChildFibers 的逻辑
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  } else {
    // 若 current 不为 null,则进入 reconcileChildFibers 的逻辑
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
  }
}
1
2
3
4
5
6
7
8
9
10

可以看出,reconcileChldren 也仅仅做了分发,在源码中,可以看到这两句:

var reconcileChildFibers = ChildReconciler(true);
var mountChildFibers = ChildReconciler(false);
1
2
function ChildReconciler(shouldTrackSideEffects){
  // 删除节点的逻辑
  function deleteChild(returnFiber, childToDelete){
    if(!shouldTrackSideEffects){
      // Noop.
      return;
    }
    
    // 以下执行删除逻辑
  }
  // ...
  
  // 单个节点的插入逻辑
  function placeSingleChild(newFiber){
    if(shouldTrackSideEffects && newFiber.alternate === null){
      newFiber.flags = Placement;
    }
    
    return newFiber;
  }
  
  // 插入节点的逻辑
  function placeChild(newFiber, lastPlacedIndex, newIndex){
    newFiber.index = newIndex;
    if(!shouldTrackSideEffects){
      // Noop.
      return lastPlacedIndex;
    }
    
    // 以下执行插入逻辑
  }
  // ...
  
  // 此处省略一系列 updateXXX 函数,它们用于处理 Fiber 节点的更新
  
  // 处理不止一个子节点的情况
  function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes){
    // ...
  }
  
  // 此处省略一系列 reconcileXXX 函数,它们负责处理具体的 reconcile 逻辑
  
  function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes){
    // 这是一个逻辑分发器,它读取入参后,会经过一系列的条件判断,调用上方所定义的负责具体节点操作的函数
  }
  
  // 将总的 reconcileChildFibers 函数返回
  return reconcileChildFibers;
}
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

上面的代码做个小总结:

  • reconcileChildFibersmountChildFibers 的不同,在于对副作用的处理不同。
  • ChildReconciler 中定义了大量如 placeXXX、deleteXXX、updateXXX、reconcileXXX 等函数,这些函数覆盖了对 Fiber 节点的创建、增加、删除、修改等动作,将直接或间接地被 reconcileChildFibers 所调用。
  • ChildReconciler 的返回值是一个名为 reconcileChildFibers 的函数,该函数是一个逻辑分发器,它将根据入参的不同,执行不同的 Fiber 节点操作,最终返回不同的目标 Fiber 节点。

以下是 beginWork 的逻辑图,completeWork 也类似:

至此, Fiber 节点的创建过程已经有了头绪,那么 Fiber 树的构建过程,可以从 workLoopSync 看:

function workLoopSync(){
  // 若 workInProgress 不为空
  while(workInProgress !== null){
    // 针对它执行 performUnitOfWork 方法
    performUnitOfWork(workInProgress);
  }
}

// 通过循环调用 performUnitOfWork 来触发 beginWork,新的 Fiber 节点就会被不断地创建。当 workInProgress 为空时,说明就没有新的节点可以被创建了,也就是说完成了整棵 Fiber 树的构建。
1
2
3
4
5
6
7
8
9

其中 performUnitOfWork 相关代码如下:

// 新建 Fiber 节点
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);

// 将新的 Fiber 节点赋值给 workInProgress
if(next === null){
  // if this doesn't spawn new work, complete the current work.
  completeUnitOfWork(unitOfWork);
} else {
  workInProgress = next;
}

// 确保 performUnitOfWork 执行完后,workInProgress 都保存着下一个要被处理的节点,从而为下一个 workLoopSync 循环做好准备
1
2
3
4
5
6
7
8
9
10
11
12

不同的 Fiber 之间,将通过 childreturnsibling 这三个属性建立关系,其中 childreturn 记录的是父子节点关系,sibling 记录的是兄弟节点的关系。

# completeWork

completeWork 将 Fiber 节点映射为 DOM 节点,下图是一个渲染时机:

performUnitOfWorkcompleteWork,会经过这样一个链路:

其中 completeUnitOfWork 可以当做是发起 completeWork 的工具人,completeUnitOfWork 是在 performUnitOfWork 中被调用的,那么 performUnitOfWork 是如何把握其调用时机的呢?

function performUnitOfWork(unitOfWork) {
  // ......
  // 获取入参节点对应的 current 节点
  var current = unitOfWork.alternate;
  var next;
  if (xxx) {
    // ...
    // 创建当前节点的子节点
    next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
    // ...
  } else {
    // 创建当前节点的子节点
    next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
  }
  // ......
  if (next === null) {
    // 调用 completeUnitOfWork
    completeUnitOfWork(unitOfWork);
  } else {
    // 将当前节点更新为新创建出的 Fiber 节点
    workInProgress = next;
  }
  // ......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

可以看出 performUnitOfWork 每次都会尝试调用 beginWork 来创建当前节点的子节点,若创建出的子节点为空(也就是说当前节点不存在子 Fiber 节点),则说明当前节点是一个叶子节点。按照深度优先遍历的原则,当遍历到叶子节点时,“递” 阶段就结束了,随之而来的是 “归” 的过程。在这种情况下,就会调用 completeUnitOfWork,执行当前节点对应的 completeWork 逻辑。

function completeWork(current, workInProgress, renderLanes) {
  // 取出 Fiber 节点的属性值,存储在 newProps 里
  var newProps = workInProgress.pendingProps;
  // 根据 workInProgress 节点的 tag 属性的不同,决定要进入哪段逻辑
  switch (workInProgress.tag) {
    case ......:
      return null;
    case ClassComponent:
      {
        .....
      }
    case HostRoot:
      {
        ......
      }
    // h1 节点的类型属于 HostComponent,因此这里为你讲解的是这段逻辑
    case HostComponent:
      {
        popHostContext(workInProgress);
        var rootContainerInstance = getRootHostContainer();
        var type = workInProgress.type;
        // 判断 current 节点是否存在,因为目前是挂载阶段,因此 current 节点是不存在的
        if (current !== null && workInProgress.stateNode != null) {
          updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
          if (current.ref !== workInProgress.ref) {
            markRef$1(workInProgress);
          }
        } else {
          // 这里首先是针对异常情况进行 return 处理
          if (!newProps) {
            if (!(workInProgress.stateNode !== null)) {
              {
                throw Error("We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue.");
              }
            } 
            return null;
          }
          // 接下来就为 DOM 节点的创建做准备了
          var currentHostContext = getHostContext();
          // _wasHydrated 是一个与服务端渲染有关的值,这里不用关注
          var _wasHydrated = popHydrationState(workInProgress);
          // 判断是否是服务端渲染
          if (_wasHydrated) {
            // 这里不用关注,请你关注 else 里面的逻辑
            if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {
              markUpdate(workInProgress);
            }
          } else {
            // 这一步很关键, createInstance 的作用是创建 DOM 节点
            var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
            // appendAllChildren 会尝试把上一步创建好的 DOM 节点挂载到 DOM 树上去
            appendAllChildren(instance, workInProgress, false, false);
            // stateNode 用于存储当前 Fiber 节点对应的 DOM 节点
            workInProgress.stateNode = instance; 
            // finalizeInitialChildren 用来为 DOM 节点设置属性
            if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
              markUpdate(workInProgress);
            }
          }
          ......
        }
        return null;
      }
    case HostText:
      {
        ......
      }
    case SuspenseComponent:
      {
        ......
      }
    case HostPortal:
      ......
      return null;
    case ContextProvider:
      ......
      return null;
    ......
  }
  {
    {
      throw Error("Unknown unit of work tag (" + workInProgress.tag + "). This error is likely caused by a bug in React. Please file an issue.");
    }
  }
}
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

completeWork 是一个巨大的 switch 语句,completeWork 将根据 workInProgress 节点的 tag 属性的不同,进入不同的 DOM 节点的创建、处理逻辑

有关 completeWork,可以做个小总结:

  1. completeWork 的工作内容:负责处理 Fiber 节点到 DOM 节点的映射逻辑。
  2. completeWork 内部有 3 个关键动作:
    1. 创建 DOM 节点(CreateInstance)
    2. 将 DOM 节点插入到 DOM 树中(AppendAllChildren)
    3. 为 DOM 节点设置属性(FinalizeInitialChildren)
  3. 创建好的 DOM 节点会被赋值给 workInProgress 节点的 stateNode 属性。也就是说想要定位一个 Fiber 对应的 DOM 节点时,访问它的 stateNode 属性就可以了。
  4. 将 DOM 节点插入到 DOM 树的操作是通过 appendAllChildren 函数来完成的。(实际上是将子 Fiber 节点所对应的 DOM 节点挂载到其父 Fiber 节点所对应的 DOM 节点里去。)

# completeUnitOfWork

completeUnitOfWork 的作用是发起一个循环,循环里做三件事:

  1. 针对传入的当前节点,调用 completeWork
  2. 将当前节点的副作用链(EffectList)插入到其父节点对应的副作用链(EffectList)中
  3. 以当前节点为起点,循环遍历其兄弟节点及其父节点。当遍历到兄弟节点时,将 return 掉当前调用,触发兄弟节点对应的 performUnitOfWork 逻辑;而遍历到父节点时,则会直接进入下一轮循环,也就是重复 1、2 的逻辑。

completeUnitOfWork 中处理兄弟节点和父节点的顺序是:先检查兄弟节点是否存在,若存在则优先处理兄弟节点;确认没有待处理的兄弟节点后,才转而处理父节点。这也就意味着,completeWork 的执行是严格自底向上的,子节点的 completeWork 总会先于父节点执行。

副作用链(EffectList)可以当做 render 阶段结果的集合:每个 Fiber 节点都维护着一个属于它自己的 effectList,effectList 在数据结构上以链表的形式存在,链表内的每一个元素都是一个 Fiber 节点。这些 Fiber 节点需要满足两个共性:

  1. 都是当前 Fiber 节点的后代节点
  2. 都有待处理的副作用

所以 Fiber 节点里的 effectList 里记录的是 其需要更新的后代节点

把所有需要更新的 Fiber 节点单独串成一串链表,方便后续有针对性地对它们进行更新,这就是所谓的 “收集副作用” 的过程

effectList 的创建逻辑如下,它维护着 firstEffect(第一个节点) 和 lastEffect(最后一个节点):

// 若副作用类型的值大于“PerformedWork”,则说明这里存在一个需要记录的副作用
if (flags > PerformedWork) {
  // returnFiber 是当前节点的父节点
  if (returnFiber.lastEffect !== null) {
    // 若父节点的 effectList 不为空,则将当前节点追加到 effectList 的末尾去
    returnFiber.lastEffect.nextEffect = completedWork;
  } else {
    // 若父节点的 effectList 为空,则当前节点就是 effectList 的 firstEffect
    returnFiber.firstEffect = completedWork;
  }

  // 将 effectList 的 lastEffect 指针后移一位
  returnFiber.lastEffect = completedWork;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

flags 是用来标识副作用类型的,而 completedWork 这个变量,在当前上下文中存储的就是 “正在被执行 completeWork 相关逻辑” 的节点;至于 PerformedWork,它是一个值为 1 的常量,React 规定若 flags(又名 effectTag)的值小于等于 1,则不必提交到 commit 阶段。因此 completeUnitOfWork 只会对 flags 大于 PerformedWork 的 effect fiber 进行收集。

所以,根节点(rootFiber) 上的 effectList 信息,是 commit 阶段的更新线索。

# commit 阶段

commit 阶段有三个小阶段:

  1. before mutation ,这个阶段 DOM 节点还没有被渲染到界面上去,过程中会触发 getSnapshotBeforeUpdate,也会处理 useEffect 钩子相关的调度逻辑。
  2. Mutation ,这个阶段负责 DOM 节点的渲染。在渲染过程中,会遍历 effectList,根据 flags(effectTag)的不同,执行不同的 DOM 操作。
  3. layout,这个阶段处理 DOM 渲染完毕之后的收尾逻辑。比如调用 componentDidMount / componentDidUpdate,调用 useLayoutEffect 钩子函数的回调等。除了这些之外,它还会 把 fiberRoot 的 current 指针指向 workInProgress Fiber 树