《深入理解react》之render流程

一、前面的话

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

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

js 复制代码
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之前

在上一篇文章《深入理解react》之优先级(下)中,我们讲到了真正调度的具体任务是 performConcurrentWorkOnRoot 这个函数,但是它并没有立马进入render流程,让我们来看看在这之前发生了什么呢?

js 复制代码
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类的,一共有这么几种

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

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

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

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

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

  3. Scheduler必须还拥有时间片

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

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

二、render阶段

接下来执行流来到renderRootSync

js 复制代码
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是这个:

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

我们来看一下prepareFreshStack的过程

js 复制代码
 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

js 复制代码
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;
  } 

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

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

js 复制代码
function workLoopSync() {
    while (workInProgress !== null) {
      performUnitOfWork(workInProgress);
    }
}

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

js 复制代码
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结构

js 复制代码
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运行时,总会有两棵fiber树存在在内存中,render阶段的任务就是不断基于当前的current树,构建接下来要渲染的workInProgress

初始化

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

紧接着,经过render流程之后

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

更新

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

四、最后的话

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

后面的文章我们会依然会深入剖析react的源码,学习react的设计思想,如果你也对react相关技术感兴趣请关注我的《深入理解react》专栏,我们一起进步,有帮助的话希望朋友点个赞支持下,多谢多谢!

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax