gpt4 book ai didi

重新捋一捋React源码之更新渲染流程

转载 作者:我是一只小鸟 更新时间:2023-01-03 06:31:13 25 4
gpt4 key购买 nike

前言

前些天在看Dan Abramov个人博客(推荐阅读,站在React开发者的角度去解读一些API的设计初衷和最佳实践)里的一篇 文章 ,其重点部分的思想就是即使不使用 Memo() ,也可以通过组合的方式来减少组不必要的渲染.

作者在放出代码讲述结论的时候并没有细说原理只是一笔带过,所以笔者自己想着从React内部的更新渲染机制去思考其原理时,发现并不顺畅,在一些关键位置的理解有些模棱两可,遂意识到自己对于React的理解并没有想象中的那么深。 因此打算重新捋一捋React源码中涉及更新渲染时的整个流程并记录下来.

背景

在进入正文前,需要先声明下该文章的读者需要了解过一些基本的React原理知识。例如虚拟DOM(Virtual Dom),Diff算法、fiber架构等此类概念。我们大致过一下:

Virtual Dom

React官方文档 :Virtual DOM 是一种编程概念。在这个概念里, UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。这一过程叫做协调.

Virtual Dom是一棵与DOM类似的树形结构。当我们创建或者更新组件时,通过协调(reconcile)过程构建新的Virtual Dom树,并与老的树比较计算出需要修改DOM的地方.

Fiber

自16版本之后,React引入了全新的fiber架构。React在构建的Virtual Dom树上的每个节点都被称为fiber节点。这种将fiber节点作为最小的工作单元的组织方式,为React未来版本的 可中断的异步更新 提供了最底层的支持。本文的源码解析基于17版本.

双缓冲fiber树

在React完成初次渲染之后会同时存在两棵fiber树。当前已经渲染到页面上的内容对应的fiber树称为 current fiber树 ,正在内存中构建的被称为 workInProgress fiber树 .

Diff算法就是发生在构建 workInProgress fiber树 的过程中,边构建边与 current fiber树 做比较并尽可能地复用节点。当一次更新渲染完成后,在根节点上直接通过将 current 指针指向 workInProgress fiber树 来执行两棵树的切换.

fiber树的遍历方式

考虑组织树形数据结构的方式,最常见的一种方式就是通过children字段:

                        
                          {
    "name": "A",
    "children": [
        { "name": "B" },
        {
            "name": "C",
            "children": [
                { "name": "D" }
            ]
        }
    ]
}

                        
                      

但React并不是这种方式,而是采用了通过指针来关联节点的方式:无论有多少个子节点,只存储一个 child 字段来指向第一个子节点;多个子节点中通过 sibling 指向下一个兄弟节点;所有的子节点都有 return 字段指向同一个父节点.

来个示例图:

这种方式非常符合Fiber架构的需求。方便以各种方式遍历整棵树,并且在调整树结构时,只需操作指针指向就可以了.

React对该树的遍历方式采取的是深度优先遍历。深度优先遍历有两个不同的阶段,分别是是“递”和“归”阶段;对应的 [源码] 简化后:

                        
                          function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;

  let next;
  next = beginWork(current, unitOfWork, subtreeRenderLanes);

  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

                        
                      

workInProgress 指针指向当前遍历到的节点。对于每个遍历到的fiber节点,会调用 beginWork 方法。该方法根据传入的fiber节点创建第一个子fiber节点,并将这两个节点通过child字段连接起来。这是“递”阶段.

当遍历到叶子节点 即没有子节点的组件时,就进入到“归”阶段:对该fiber节点执行 completeUnitOfWork 方法。执行完该节点的“归”阶段后,会检查其是否存在兄弟节点 即sibling字段。如果存在则进入到兄弟fiber节点的“递”阶段;如果没有兄弟节点了,则向上开始父fiber节点的“归”阶段.

“递”和“归”阶段不断交错执行直至“归”到根节点结束。示意如下:

状态更新的起点

前面说了那么多,终于要进入到正文啦.

触发React更新的方式有许多中:

  • this.setState
  • this.forceUpdate
  • useState
  • useReducer
  • ......

不管是何种方式更新状态,都会创建一个 用于保存更新状态相关信息的对象 ,称为 Update 。并将其插入到对应fiber节点的 updateQueue 上( enqueueUpdate(fiber, update) 函数中完成),并加入调度update( scheduleUpdateOnFiber(fiber, lane, eventTime) ).

我们以ClassComponent的 this.setState 为例,将其作为状态更新的起点.

在 [源码] 中看下 setState 方法的定义:

                        
                          Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
}

                        
                      

先是做对参数的检查,接着调用 updater 上的 enqueueSetState 。这里的 updater 并不是在react包中定义的,而是通过依赖注入的方式在组件的初始化构建时注入 [源码] 的。所以在ClassComponent的 constructor() 中调 this.setState 是非法的.

进入到 enqueueSetState 的方法 [源码] 中:

                        
                          enqueueSetState(inst, payload, callback) {
    // 根据当前的组件实例获取对应的fiber节点
    const fiber = getInstance(inst);
    
    // 优先级相关的数据
    const eventTime = requestEventTime();
    const lane = requestUpdateLane(fiber);
    
    // 创建update
    const update = createUpdate(eventTime, lane);
    update.payload = payload;
    // update.tag = ForceUpdate; 如果是通过this.forceUpdate()更新的话,这里还会加个标记为强制更新,防止组件在后续的优化手段中被跳过重渲染
    
    // 处理回调函数
    if (callback !== undefined && callback !== null) {
      update.callback = callback;
    }
    
    // 将update插入到updateQueue中
    enqueueUpdate(fiber, update);
    // 调度update
    scheduleUpdateOnFiber(fiber, lane, eventTime);
}

                        
                      

无论是以何种方式进行更新,最后都会统一进入到 scheduleUpdateOnFiber 函数 [源码] 中来 :

                        
                          function scheduleUpdateOnFiber(fiber, lane, eventTime) {
  // 检查是否陷入无限循环更新;例如在render()函数中调用setState()就会导致无限更新
  checkForNestedUpdates();
  
  // 自底向上收集fiber.childLanes
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    return null;
  }
    
  if (lane === SyncLane) {
      if(
          (executionContext & LegacyUnbatchedContext) !== NoContext &&	// 当前处于unbatched的上下文环境中;组件在mount和卸载时会拥有该上下文
          (executionContext & (RenderContext | CommitContext)) === NoContext	// 当前不是处于render或commit阶段
      ) {
          performSynWorkOnRoot(root);
      } else {
          ensureRootIsScheduled(root, eventTime);
          
          if(executionContext === NoContext) {
              flushSyncCallbackQueue();
          }
      }
  } else {
      ...
      ensureRootIsScheduled(root, eventTime);
  }
}

                        
                      

从上往下看:

  1. 因为该方法是所有状态更新都必经的入口,所以最先做的是检查死循环的可能.

  2. markUpdateLaneFromFiberToRoot 的作用是从当前需要更新的fiber节点开始,根据父节点指针不断向上遍历直至root节点,并将fiber.lane存入这一向上路径上所有祖先节点的childLanes中。childLanes字段在后续的render阶段遍历fiber树时用于判断子树中是否存在更新的依据.

  3. 接下来根据渲染方式分流 。

    • 如果是传统的同步渲染方式,则进入第一个if的逻辑中;老版本中最常见的 ReactDOM.render 就是该模式.

      • 判断代码注释中说明的条件,如果都满足的话(通常就是第一次渲染的时候)直接执行同步更新 performSynWorkOnRoot() ,开始render阶段.

      • 否则调用 ensureRootIsScheduled(root, eventTime) ,调度此次更新.

        在这之后会有一条 if(executionContext === NoContext) 判断当前是否没有任何的执行上下文,如果为 true 就表现此次更新并不是处在React的上下文中,则调用 flushSyncCallbackQueue() 立即同步执行此次更新。 这里涉及到一个常见的React问题:“ this.setState 什么时候同步更新,什么时候是异步(批量)更新”? 答:当处于React相关生命周期函数和事件处理回调中时,代码层面上看就是拥有执行上下文 executionContext ,这时候 this.setState 是批量更新的;而在脱离这些环境(如 fetch 网络请求返回或者 setTimeout 延迟执行)时, if(executionContext === NoContext) 成立,就会同步执行 this.setState 的更新.

    • 异步渲染的方式,也是调用 ensureRootIsScheduled(root, eventTime) .

ensureRootIsScheduled() 函数 [源码] 中涉及到对过期任务的立即同步执行,对旧任务的复用等逻辑;这一块不是关注的重点先忽略掉( scheduler 的调度的对象是任务(task),而不是指单个fiber节点的更新,这里要注意下。任务是指根据current fiber树构建新的fiber树并diff的整个过程 即整个render阶段。而 React 在 ensureRootIsScheduled() 函数里做的只是根据旧任务和此次更新的优先级来决定是否要复用旧任务 亦或生成新任务,剩下的就丢给 scheduler 去调度了。这一块的内容要拉出来细说的话得相当大的篇幅,之后再单独写一篇关于任务调度部分的文章 : ) 这里我们只要知道 scheduler 是通过 MessageChannel 来实现task(宏任务),以此达到异步调度执行任务的目的.

关注在流程上最核心的代码部分:

                        
                          function ensureRootIsScheduled(root, current) {
    ...
    if (existingCallbackNode !== null) {
        const existingCallbackPriority = root.callbackPriority;
        if (existingCallbackPriority === newCallbackPriority) {
          // 优先级不变,则直接复用已有任务
          return;
        }
        // 优先级改变了,取消已有任务,下面开始调度一个新任务
        cancelCallback(existingCallbackNode);
    }
    
    // 调度一个新任务
    let newCallbackNode;
    if (newCallbackPriority === SyncLanePriority) {
        newCallbackNode = scheduleSyncCallback(
          performSyncWorkOnRoot.bind(null, root),
        );
     } else {
        const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
          newCallbackPriority,
        );
        newCallbackNode = scheduleCallback(
          schedulerPriorityLevel,
          performConcurrentWorkOnRoot.bind(null, root),
        );
     }
    
     root.callbackPriority = newCallbackPriority;
     root.callbackNode = newCallbackNode;
}

                        
                      

其中 scheduleSyncCallback 和 scheduleCallback 是由 scheduler包 提供的方法,用于根据优先级调度传入的回调函数.

被调度的函数 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot ,取决于本次更新是同步更新还是异步更新;这两个函数即是render阶段的入口.

总结

梳理下从触发更新为起始到进入渲染阶段流程中的关键节点:

render阶段

在 performSyncWorkOnRoot / performConcurrentWorkOnRoot 里会调用 renderRootSync(root, lanes) / renderRootConcurrent(root, lanes) 和 commitRoot(root) ,这两者分别就是React更新中的render阶段和cmooit阶段了.

首先讲解的是render阶段的工作流程。进到 renderRootSync(root, lanes) / renderRootConcurrent(root, lanes) 中,看到关键函数 workLoopSync / workLoopConcurrent :

                        
                          function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

                        
                      

它们的区别在于是否有调用 shouldYield() 。 shouldYield() 用于判断当前浏览器帧的时间还足够,不够了就打断此次fiber树的构建,等到空闲时再次执行;这也是为什么引入异步更新后可能会导致组件会多次render的原因.

workInProgress 变量表示当前已创建的 workInProgress fiber 节点。 performUnitOfWork 会根据当前的 workInProgress fiber 节点以前文所说的深度遍历的方式决定下一个 fiber 节点是哪个并创建出来,然后将 workInProgress 与新创建的子 fiber 节点连接起来,再将新创建的节点赋值给 workInProgress .

在 while 循环中不断调用 performUnitOfWork(workInProgress) ,重复上述工作 就构造出了一棵完整的 fiber树 。现在看到 performUnitOfWork 中的代码 [源码] :

                        
                          function performUnitOfWork(unitOfWork: Fiber): void {
    const current = unitOfWork.alternate;
    
    let next;
    next = beginWork(current, unitOfWork, subtreeRenderLanes);	// “递”阶段
	...
    if(next === null) {
        completeUnitOfWork(unitOfWork);		// “归”阶段
    } else {
        workInProgress = next;
    }
}

                        
                      

前文说过, beginWork 和 completeUnitOfWork 分别对应的就是 fiber树 更新的“递”和“归”阶段.

beginWork

将 beginWork 简化处理后如下 [源码] :

                        
                          function beginWork(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes):Fiber | null {
    const updateLanes = workInProgress.lanes;
    
    if(current !== null) {
        const oldProps = current.memoizedProps;
	    const newProps = workInProgress.pendingProps;
        
        if(oldProps !== newProps || hasLegacyContextChanges()) {
            didReceiveUpdate = true;
        } else if(!includesSomeLane(renderLanes, updateLanes)) {
            // 当前更新优先级renderLanes不包括fiber.lanes
            didReceiveUpdate = false;
            ...
            return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
        } else {
            didReceiveUpdate = false;
        }
    } else {
        didReceiveUpdate = false;
    }
    
    switch(workInProgress.tag) {
		...
        case FunctionComponent:
            return updateFunctionComponent(...);
		case ClassComponent:
			return updateClassComponent(...);
        case MemoComponent:
            return updateMemoComponent(...);
        ...
    }
}

                        
                      

我们已经知道React会有两棵fiber树,那么除根节点 rootFiber 外,任何节点在初次渲染时其 current 都为空 即不存在 workInProgress.alternate 。因此可以根据 if(current !== null) 来区分当前是 mount 还会 update .

这里的 didReceiveUpdate 从变量名就能看出来,是用于标记该节点是否有接收到更新;在后续的流程代码中我们会看到需要使用该变量来判断优化逻辑的地方.

当 current 为空时很简单,将 didReceiveUpdate 置为false,就直接就进入最下面根据 tag 字段区分节点类型,创建并返回新的 子fiber节点 .

当 current !== null 即更新时,React会根据一定条件尽可能地去复用 current 节点:

  1. props 或者 context 有变动的;这种情况是无法复用 current 节点的;将 didReceiveUpdate 置为true。这里有两个需要注意的点:

    • 这里的 oldProps newProps 是由组件外部传入的所有属性创建而来的对象。即使我们在每次重渲染时向子组件传递的所有props属性字段值都是一样的,但在子组件对应的fiber中总是会new一个新对象再将属性存入其中。因此,这里的 oldProps !== newProps 总是为真。这也是为什么当父组件重新render时,即使传给子组件的props属性都不变也会导致子组件都重渲染。
    • 如果使用了一些优化手段,例如当前节点是 Memo 包裹的组件;那么在创建 Memo 类型的节点时会对 oldProps newProps 做一次浅比较。如果浅比较发现所有字段值都相同,则会将 didReceiveUpdate 置回false。
  2. 否则判断 当前fiber的更新优先级与此次fiber树的更新优先级判断,如果存在更新且更新优先级与fiber树优先级一致则 includesSomeLane(renderLanes, updateLanes) 会返回 true 。当前述两点不满足时(说明不存在更新或优先级不够),则进入该分支执行 bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) ,复用fiber节点(后续会细说这个方法).

  3. 前两个分支都无法满足,说明该fiber节点存在更新但无外部变化( props 或是 context 改变), didReceiveUpdate 置为 false .

在进一步讲解 beginWork 方法之前,先来看下一 bailoutOnAlreadyFinishedWork 的实现 [源码] :

                        
                          function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
      // ...
      
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        return null;
      } else {
        cloneChildFibers(current, workInProgress);
        return workInProgress.child;
      }
  }

                        
                      

bailoutOnAlreadyFinishedWork 中最主要的就是这两个判断分支。在前面讲解 scheduleUpdateOnFiber 方法中说到,执行 markUpdateLaneFromFiberToRoot(fiber, lane) 语句,对于产生更新的fiber节点,会将其更新优先级的信息 lane 变量自底向上,赋值给所有祖先节点的childLanes变量中。那么:

  • includesSomeLane(renderLanes, workInProgress.childLanes) 语句判断当前fiber节点的childLanes是否在本次更新的优先级信息renderLanes中;如果不是,则说明 workInProgress 的整棵子树中都不存在更新,所以直接返回 null ,上文说到的 performUnitOfWork 方法中,如果判断返回的变量是 null 则表示“递”阶段完成,开始此fiber节点的“归”阶段。再来回顾下 performUnitOfWork 的代码:

                                
                                  function performUnitOfWork(unitOfWork: Fiber): void {
        const current = unitOfWork.alternate;
        
        let next;
        next = beginWork(current, unitOfWork, subtreeRenderLanes);	// “递”阶段
    	...
        if(next === null) {
            completeUnitOfWork(unitOfWork);		// “归”阶段
        } else {
            workInProgress = next;
        }
    }
    
                                
                              
  • 否则的话说明子树中存在更新,需要继续往下“递”。但是当前的fiber节点是可以复用的,执行 cloneChildFibers(current, workInProgress) 克隆所有子节点;返回下一个需要更新的fiber节点 即第一个子节点.

beginWork 方法体的最下面就是根据当前fiber节点的类型调用各自的创建函数,并返回下一个要更新的fiber节点。在这一过程中,会执行各种优化措施和生命周期相关的钩子 例如:

  • 对于Class组件,如果有重写了 componentWillReceiveProps 方法的会在此时调用;有重写了 shouldComponentUpdate 方法的话,会将执行结果加入到是否要跳过更新的判断中去 [updateClassComponent -> updateClassInstance] 。如果最终判断可以跳过更新,也是进入到 bailoutOnAlreadyFinishedWork 函数 [updateClassComponent -> finishClassComponent] .

  • 如果是Function组件,进入到 updateFunctionComponent 函数 [源码] 中:

                                
                                  function updateFunctionComponent(
      current,
      workInProgress,
      Component,
      nextProps: any,
      renderLanes
    ) {
          nextChildren = renderWithHooks();	// 返回函数组件执行后return的JSX内容
          
          if (current !== null && !didReceiveUpdate) {
            bailoutHooks(current, workInProgress, renderLanes);	// 移除副作用和更新标记
            return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
          }
          // 创建并返回下一个fiber节点
          reconcileChildren(current, workInProgress, nextChildren, renderLanes);
    	  return workInProgress.child;
    }
    
                                
                              

    在这里我们又看到了 didReceiveUpdate 变量。在这一行代码中判断如果是更新行为( current !== null )且此前在 beginWork 中设置了 didReceiveUpdate 为 false 的话,则复用fiber节点;同样也是进到 bailoutOnAlreadyFinishedWork 方法.

  • 如果是Memo组件,则会对新旧 props 做浅比较,相等的话则会复用 [源码] :

                                
                                  const currentChild = ((current.child: any): Fiber);	// 取出唯一的一个child,即我们写在React.memo(...)中的组件
    if (!includesSomeLane(updateLanes, renderLanes)) {	// 当前fiber节点中是没有更新的
        const prevProps = currentChild.memoizedProps;
        
        let compare = Component.compare;
        compare = compare !== null ? compare : shallowEqual;
        if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {	// 浅比较新旧props
            return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
        }
    }
    
                                
                              
  • 其他组件等等...... 。

对于常见的组件类型 如 ClassComponent / HostComponent / FunctionComponent / ForwardRef 等,如果没有命中优化手段,最终都会进入都 reconcileChildren 中.

reconcileChildren 函数的内容不多,如下 [源码] :

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

                        
                      

和 beginWork 中一样,这里也是根据 current === null 来区分当前节点是第一次挂载还是更新的。 mountChildFibers / reconcileChildFibers 创建新的所有子fiber节点,并将第一个子节点赋值给 workInProgress.child 以此将两者相连起来.

mountChildFibers 和 reconcileChildFibers 方法的逻辑差不多一样。主要不同的地方在于 mount 时根据JSX(也就是上面的 nextChildren 参数)创建子fiber节点。而update时会将JSX与上次更新时的fiber节点做比较(这个过程就是“众所周知”的Diff算法),根据比较结果生成新的子fiber节点;此外还会在fiber节点上设置 flags ,即副作用标记。 flags 通过二进制位的方式存储了需要对fiber节点对应DOM节点执行的操作,例如这些 [源码] :

                        
                          // 插入DOM
export const Placement = /*                    */ 0b000000000000000010;
// 更新DOM
export const Update = /*                       */ 0b000000000000000100;
// 插入并更新DOM
export const PlacementAndUpdate = /*           */ 0b000000000000000110;
// 删除DOM
export const Deletion = /*                     */ 0b000000000000001000;

                        
                      

传入 reconcileChildFibers 方法的前三个参数,分别是 workInProgress fiber节点 、 current fiber节点 和我们写的组件中render返回的 JSX内容 。在这里需要清楚一些概念,就是在页面上的DOM节点,每次更新渲染时都有四种节点与之对应:

  1. 页面上的DOM节点
  2. 组件render返回的JSX内容
  3. current fiber节点
  4. workInProgress fiber节点

React的Diff操作要做的其实就是比较【2】和【3】,生成【4】.

进入到 reconcileChildFibers 方法,也就是Diff操作的入口 [源码] ,看下它的整体流程:

                        
                          function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
        // children是否为一个对象
        const isObject = typeof newChild === 'object' && newChild !== null;
        
        if(isObject) {
            // ...调用reconcileSingleElement()
        }
        
        // 为文本节点
        if (typeof newChild === 'string' || typeof newChild === 'number') {
            // ...调用reconcileSingleTextNode()
        }
        
        // 为数组
        if (isArray(newChild)) {
            // ...调用reconcileChildrenArray()
        }
        
        // 上述情况都未命中,则说明需要删除掉子节点
        return deleteRemainingChildren(returnFiber, currentFirstChild);
    }

                        
                      

整体思路就是根据children类型分为单节点和数组节点两种情况;最核心的就是根据节点的 key 和 type 判断尽可能地复用子节点,数组情况下会复杂一些因为涉及到了同级节点移动的情况。Diff算法的具体步骤源码在 调用reconcileSingleElement 和 reconcileChildrenArray 函数中,在这里就不再展开了,讲解React Diff算法的相关文章随便搜索下简直不要太多:).

一张流程图总结下 beginWork 的执行流程:

completeUnitOfWork

在上文的的 performUnitOfWork 函数中我们看到了,作为“递”阶段的 beginWork 每次执行都会返回next变量,作为下一个要处理的fiber节点。如果当前处理完的fiber节点是个叶子节点,其没有子节点了,那么返回的next则为空:

                        
                          if(next === null) {
	completeUnitOfWork(unitOfWork);	// 内部会调用completeWork方法
}

                        
                      

于是就该执行该fiber节点的“归”阶段了,即 completeUnitOfWork 函数 [源码] :

                        
                          function completeUnitOfWork(unitOfWork: Fiber): void {
	let completedWork = unitOfWork;
    do {
        const current = completedWork.alternate;
        const returnFiber = completedWork.return;

        let next;
        next = completeWork(current, completedWork, subtreeRenderLanes);
		
		if (next !== null) {	// next不为空的特殊情况
            workInProgress = next;
	        return;
        }

		/* effectList相关的操作,放到最后讲 */
		if (returnFiber !== null && (returnFiber.flags & Incomplete) === NoFlags) {
            // 将当前fiber节点的effectList拼接进父节点的effectList中
            if (returnFiber.firstEffect === null) {
              returnFiber.firstEffect = completedWork.firstEffect;
            }
            if (completedWork.lastEffect !== null) {
              if (returnFiber.lastEffect !== null) {
                returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
              }
              returnFiber.lastEffect = completedWork.lastEffect;
            }
            
            // 如果当前fiber节点有副作用,将当前节点拼接进父节点的effectList中
            const flags = completedWork.flags;
            if (flags > PerformedWork) {
              if (returnFiber.lastEffect !== null) {
                returnFiber.lastEffect.nextEffect = completedWork;
              } else {
                returnFiber.firstEffect = completedWork;
              }
              returnFiber.lastEffect = completedWork;
            }
        }
		/* --- */

		const siblingFiber = completedWork.sibling;	// 下一个兄弟节点
        if (siblingFiber !== null) {
          workInProgress = siblingFiber;
          return;
        }
        completedWork = returnFiber;	// 如果没有兄弟节点,则“归”到父节点
        workInProgress = completedWork;
    } while (completedWork !== null);
}

                        
                      

对fiber节点“归”阶段要执行的主要工作是在 completeWork 方法中实现的。 completeUnitOfWork 这里只是负责对fiber节点调用 completeWork 方法并返回 next 变量.

绝大部分情况下 next 都为 null 。因为根据深度优先遍历的规则,当前节点遍历完了说明其子树中的节点一定也都处理完了,下一步应该是开始下一个兄弟节点的“递”阶段:

                        
                          const siblingFiber = completedWork.sibling;	// 下一个兄弟节点
if (siblingFiber !== null) {
    workInProgress = siblingFiber;
    return;
}

                        
                      

如果没有兄弟节点了,按照规则应该是返回上一层的父节点并执行它的“归”操作。这里将父节点赋值给 completedWork 变量: completedWork = returnFiber ,并在下一轮 do while 循环中开始这个父节点的“归”操作.

但凡事都有意外。如果返回的 next 变量不为 null :

                        
                          if (next !== null) {	// next不为空的特殊情况
    workInProgress = next;
    return;
}

                        
                      

将 next 变量赋值给 workInProgress ,开始进入到 next 表示节点的“递”阶段。这里的特殊情况就是指 Suspense 组件懒加载时的场景(在下一节的 completeWork 方法中笔者会标记出其位置)。 Suspense 组件完成了“归”阶段的工作,但由于组件未加载完成因此需要重渲染 Suspense.fallback 的内容,所以要重新进入到节点的“递”阶段.

completeWork

让我们进入到 completeWork 的实现中来 [源码] :

                        
                          function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
      switch (workInProgress.tag) {
          case LazyComponent:
          case SimpleMemoComponent:
          //...
          case FunctionComponent:
          case MemoComponent:
          case ClassComponent:
            return null;

          case HostComponent: {
              // ...此处先省略
          }

          case SuspenseComponent: {
              if ((workInProgress.flags & DidCapture) !== NoFlags) {	// Suspense下的组件未加载时会抛出异常并被捕获
                  workInProgress.lanes = renderLanes;
                  return workInProgress;
              }
              // ...
              return null
          }
          // ...
      }
  }

                        
                      

对于常见的组件类型FunctionComponent、MemoComponent和ClassComponent等并没有什么特别的操作,都是直接返回 null .

对于SuspenseComponent类型的组件,如果 (workInProgress.flags & DidCapture) !== NoFlags 成立,说明懒加载未完成,返回当前的fiber节点;否则返回 null 。这里便是与上节 completeUnitOfWork 方法中 next 变量不为空的特殊情况相照应的地方.

重点关注HostComponent类型(原生DOM对应的fiber节点类型)的处理:

                        
                          case HostComponent: {
    const rootContainerInstance = getRootHostContainer();
    const type = workInProgress.type;
    
    if (current !== null && workInProgress.stateNode != null) {	// update时的情况
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );
    } else {													// mount时的情况
        const currentHostContext = getHostContext();
        // 创建DOM节点
        const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
        );
        // 将子孙DOM节点插入到新的DOM节点下
        appendAllChildren(instance, workInProgress, false, false);

        workInProgress.stateNode = instance;
 
        // 设置DOM节点上的属性
        if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
         ) {
            markUpdate(workInProgress);
        }
    }
}

                        
                      

在这里区分mount还是update时的条件和前面的不一样了,不再只是判断 current === null ? 了,还多了一条 workInProgress.stateNode != null 。因为我们当前处理的是HostComponent类型,因此判断是否为更新除了要有fiber节点( current ),还得有对应的DOM节点( workInProgress.stateNode ).

对照上述的代码捋下mount和update做的工作.

mount:

  1. 创建fiber节点对应的DOM节点.

  2. 将子孙DOM节点插入到刚刚新创建的DOM节点中。我们知道作为“归”阶段的 complete 是自底向上完成的,所以在处理当前fiber节点 workInProgress 时, workInProgress 的所有子fiber节点所对应的DOM节点一定是都已经创建好了的.

  3. 创建好的DOM节点赋值给 workInProgress.stateNode .

  4. 执行 finalizeInitialChildren 函数,最终进入到 setInitialDOMProperties 中(路径 [finalizeInitialChildren -> setInitialProperties -> setInitialDOMProperties] )。该函数将节点上的属性 prop 设置到DOM上,如 style 样式属性、 children 属性描述的内部文本/数值内容、各种自定义属性以及注册事件处理等.

update:

update时的工作相对就简单多了,就是执行 updateHostComponent [源码] 方法:

                        
                          updateHostComponent = function(
	current: Fiber,
    workInProgress: Fiber,
    type: Type,
    newProps: Props,
    rootContainerInstance: Container
) {
    const oldProps = current.memoizedProps;
    if (oldProps === newProps) {
      return;
    }
    
    const instance: Instance = workInProgress.stateNode;
    const currentHostContext = getHostContext();
    const updatePayload = prepareUpdate(
      instance,
      type,
      oldProps,
      newProps,
      rootContainerInstance,
      currentHostContext,
    );

    workInProgress.updateQueue = (updatePayload: any);
}

                        
                      

与mount时的第4步类似,也是负责处理DOM节点上的属性。但不同的地方在于, updateHostComponent 中只是做了事件监听的注册和属性的预处理: const updatePayload = prepareUpdate(...) ;然后将需要更新的 props 内容赋值到fiber节点的 updateQueue 上: workInProgress.updateQueue = updatePayload 。最终在React渲染的 commit阶段 才会将 prop 更新真正应用到DOM节点上.

effectList

在 completeUnitOfWork 函数体的中间有一大段关于 effectList 的操作。此处做的操作就是将当前fiber节点以及子树中有更改的节点(即 effectList )“拼接”进父节点的 effectList .

我们从前文的 beginWork 执行过程中知道,对整棵fiber树进行完“递”阶段后,所有存在变更的fiber都被标记上 flags 表明是何种变更。在之后的 commit 阶段 React 需要对所有存在变更的fiber节点执行对应的DOM操作的话,如果再完全遍历一次fiber树的话是不太可取的.

因此 React 做法是将所有存在 flags 的fiber节点都存放在单向链表结构的 effectList 中。对照代码,来看下实际的实现:

  • 当前fiber节点有两个变量, firstEffect 指向链表的第一个元素, lastEffect 指向链表的最后一个元素。这条链表上包含了 子树中所有标记了 flags 的子孙节点 。这一条链表就是 effectList .

  • 在 completeUnitOfWork 函数中处理当前fiber节点,通过操作父节点的 firstEffect 和 lastEffect 变量,将当前fiber节点的 effectList 拼接进父节点的 effectList 。示意图如下:

  • 如果当前fiber节点也存在变更,则还需要将当前节点放到父节点 effectList 的末尾:

  • 如果对整棵fiber树自底向上执行完 completeUnitOfWork 后,root节点的 effectList 就是一条拥有整棵fiber树上所有带有变更的fiber节点所组成的链表了。在之后的commit阶段需要将所有fiber节点的变更应用到页面DOM上时,只需要遍历root节点的 effectList 即可.

在后续的commit阶段有许多操作是需要遍历 effectList 。我们由上述生成 effectList 的过程可以知道,遍历 effectList 中fiber节点的顺序 对应fiber树中的结构是自底向上的,从子孙节点到父节点,再到祖先节点直至root根节点。在后面的解析内容中会有地方再次提到该知识点.

render阶段完成

至此,对于整棵fiber树的递( beginWork )和归( completeUnitOfWork )操作都已完成。现在把目光再拨回到fiber树更新的起点, performSyncWorkOnRoot / performConcurrentWorkOnRoot 函数中:在执行完render阶段的入口 renderRootSync / renderRootConcurrent 后,如果render阶段正常完成,最终它们都会调用 commitRoot(root) ,开启commit阶段.

commit阶段

看进 commitRoot 的实现 [commitRoot -> commitRootImpl] 中,函数体非常的长。在React源码的注释中,是将commit阶段划分为以下三个阶段并命名的:

  • before mutation阶段(执行DOM操作之前)
  • mutation阶段(在此阶段执行DOM操作)
  • layout阶段(执行DOM操作之后)

笔者也按这三个阶段来分别讲解,并尽可能地简化代码,只保留比较重要的代码部分,标记出其所在 commitRootImpl 方法中对应哪些行数.

在before mutation之前

在正式开始commit阶段之前,还有一些额外的工作要做。主要是清理上一次渲染产生的回调任务以及获取完整的 effectList [commitRootImpl中1889-1988行] :

                        
                          do {
    flushPassiveEffects();	// 触发useEffect回调和其他同步任务;由于这些任务中可能再触发新的重渲染,因此需要在while循环中执行至没有任务为止
} while (rootWithPendingPassiveEffects !== null);

const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;

// 获取所有存在更改的fiber节点 - effectList
let firstEffect;
if (finishedWork.flags > PerformedWork) {
    if (finishedWork.lastEffect !== null) {
        // effectList的链表操作,将有flags的根节点加入到末尾
        finishedWork.lastEffect.nextEffect = finishedWork;
        firstEffect = finishedWork.firstEffect;
    } else {
        firstEffect = finishedWork;
    }
} else {	// 根节点没有副作用
    firstEffect = finishedWork.firstEffect;
}

if (firstEffect !== null) {
    /*
    	...
    	此处有三个while循环分别对应三个阶段的工作
    	...
    */
}

                        
                      

最开始的代码是在 while 循环中重复执行 flushPassiveEffects 方法,直至effect任务队列为空。之所以要这样设计,是因为会有可能两次更新渲染是同步执行的。例如 第一次更新渲染后会产生各个组件中的 useEffect 回调任务,这些任务将会作为”宏任务“(React内部是通过 MessageChannel [MDN] 实现的)放在下一轮事件循环中执行。 接着同步执行第二次更新渲染,这个时机是在第一次更新产生的那些“宏任务”执行之前的,所以需要在commit阶段开始前就把上一轮更新产生的副作用先给执行完了.

这个设计对深入理解 useEffect 的执行时机非常重要,来看下这段代码:

                        
                          let counter = 0

function App(props: any) {
  const [name, setName] = useState('')

  useEffect(() => {
    console.log(counter)
  })

  const click = () => {
    Promise.resolve().then(() => {
      ++counter
      setName('one')
    })

    Promise.resolve().then(() => {
      ++counter
      setName('two')
    })
  }

  return <div onClick={click}>{name}</div>
}

                        
                      

触发点击事件后,执行的两个微任务中都有更新操作。 useEffect 的回调方法中会打印 counter 变量;最终的输出结果是: 2, 2 .

  1. 在第一个微任务中的更新完成后, counter 为1,并产生一个 useEffect 的回调在任务队列中.

  2. 接着开始第二个微任务( useEffect 回调是宏任务,所以肯定在它之前), counter 为2。在完成第二次更新的 render 阶段但在开始 commit 阶段之前,会进入前面说的 flushPassiveEffects()循环 部分.

  3. 在 flushPassiveEffects 方法中提前执行了第1步中产生的 useEffect 回调,打印 counter: 2 。

  4. 第二次更新产生的 useEffect 回调放入任务队列,在下一次事件循环的宏任务中执行打印 counter: 2 。

before mutation阶段 [commitRootImpl中1990-2033行]

                        
                          if (firstEffect !== null) {
    // 给执行上下文加上CommitContext,标记当前处于commit阶段
    const prevExecutionContext = executionContext;
    executionContext |= CommitContext;
    
    nextEffect = firstEffect;
    do {
        try {
            commitBeforeMutationEffects();	// before mutation阶段做的工作都在该函数中
        } catch (error) {
            // ...处理异常...
            nextEffect = nextEffect.nextEffect;
        }
    } while (nextEffect !== null);
    
    
    // ...mutation阶段
    
    // ... layout阶段
}

                        
                      

before mutation阶段的代码很短,就是在 while 循环中执行 commitBeforeMutationEffects 方法。正常情况下 commitBeforeMutationEffects 只需要执行一次就可以了,这里将其放在 while 循环中是为了在执行过程中发生异常时可以在 try catch 块中捕获到,并执行 nextEffect = nextEffect.nextEffect 语句,跳过 effectList 中发生异常的这个节点到下一个,然后再次执行 commitBeforeMutationEffects 方法.

进入到 commitBeforeMutationEffects 函数中的实现 [源码] :

                        
                          function commitBeforeMutationEffects() {
    while (nextEffect !== null) {
        const current = nextEffect.alternate;
        
        if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
            // 处理DOM的focus、blur相关的操作
        }
        
        const flags = nextEffect.flags;
        // 该fiber节点需要调用getSnapshotBeforeUpdate生命周期钩子
        if ((flags & Snapshot) !== NoFlags) {
          // 该方法内会调用ClassComponent实例的getSnapshotBeforeUpdate方法
          commitBeforeMutationEffectOnFiber(current, nextEffect);
        }
        
        /* 存在“passive effects”,翻译过来就是“被动”的副作用,意指那些通过监听状态(依赖数组)变化产生的作用,如最常见的useEffect */
        if ((flags & Passive) !== NoFlags) {
          if (!rootDoesHavePassiveEffects) {
            // 下面flushPassiveEffects会调用所有的useEffect回调,因此只要执行一次调度即可。用该变量标记是否调度过
            rootDoesHavePassiveEffects = true;
            
            // 调度flushPassiveEffects
            scheduleCallback(NormalSchedulerPriority, () => {
              flushPassiveEffects();
              return null;
            });
          }
        }
        nextEffect = nextEffect.nextEffect;
    }
}

                        
                      

commitBeforeMutationEffects 函数会遍历 effectList ,对每个fiber节点按顺序下来做了这么三件事:

  1. 处理页面DOM节点在更新渲染或删除后的聚焦(focus)/失焦(blur)状态.

  2. 从 React 16版本之后,由于 componentWillXXX 系列的生命周期钩子会在更新渲染中触发多次,这对于开发来说不安全,因此提供了一个新的生命周期钩子: getSnapshotBeforeUpdate 。类组件 ClassComponent s发生更新后,会在完成render阶段后但在commit阶段执行DOM操作之前调用这个生命周期钩子.

  3. 调度 flushPassiveEffects 方法,该方法使得在浏览器完成绘制后(layout阶段之后)再调用 useEffect 回调.

    • flushPassiveEffects [源码] 的实现中,会先执行完 所有的 上一次更新渲染中 useEffect 返回的销毁函数后,再开始执行 所有的 本次更新后产生的 useEffect 回调函数

mutation阶段

对于页面上DOM的实际操作都是发生在mutation阶段.

mutation阶段的外层代码 [commitRootImpl中2045-2070行] 和before mutation阶段类似:

                        
                          nextEffect = firstEffect;
do {
    try {
        commitMutationEffects(root, renderPriorityLevel);	// mutation阶段做的工作都在该函数中
    } catch (error) {
        // ...处理异常...
        nextEffect = nextEffect.nextEffect;
    }
} while (nextEffect !== null);

                        
                      

进入到 commitMutationEffects 函数中的实现 [源码] :

                        
                          function commitMutationEffects(root: FiberRoot, renderPriorityLevel: ReactPriorityLevel) {
    while (nextEffect !== null) {
        const flags = nextEffect.flags;
        
        // 重置对应DOM节点的文本内容
        if (flags & ContentReset) {
            commitResetTextContent(nextEffect);
        }
        
        // 处理ref
        if (flags & Ref) {
            const current = nextEffect.alternate;
            if (current !== null) {
                commitDetachRef(current);
            }
        }
        
        // 根据flags类型分别做处理
        const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
        switch (primaryFlags) {
            case Placement: {
                commitPlacement(nextEffect);
                nextEffect.flags &= ~Placement;
                break;
            }
            case PlacementAndUpdate: {
                commitPlacement(nextEffect);
                nextEffect.flags &= ~Placement;
                
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
            }
            /* 服务端渲染相关
            case Hydrating: {...}
            case HydratingAndUpdate: {...}
            */
            case Update: {
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
            }
            case Deletion: {
                commitDeletion(root, nextEffect, renderPriorityLevel);
                break;
            }
        }
        
        nextEffect = nextEffect.nextEffect;
    }
}

                        
                      

commitMutationEffects 函数遍历 effectList ,对每个fiber节点按顺序下来做以下三件事情:

  1. fiber节点存在有 ContentReset 标记的话,需要将对应DOM节点中的文本置空。发生这种情况的判断逻辑在 beginWork 阶段对HostComponent类型的处理函数 updateHostComponent 中.

  2. 如果此次更新渲染需要挂载ref且上一次渲染时也挂载了ref,则需要执行 commitDetachRef 。该函数做的事情就是先将上一次渲染时挂载的ref( current.ref.current )置为null。至于此次渲染需要挂载的ref,是在后续的 layout阶段 完成的.

  3. 根据 flags 分类做不同的操作:

    • 插入操作 Placement 。调用 commitPlacement 函数 [源码] 完成DOM的插入.

    • 更新操作 Update 。调用 commitWork 函数 [源码] 。这里有一些需要关注的地方.

                                      
                                        function commitWork(current: Fiber | null, finishedWork: Fiber): void {
          switch(finishedWork.tag) {
              case FunctionComponent:
              case MemoComponent:
              // ...
              {
                  // ...
                  commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
                  return;
              }
              case ClassComponent: {
                  return;
              }
              case HostComponent: {
                  const instance: Instance = finishedWork.stateNode;
                  if (instance != null) {
                      const newProps = finishedWork.memoizedProps;
                      const oldProps = current !== null ? current.memoizedProps : newProps;
                      const type = finishedWork.type;
                      const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
                      finishedWork.updateQueue = null;
                      if (updatePayload !== null) {
                          commitUpdate(
                              instance,
                              updatePayload,
                              type,
                              oldProps,
                              newProps,
                              finishedWork,
                          );
                      }
                  }
                  return;
              }
          }
      }
      
                                      
                                    
      • 对于函数组件 FunctionComponent 类型的fiber节点,会调用 commitHookEffectListUnmount 函数。该函数 [源码] 会遍历fiber节点的 updateQueue ,执行完所有 useLayoutEffect 的销毁函数。由 effectList 中的fiber节点顺序可知,当子孙节点的mutation 阶段完成后才会轮到父节点;因此,在执行函数组件中的useLayoutEffect销毁函数时, 该函数组件下对应的DOM 的更新操作是已经完成了的。因此,在销毁函数中可以访问到更新后的这部分DOM内容,但不推荐这么做.

      • 对于DOM元素 HostComponent 类型的fiber节点,取出节点上的 updateQueue 来执行 commitUpdate 函数。我们在前文讲解 completeWork 函数中对 HostComponent 处理时就说到,对DOM节点的 style 、文本子节点、自定义属性等的更新内容都保存在了fiber节点的 updateQueue 字段上。而 commitUpdate 函数要做的就是将 updateQueue 中的更新实际应用到DOM节点上面.

    • 删除操作 Deletion ,需要将fiber节点对应的DOM移除掉。调用 commitDeletion 函数 [源码] :

                                      
                                        function commitDeletion(
        finishedRoot: FiberRoot,
        current: Fiber,
        renderPriorityLevel: ReactPriorityLevel
      ): void {
        // Recursively delete all host nodes from the parent.
        // Detach refs and call componentWillUnmount() on the whole subtree.
        unmountHostComponents(finishedRoot, current, renderPriorityLevel);
      
        // 利用detachFiberMutation函数将fiber节点中的属性清空
        const alternate = current.alternate;
        detachFiberMutation(current);
        if (alternate !== null) {
          detachFiberMutation(alternate);
        }
      }
      
                                      
                                    

      调用 unmountHostComponents 函数,然后清空fiber节点属性。 unmountHostComponents 的函数体比较长,就不罗列出来了,其工作就是利用 while 模拟递归来循环调用fiber节点及其子孙节点 并执行以下操作 [源码] :

      • 对于 HostComponent ,直接将DOM节点从父节点中移除掉 [unmountHostComponents中1316-1331行] 。

      • 对于其他类型,则对其调用 commitUnmount 函数 [源码] :

        • FunctionComponent 类型的fiber节点,调度 useEffect 的销毁函数 [commitUnmount中881-908行]
        • ClassComponent 类型的fiber节点,需要卸载 ref 和调用 componentWillUnmount 方法 [commitUnmount中912-916行]

layout阶段

layout阶段的外层代码同上面两个阶段一样 [commitRootImpl中2081-2105行] 。在该阶段触发的生命周期钩子和 hook 都可以安全地访问到所有更新后页面上的DOM(在mutation阶段中提到过 在当时执行的 useLayoutEffect 销毁函数中可以访问到更新后的DOM,但只是其fiber节点对应的那一部分DOM更新后的内容,并不能确保整个页面上的DOM是最新的).

                        
                          // 切换current fiber树和workInProgress fiber树
root.current = finishedWork;

nextEffect = firstEffect;
do {
    try {
        commitLayoutEffects(root, lanes);	// layout阶段做的工作都在该函数中
    } catch (error) {
        // ...处理异常...
        nextEffect = nextEffect.nextEffect;
    }
} while (nextEffect !== null);

                        
                      

在文章开头的双缓冲fiber树中讲过,每当完成一次更新渲染后,就会交换 current fiber树 和 workInProgress fiber树 ,执行这项工作的语句正是上面代码的第一行: root.current = finishedWork 。时机是mutation阶段之后,layout阶段开始之前.

进入到 commitLayoutEffects 函数中的实现 [源码] :

                        
                          function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
    while (nextEffect !== null) {
        const flags = nextEffect.flags;
        
        // 触发生命周期钩子和hook
        if (flags & (Update | Callback)) {
          const current = nextEffect.alternate;
          commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
        }
        
        // 挂载ref
        if (flags & Ref) {
          commitAttachRef(nextEffect);
        }
        
        nextEffect = nextEffect.nextEffect;
    }
}

                        
                      

commitLayoutEffects 函数遍历 effectList ,对每个fiber节点做两件事情:

  1. 调用 commitLayoutEffectOnFiber 函数
  2. 挂载ref

重点关注 commitLayoutEffectOnFiber 函数 [源码] :

                        
                          function commitLifeCycles(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
      switch(finishedWork.tag) {
          case FunctionComponent:
          case ForwardRef:
          case SimpleMemoComponent:
          case Block: {
              // 执行useLayoutEffect的回调函数
              commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
              // 分别收集useEffect的销毁函数和回调函数,并再次开启调度
              schedulePassiveEffects(finishedWork);
              return;
          }
          case ClassComponent: {
              // 调用Class组件的生命周期钩子
              const instance = finishedWork.stateNode;
              if (finishedWork.flags & Update) {
                  if (current === null) {
                      instance.componentDidMount();
                  } else {
                      const prevProps =
                            finishedWork.elementType === finishedWork.type
                      ? current.memoizedProps
                      : resolveDefaultProps(finishedWork.type, current.memoizedProps);
                      const prevState = current.memoizedState;

                      instance.componentDidUpdate(
                          prevProps,
                          prevState,
                          instance.__reactInternalSnapshotBeforeUpdate,
                      );
                  }
              }
              // 执行setState中的第二个参数回调函数
              const updateQueue = finishedWork.updateQueue;
              if (updateQueue !== null) {
                  commitUpdateQueue(finishedWork, updateQueue, instance);
              }
          }
      }
  }

                        
                      
  • 对于函数组件 FunctionComponent ,执行所有的 useLayoutEffect 的回调函数。 然后会分别收集 useEffect 的销毁和回调函数到 pendingPassiveHookEffectsUnmount 和 pendingPassiveHookEffectsMount 变量中 [源码] 。并且 还会再次开启 useEffect 的调度,这里的代码同前面 before mutation 阶段开启 useEffect 调度的方式一样:

                                
                                  if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        scheduleCallback(NormalSchedulerPriority, () => {
          flushPassiveEffects();
          return null;
        });
    }
    
                                
                              

    都是通过判断 rootDoesHavePassiveEffects 全局变量来判断是否要开启调度;因此这两个阶段的开启操作是互斥的.

    正是因为 useLayoutEffect 回调函数是在mutation阶段(DOM操作)之后 同步 执行而 useEffect 是 调度后异步执行 的,所以可以用 useLayoutEffect 这个来解决 useEffect 回调中操作DOM会有闪屏的问题.

  • 对于类组件 ClassComponent ,需要区分是 mount 还是 update ,然后执行生命周期钩子 componentDidMount 或 componentDidUpdate 。 然后取出fiber节点上的 updateQueue ,调用其中的回调函数如 setState 的第二个参数中传入的函数.

  • 此外还有一些其他类型的处理,如对于 HostComponent 类型如果是 mount 情况会处理其自动聚焦的状态、对于 HostRoot ,会执行根节点渲染的回调函数 即 ReactDOM.render(<App/>, container, () => { ... }) 中的第三个参数.

至此,layout阶段完结了,整个 React 从触发更新至更新渲染完成的流程也结束了~ 。

最后此篇关于重新捋一捋React源码之更新渲染流程的文章就讲到这里了,如果你想了解更多关于重新捋一捋React源码之更新渲染流程的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com