头图

React 源码解析系列 - React 的 render 阶段(一):基本流程介绍

array_huang
English

系列文章目录(同步更新)

本系列文章均为讨论 React v17.0.0-alpha 的源码

React 的工作流程

React 的工作流程主要分为 render 和 commit 两个阶段:

  • render 阶段根据 JSX 转化成的 ReactElement (更准确地来说,jsx 在代码编译阶段经 babel 转化成由 React.createElement() 包裹的代码段,在 render 阶段该代码段被执行后便生成了对应的 ReactElement 对象)来创建 Fiber 树;React 采用“双缓存”的思想,因此同一时刻维持有两棵 Fiber 树 ,一颗是 current,对应浏览器当前已渲染的 DOM 树,而另一棵则是 workInProgress,是在初始化时或者组件状态更新后由 reconciler 创建的一个工作副本。
  • commit 阶段指的是把 workInProgress 渲染到页面上 ,当然这个过程并不会是全量更新,而是根据创建 workInProgress 时打的一些“标记”(effectTag),来确定在某个 DOM 节点上具体做什么操作,比如更新文本、删除节点等,以尽量小的代价来在 DOM 上还原 workInProgress ;workInProgress 会在 commit 后被标记为 current。

render 阶段的入口

render 阶段开始于 performSyncWorkOnRootperformConcurrentWorkOnRoot 方法的调用,这两个方法的差异在于一个是同步,而另一个是异步(concurrent)的。

这两个方法分别会调用下面两个方法—— workLoopSync 和 workLoopConcurrent:

function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

这两个方法的差别在于是否调用 shouldYield() 来判断当前浏览器帧有没有空余时间,对于 concurrent 模式来说,如果没有空余时间就会退出当前循环了;可以看到这两个方法最终都调用了 performUnitOfWork

通过方法中的 while 循环,Fiber Reconciler 将通过“深度遍历”的策略来完成对整棵 workInProgress Fiber Tree 的创建。

performUnitOfWork

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate; // current树上对应的Fiber节点,有可能为null
  // ...省略

  let next; // 用来存放beginWork()返回的结果
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    // ...省略
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    // ...省略
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }

  // ...省略
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) { // beginWork返回null,表示无(或无需关注)当前节点的子Fiber节点
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next; // 下次的workLoopSync/workLoopConcurrent的while循环的循环主体为子Fiber节点
  }

  // ...省略
}

Fiber Reconciler 由于是从 Stack Reconciler 重构来的,它实际上模拟了“递归”的执行过程,这实际上也是一个类似于“深度优先遍历”的过程。而 performUnitOfWork 则是具体控制“递归”流程的方法,其主要包含:

  1. "递"阶段 —— beginWork
  2. “归”阶段 —— completeUnitOfWork

递归的结束条件

结合 performUnitOfWork 的调用者—— workLoopSync 和 workLoopConcurrent 方法来看,只要 workInProgress 这个 Fiber 节点的指针被置为 null ,则整个递归遍历结束。

function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

render 的“递”阶段 —— beginWork

在“递”阶段,调用的是 beginWork 方法生成子 Fiber 节点,而 workInProgress 则会指向这新的 Fiber 节点,作为下次循环的主体(unitOfWork);但根据实际情况,beginWork 可能会返回 null,而这正是递归的终止条件。

render 的“归”阶段 —— completeUnitOfWork

一旦 beginWork 返回null,即满足递归的终止条件,则进入本次循环的“归”阶段—— completeUnitOfWork

在“归”阶段,主要是调用 completeWork 方法来收拢所有后代 Fiber 节点(也可以称为子 Fiber 节点树)上的数据到本次循环主体(unitOfWork)上

然后,判断是否有兄弟节点,如果有的话,则将 workInProgress 指针指向兄弟节点,作为下次循环的主体(unitOfWork);否则将不停循环回退父节点(当然回退的过程中也是会执行 completeWork 方法的),直到找到某个兄弟节点来进入下次循环(performUnitOfWork),或是父节点为null(此时将会让 workInProgress 指针指向 null)。

注意:workInProgress 这个指针变量位于 completeUnitOfWork 的父级作用域中,因此 completeUnitOfWork 方法不需要 return 就可以赋值 workInProgress 指针。

递归完成

若在 completeUnitOfWork 中将 workInProgress 指针指向 null ,表示已完成遍历创建整棵 workInProgress 树。

至此, render 阶段全部工作完成。在 performSyncWorkOnRoot 函数中 fiberRootNode 被传递给 commitRoot 方法,开启 commit 阶段工作流程。

举例说明递归的过程

function App() {
    return (
        <p>
            <span>array</span>
            <span>huang</span>
        </p>
    )
}

ReactDom.render(<App />, document.getElementById('#root'));

image.png

1. rootFiber beginWork 
2. App Fiber beginWork 
3. p Fiber beginWork 
4. span Fiber beginWork 
5. span Fiber completeWork // 注意这里是直接走到 span 的“归”阶段,因为文本节点 "array" 被优化处理了
6. span Fiber beginWork 
7. span Fiber completeWork
8. p Fiber completeWork 
9. App Fiber completeWork 
10. rootFiber completeWork
阅读 1.2k

实用至上
记录和分享我在开发过程中的心得与感悟
10.3k 声望
6k 粉丝
0 条评论
10.3k 声望
6k 粉丝
文章目录
宣传栏