likes
comments
collection
share

《深入理解react》之render流程

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

一、前面的话

我们终于可以进入render阶段了,在这个阶段我们可以学习到很多东西,它是为什么react能够呈现出UI,以及计算react状态变化的核心,那么怎么定义当前阶段是不是出于render阶段呢?其实网上的说法可能各不相同,这里我们需要把这个定义明晰一下

在源码中其实有关于状态的说明,使用了executionContext这个全局变量来表示当前的状态

export const NoContext = /*             */ 0b000; 0
const BatchedContext = /*               */ 0b001; 1
const RenderContext = /*                */ 0b010; 2
const CommitContext = /*                */ 0b100; 4

在本专栏中我们使用主流的概念,我们将executionContext中含有RenderContext的过程视为render阶段

在前面的内容中我们学习了优先级的概念,并知道react是通过Scheduler来调度的异步任务,而这个任务本质上就是一个函数,它的名字叫做performConcurrentWorkOnRoot 或者 performSyncWorkOnRoot 他们都是用来执行任务的,而本篇文章要讲的render阶段就蕴含其中,接下来就让我们一起来学习吧!

二、render之前

function performConcurrentWorkOnRoot(root, didTimeout) {
    // didTimeout 是Scheduler 提供的,如果当前任务还有时间片就返回 false,否则返回true 
    ...
    var lanes = getNextLanes( // 获取任务优先级
      root,
      root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
    );

    if (lanes === NoLanes) { // 防止空调用
      return null;
    } 
    var shouldTimeSlice = // 它的条件就是 不属于includesBlocking的优先级,且没有过期的优先级
      !includesBlockingLane(root, lanes) &&
      !includesExpiredLane(root, lanes) &&
      !didTimeout;
    var exitStatus = shouldTimeSlice
      ? renderRootConcurrent(root, lanes)
      : renderRootSync(root, lanes);
    
    // 后面是commit 的内容,我们后面看
    ...

    return null;
  }

首先他会获取一下当前的任务优先级,根据这个优先级来进行后面的状态计算

我们可以看到当逻辑即便来到了performConcurrentWorkOnRoot 之后也并不一定是并发渲染的,需要经过一个逻辑的判断,判断的主要依据就是根据当前的任务优先级,如果想要进入并发渲染模式需要满足三个条件

  1. includesBlockingLane 当前任务优先级必须不包含includesBlocking类的,一共有这么几种

    function includesBlockingLane(root, lanes) {
     // lanes 属于下面的几个中的一个就返回 false ,不属于返回true
     var SyncDefaultLanes =
       InputContinuousHydrationLane |
       InputContinuousLane |
       DefaultHydrationLane |
       DefaultLane;
     return (lanes & SyncDefaultLanes) !== NoLanes;
    }
    

    也就是说当前的任务优先级必须不属于 SyncDefaultLanes

  2. 其次当前的任务优先级不能过期

    function includesExpiredLane(root, lanes) {
     // 当前的优先级属于过期的优先级就会返回false ,不过期就会返回true
     return (lanes & root.expiredLanes) !== NoLanes;
    }
    

    只要当前的任务优先级不属于root.expiredLanes中的就可以了

  3. Scheduler必须还拥有时间片

假设当前是第一次渲染页面就会被判定为同步渲染,因为第一次默认渲染的任务优先级是DefaultLane,条件一未通过,如下图所示

《深入理解react》之render流程

因此我们就来到了renderRootSync,它的含义就是从root开始进行 render阶段

二、render阶段

接下来执行流来到renderRootSync

function renderRootSync(root, lanes) {
    var prevExecutionContext = executionContext;
    executionContext |= RenderContext; // 标记render阶段
    var prevDispatcher = pushDispatcher();  // 
    if (
      workInProgressRoot !== root ||
      workInProgressRootRenderLanes !== lanes
    ) {
      // 获得transition优先级
      workInProgressTransitions = getTransitionsForLanes();
      // 在这里进行 workInProgress的准备,将Update放在shared.pending上
      prepareFreshStack(root, lanes);
    }
    // 开始同步render
    do {
      try {
        workLoopSync();
        break;
      } catch (thrownValue) {
        handleError(root, thrownValue);
      }
    } while (true);
    // 归还 context Dispatcher 等全局变量
    resetContextDependencies();
    executionContext = prevExecutionContext;
    popDispatcher(prevDispatcher);
    workInProgressRoot = null;
    workInProgressRootRenderLanes = NoLanes;
    return workInProgressRootExitStatus;
  }

第一步: executionContext |= RenderContext 将流程正式标记为render阶段

第二步: 准备Dispacher,其实就是我们调用useState等API时的引用,它是通过ReactCurrentDispatcher$2.current这样的一个全局变量来调用的,通过给他赋予不同的对象,我们就会调用不同的API,这就是为什么我们在组件之外的地方使用hooks的时候会报错了,因为此时赋予的Dispatcher是这个:

《深入理解react》之render流程

在第一次挂载的时候,workInProgressRoot一定是没有值的,因此要准备一个空的workInProgress,因为render过程的本质就是根据最新的状态,得到最新的ReactElements,然后diff这个ReactElement和current树,从而计算出不同,打上这个不同的标签

我们来看一下prepareFreshStack的过程

 function prepareFreshStack(root, lanes) {
    // 将finishedWork置空
    root.finishedWork = null;
    root.finishedLanes = NoLanes;

    if (workInProgress !== null) { // 初始化时为空
      ...
    }
    // 对一些关于workInProgress相关的全局变量的赋值
    workInProgressRoot = root;
    // 创建一棵空的workInProgress树
    var rootWorkInProgress = createWorkInProgress(root.current, null); // 创建一个Fiber,基本就是对RootFiber的复制。
    // 对一些关于workInProgress相关的全局变量的赋值
    workInProgress = rootWorkInProgress; //赋值给全局的workInProgress
    workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
    workInProgressRootExitStatus = RootInProgress;
    ...
    // 把updateQueue 中的 interleaved指向的更新队列指向 shared.pending
    finishQueueingConcurrentUpdates();

    return rootWorkInProgress; // 将这棵空树返回
  }

这一步的主要作用就是对workInProgress做相关的准备,createWorkInProgress基本上就是基于current克隆出来了一个新的Fiber节点,下面可以看一下如何创建的 workInProgress

function createWorkInProgress(current, pendingProps) {
    var workInProgress = current.alternate;
    if (workInProgress === null) { // 初始化时就是空的
      workInProgress = createFiber( // 创建一个fiber,和current一摸一样。
        current.tag,
        pendingProps,
        current.key,
        current.mode
      );
      workInProgress.elementType = current.elementType;
      workInProgress.type = current.type;
      workInProgress.stateNode = current.stateNode;
      workInProgress.alternate = current;
      current.alternate = workInProgress;
    } else { // 更新时
      workInProgress.pendingProps = pendingProps; // Needed because Blocks store data on type.
      workInProgress.type = current.type; 
      // 初始化所有的副作用
      workInProgress.flags = NoFlags; // The effects are no longer valid.
      workInProgress.subtreeFlags = NoFlags;
      workInProgress.deletions = null;
    } 
    workInProgress.flags = current.flags & StaticMask;
    workInProgress.childLanes = current.childLanes;
    workInProgress.lanes = current.lanes;
    workInProgress.child = current.child;
    workInProgress.memoizedProps = current.memoizedProps;
    workInProgress.memoizedState = current.memoizedState;
    workInProgress.updateQueue = current.updateQueue; // 把updateQueue的引用拿过来。
    ...
    return workInProgress;
  } 

现在内存中就有这样的一个结构

《深入理解react》之render流程

接下来正式进入构建workInProgress树的阶段,初始化时是同步执行的因此走的是workLoopSync

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

由于提前已经准备好了第一个 workInProgress的节点,因此这个循环会不断执行,直到整棵树调和完毕

function performUnitOfWork(unitOfWork) { // 参数就是workInProgress的第一个节点
    var current = unitOfWork.alternate;
    var next;
    // 向下调和
    next = beginWork(current, unitOfWork, subtreeRenderLanes); // subtreeRenderLanes就是本次更新的任务优先级,在准备wormInProgress时赋值的。
    
    unitOfWork.memoizedProps = unitOfWork.pendingProps;

    if (next === null) {
      // 向上归并
      completeUnitOfWork(unitOfWork);
    } else {
      workInProgress = next;
    }
    ReactCurrentOwner$2.current = null;
  }

render阶段的两大流程在这里就一览无余了,每一个fiber节点都会经历一次 beginWorkcompleteWork这两个流程,假设我们现在有这样的一个dom结构

const App = ()=>{
   return (
     <div>
       <p>
         <span>hello</span>
       </p>
       <span>深入理解react</span>
     </div>
   )
}

那么整体的调和流程就是:

  1. beginWork: RootFiber
  2. beginWork: App
  3. beginWork: div
  4. beginWork: p
  5. beginWork: span-hello
  6. completeWork: span-hello
  7. completeWork: p
  8. beginWork: span-深入理解react
  9. completeWork: span-深入理解react
  10. completeWork: div
  11. completeWork: App
  12. complleteWork: RootFiber
  13. 调和结束

一图胜千言,我们用一个流程图来表示一下:

《深入理解react》之render流程

三、两棵树

在整个react运行时,总会有两棵fiber树存在在内存中,render阶段的任务就是不断基于当前的current树,构建接下来要渲染的workInProgress

初始化

在mount阶段,也就是页面从0到1的过程中,会通过初始化流程建立这样的一棵树

《深入理解react》之render流程

紧接着,经过render流程之后

《深入理解react》之render流程

内存中有这样的一个结构,react根据workInProgress渲染真正的DOM界面之后,再把current 指针指向这个workInProgress

更新

在更新流程中,内存中基于现在的current,重新构建一棵新的workInProgress树,当然这个过程会尽可能复用之前的fiber节点,经过render流程之后

《深入理解react》之render流程

四、最后的话

好了,本篇文章比较简单,没什么特别深入的内容,主要是给接下来的文章铺路,下面的内容我会将beginWorkcompleteWork做一个梳理,看看他们具体做了什么,在初始化阶段和更新阶段有什么不同!