likes
comments
collection
share

React中的render流程

作者站长头像
站长
· 阅读数 44

一、前沿

我们都知道 react 整体分为三个阶段 Scheduler、render、commitScheduler负责任务的调度;render负责生成fiber树,打上副作用标签,diff等等。commit负责生成真实的dom并渲染到浏览器上。在之前的文章中我们已经介绍过了 scheduler。那么在本章中我们介绍一下 render流程

二、整体流程

我们的 render主要分为两大部分。beginWork、complete

beiginWork

首先我们的 react会从 rootFiber开始按照深度优先递归的方式,遍历 Fiber节点。并且会对每个 Fiber都执行一次 beiginwork方法。该方法主要负责根据已有的Fiber节点,创建出新的Fiber节点。()

  • Fiber -child-> childFiber
  • childFiber -return-> Fiber

然后两者通过childreturn属性连接起来

当执行到叶子 Fiber节点时就会执行向上“归”的流程也就是 complete

complete

complete中会根据不同的 tag类型执行不同的逻辑

    function completeWork (current, workInProgress, renderLanes) {
    
      switch (workInProgress.tag) {
        case IndeterminateComponent:
        case LazyComponent:
        case SimpleMemoComponent:
        case FunctionComponent:
        case ForwardRef:
        case Fragment:
        case Mode:
        case Profiler:
        case ContextConsumer:
        case MemoComponent:
          bubbleProperties(workInProgress);
          return null;
        case ClassComponent:
          {
            var Component = workInProgress.type;

            if (isContextProvider(Component)) {
              popContext(workInProgress);
            }

            bubbleProperties(workInProgress);
            return null;
          }
         ......

当执行完一个 Fiber节点的 complete阶段后,Fiber会先寻找 silbing(兄弟节点)。如果有兄弟节点就进入兄弟节点的 complete流程。如果没有兄弟节点,那么就进入 return (父节点)的 complete流程。一直到根节点

至此整个 render流程结束

例子

我们先来看一段代码实例

function App() {
    return <div>
        huyunkun
        <span>test</span>
    </div>
}

根据这个 App组件我们最终会生成一棵这样子的 Fiber

React中的render流程

首先让我们来从源码的角度看一看在 mount时,是如何生成这颗 Fiber树的。

mount时的beginWork和compele

render流程的入口为 performConcurrentWorkOnRoot或者 performSyncWorkOnRoot

最终 performSyncWorkOnRoot会进入到 workLoopSync。而 performConcurrentWorkOnRoot会进入到 workLoopConcurrent

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

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

workLoopConcurrent会通过 shouldYield来判断浏览器是否还有空余时间,从而实现时间切片。最终他们都会进入 performUbitOfWork方法。(想要了解更多时间切片的内容可以看笔者的另一片文章 scheduler如何时间时间切片)。

function performUnitOfWork (unitOfWork) {
  var next;

  // beginwork 阶段
  if ((unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork$1(current, unitOfWork, renderLanes$1);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork$1(current, unitOfWork, renderLanes$1);
  }

  // 已经执行到叶子结点的时候就开始执行complete流程
  // complete 阶段
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    // 将当前执行的节点 执行 next
    workInProgress = next;
  }

  ReactCurrentOwner$2.current = null;
}

我们可以看到该方法的主要功能就是执行 Fiber节点的 beiginWork和complete

mount时的beiginWork

下面我们正式进入 beiginWork方法的分析

function beginWork (current, workInProgress, renderLanes) {
    if (current !== null) {
        // 这里一大段逻辑判断是否可以复用子Fiber
    } else {
       didReceiveUpdate = false;
    }
    
    workInProgress.lanes = NoLanes;
    
    switch(workInProgress.tag) {
        case IndeterMinateComponent: 
        {
            return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
        }
    }
}

我们可以看到,

  1. beiginWork中通过 current !== null判断当前是否为 mount。如果current !== null说明当前为 update,否则为 mount
  2. 接下来进入到mount的判断中, didReceiveUpdate被赋值为false。使用 didReceiveUpdate来判断是否复用子 Fiber节点。
  3. 接着赋值优先级 lanesNolanes
  4. 最后进入到不同组件类型的执行逻辑中。因为当前是 mountreact还不知道当前组件是 Function还是 Class因此会进入 IndeterMinateComponent类型的逻辑。
 function mountIndeterminateComponent (_current, workInProgress, Component, renderLanes) {
     ......
     reconcileChildren(null, workInProgress, value, renderLanes);
     return workInProgress.child;
 }
 
 function reconcileChildren (current, workInProgress, nextChildren, renderLanes) {
      if (current === null) {
        workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
      } else {
        // 子fiber节点
        // current.child是fiber nextChildren是reactElement对象
        workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
      }
    }

最终进入到 reconcileChildren方法中。通过判断 current是否为null进入到不同的逻辑。当前我们是mount状态,currentnull。因此我们会进入 mountChildFibers。接下去生成新的子 Fiber。这部分和 diff算法有关。笔者会另起一篇文章来介绍diff算法。diff过程中会给每一个 Fiber打上不同的flags,表示要对该 Fiber做不同的操作。那么到这里针对一个 Fiber节点在mount状态下的 beginWork阶段已经结束。当执行到叶子节点的时候,我们便会开启 complete流程。

最后我们来看一下 flags

export const Placement = /*                    */ 0b00000000000000000000000010;
export const Update = /*                       */ 0b00000000000000000000000100;
export const ChildDeletion = /*                */ 0b00000000000000000000001000;
export const ContentReset = /*                 */ 0b00000000000000000000010000;
export const Callback = /*                     */ 0b00000000000000000000100000;
export const DidCapture = /*                   */ 0b00000000000000000001000000;
export const ForceClientRender = /*            */ 0b00000000000000000010000000;
export const Ref = /*                          */ 0b00000000000000000100000000;
export const Snapshot = /*                     */ 0b00000000000000001000000000;
export const Passive = /*                      */ 0b00000000000000010000000000;
export const Hydrating = /*                    */ 0b00000000000000100000000000;
export const Visibility = /*                   */ 0b00000000000001000000000000;
export const StoreConsistency = /*             */ 0b00000000000010000000000000;

mount时的complete

function completeUnitOfWork (unitOfWork) {
    var completedWork = unitOfWork
    var next
    do {
        ......
        next = completeWork(completedWork)
        
        var siblingFiber = completedWork.sibling
        
        if (siblingFiber !== null) {
            workInProgress = siblingFiber
            return
        }
        
        completedWork = returnFiber
        
        workInProgress = completedWork
        
    } while(completedWork !== null)
    
}
  1. 在mount时先执行completeWork函数。
  2. 接着判断当前的 fiber节点是否有兄弟节点,有兄弟节点则执行兄弟节点的 completeWork流程,没有则会执行父节点的completeWork流程。
function completeWork (current, workInProgress, renderLanes) {
    var newProps = workInProgress.pendingProps;
    
    switch (workInProgress.tag) {
        case HostComponent:
           {
                // mount阶段
              var _rootContainerInstance = getRootHostContainer();

              // 生成dom节点
              var instance = createInstance(_type, newProps, _rootContainerInstance, _currentHostContext, workInProgress);
              // 将子孙DOM节点插入刚生成的DOM节点中
              appendAllChildren(instance, workInProgress, false, false);
              // DOM节点赋值给fiber.stateNode

              // 处理props
              if (finalizeInitialChildren(instance, _type, newProps)) {
                markUpdate(workInProgress);
              }
           }
    }
}

我们关注生成真实dom节点的 HostCOmponent类型的节点。在 complete会生成真实的dom节点 instance。然后通过 appendAllChildren方法将真实的dom节点插入到父节点当中。当 complete到根节点的时候我们就可以得到一棵完整的dom树 最后处理一遍 props就完成了 mountcomplete的流程。

update中的beginWork

beginWorkmountupdate的逻辑大体相同。主要区别在于在update中加上了优化逻辑。(通过标记didReceiveUpdate来判断子fiber是否可复用)

function beginWork (current, workInProgress, renderLanes) {

  // 是否要协调的逻辑
  // current !== null 则是update时触发
  if (current !== null) {
    //update阶段执行
    // 如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child
    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 && // If this is the 
        (workInProgress.flags & DidCapture) === NoFlags) {
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
      }

      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {

        didReceiveUpdate = true;
      } else {

        didReceiveUpdate = false;
      }
    }
  }
}
  1. 首先依然通过current !== null来判断当前是否是mount阶段。当前为update阶段。进入update的逻辑
  2. 然后满足某些情况下 didReceiveUpdate会被标记为false。表示子fiber可复用。以下为可复用的情况
  • oldProps !== newProps ,workInProgress.type !== current.type
  • !hasScheduledUpdateOrContext && (workInProgress.flags & DidCapture) === NoFlags

接下来就会进入到不同tag的逻辑中了

update中的complete

对于hostComponet类型,在更新阶段已经有dom了所以不需要重新生成dom。只需要处理props

当进入到complete时,tag为HostComponent类型会进入到updateHostComponent的逻辑中。

updateHostComponent中处理完props的逻辑后会生成fiberupdateQueue。当所有的complete工作都技术之后会进入到commit阶段。到这里我们的render阶段也就结束了

总结

最后我们总结一下。整个render过程会分为beginWorkcomplete两个过程(也就是所谓的递和归)。在beginWork中我们会进行diff然后给fiber节点打上flags。在complete中会生成dom节点,并处理props属性。最终交给commit。渲染出真实的dom