【React 18.2 源码学习】React render 原来你是这样的

812 阅读7分钟

前面我们了解了 React 运行的大体流程,下面我们来看看其中的 render 阶段的流程

为什么 React Reconciler 工作的阶段叫 render 阶段

render 阶段工作在 renderRootConcurrent方法(并发 render 流程)或者 renderRootSync 方法(同步 render 流程)开始。下面看看具体代码:

image.png

图中是并发 render 的流程,同步的流程与这个差不多。可以看到在这个方法中 executionContext 被赋值 RenderContext,我理解这就是 Reconciler 的工作被叫做 render 的原因吧。

executionContext(执行上下文)

executionContext是 React 中的一个全局变量,表示当前代码执行的上下文环境。它可以用来区分当前代码是在哪个阶段执行的,比如是否在 render 阶段或者 commit 阶段。React 中有如下几个环境: image.png

  • NoContext:表示当前不属于 React 本身工作的阶段。
  • BatchedContext:表示批量更新的阶段,对更新进行合并
  • RenderContext:表示 render 阶段,正在构建 Fiber 树。
  • CommitContext: 表示 commit 阶段,正在更新 DOM ,以及执行副作用。

React render 流程

之前的文章说了,render 阶段主要是为了生成 Fiber 树, renderRootConcurrent 方法和 renderRootSync 方法最终都会循环遍历 FiberNode 执行 performUnitOfWork 方法,每次执行 performUnitOfWork 都会根据传入的 FiberNode 生成下一级的 FiberNode,最终得到整棵 Fiber 树。

image.png

image.png

renderRootSync(同步) 和 renderRootConcurrent(并发)唯一的区别就是有没有 shouldYield(是否中断) 的判断。workInProgress(后面简称 wip) 为当前正在工作的 FiberNode,会在 prepareFreshStack 方法中赋值为 FiberRoot,后续会在 “递” 或者 “归” 的过程中更新值,直到 workInProgress 赋值为 null 跳出循环。

那么 performUnitOfWork 怎么执行的呢?

performUnitOfWork 的工作分为 “递” 和 “归” 两部分。“递” 会从根节点开始向下以 DFS 的方式遍历,为遍历到的每个元素生成下一级的 FiberNode,具体的方法为 beginWork。当遍历生成的子节点为 null (相当于遍历到叶子结点)时进入 “归” ,对应completeUnitOfWork 中的 completeWork 方法,主要功能为 flags 进行冒泡。当某个 FIberNode 执行 completeWork 完成后,如果其存在兄弟节点,则会进入兄弟节点的 “递” 过程,如果不存在兄弟节点,则进入父节点的“归”过程。按照这个流程, “递” 和 “归” 会交错执行直到 HostRoot 执行 “归”为止。

下面我们以一个例子来看看具体的流程

function Index() { 
    return <p>函数组件 <span>123</span> </p> 
}
function App() { 
    return ( 
    <div className="App">
        <h1>子节点</h1>
        <Index /> 
    </div> 
    ) 
}

执行流程如下:

image.png

图中 BW 代表 beginWork,CW 代表 completeWork,后面的数字代表执行的顺序。

  1. HostRoot beginWork 生成 App FIberNode
  2. App beginWork 生成 div FiberNode
  3. div beginWork 生成 h1 FiberNode
  4. h1 beginWork 返回 null(DFS 到叶子结点,当前‘递’结束,开始‘归’)
  5. h1 执行 completeWork(发现 h1 有兄弟,继续兄弟的‘递’ ‘归’)
  6. Index beginWork 生成 p FiberNode
  7. p beginWork 生成 ‘函数组件’ FiberNode
  8. ‘函数组件’ beginWork 返回 null(DFS 到叶子结点,当前‘递’结束,开始‘归’)
  9. ‘函数组件’ 执行 completeWork(发现‘函数组件’有兄弟,继续兄弟的‘递’‘归’)
  10. span beginWork 返回 null(DFS 到叶子结点,当前‘递’结束,开始‘归’)
  11. span 执行 completeWork(没有兄弟节点,开始进行父节点的‘归’)
  12. p 执行 completeWork(没有兄弟节点,开始进行父节点的‘归’)
  13. Index 执行 completeWork(没有兄弟节点,开始进行父节点的‘归’)
  14. div 执行 completeWork(没有兄弟节点,开始进行父节点的‘归’)
  15. App 执行 completeWork(没有兄弟节点,开始进行父节点的‘归’)
  16. HostRoot 执行 completeWork(没有兄弟节点,开始进行父节点的‘归’)

当 HostRoot 完成 completeWork 的执行, executionContext 置为 NoContext render 的阶段就结束了。

下面来看看具体的代码

performUnitOfWork

image.png

beginWork

image.png 在 beginWork 中根据 wip 的 tag(表示节点的类型)进入不同类型节点的处理函数(比如函数组件、类组件、dom),最终处理生成并返回下一级的 FiberNode。

  • 有个参数是 current(代表老的 FIberNode),在 React 内部,通常通过 current 来判断当前流程是初次渲染还是更新:current 存在即是更新流程,如果不存在则是初次渲染。

beginWork 详解

接下来我们以 函数组件、类组件、原生 dom 三种元素为例来看看 beginWork 是怎么生成 FiberNode 的。

函数组件

image.png 对于函数组件在 beginWork 中最终执行了 updateFunctionComponent 函数并返回了执行的结果,这个结果就是 FiberNode 了。

image.png 比如下面这个函数组件:

function Index() { 
  console.log('render');
  return <p>函数组件 <span>123</span> </p> 
}

在 renderWithHooks 中会执行 Index 函数,最后返回函数组件的子 React 元素(也就是 p 元素)赋值给 nextChildren ,如下图,log 日志会在此时输出。 image.png

后面会执行 reconcileChildren 方法,这个就很重要了,子 FiberNode 就在这个方法中生成,并且会给有变化的 FiberNode 打上对应的 flag(flag 在后面介绍),并把子 FiberNode赋值给 workInProgress.child,最后返回 workInProgress.child ,这个生成子节点的过程就叫做 协调

协调就是 React 中的 diff 流程就,这个后面会专门写一篇文章。

类组件

image.png 对于类组件在 beginWork 中最终会执行 updateClassComponent 函数并返回执行的结果,这个结果就是 FiberNode 。下面我们看看 updateClassComponent 内部干了什么 image.png

原生 dom

image.png 对于原生 dom 在 beginWork 会执行 updateHostComponent 方法,下面我们来看看具体的内容

image.png

flag 是什么

flag 是一些二进制的数字,主要用来标记当前 FiberNode 节点是否有副作用,比如需要删除元素、更新元素等。以下是一些常见的 flag

  • Placement:节点是新增的,需要被插入到父节点中。
  • Update:节点需要被更新。
  • ChildDeletion:节点的某些子节点需要被删除。
  • ContentReset:节点的文本内容需要被重置。
  • Ref:节点需要设置 Ref。
  • Snapshot:在提交阶段需要对该节点进行快照的处理 也就是类组件有 getSnapshotBeforeUpdate 生命周期函数要执行

具体定义代码如下 image.png

标记 flag

标记 flag 的本质是二进制数的位运算,比如:当前的节点有更新,就会进行如下的操作

workInProgress.flags |= Update;

位运算不仅可以很方便的表达‘增、删、改、查’,并且因为是二进制运算还能获得更好的性能,在 React 内部,运用了大量的位运算,比如优先级相关,执行上下文等。

completeUnitOfWork

image.png 当 beginWork 返回的结果为 null 时,开始执行 completeUnitOfWork 函数,在这个函数内部进行 ‘归’ 的流程。当前节点执行 completeWork 函数完成后,通过判断 completeWork.silbling (结点是否存在兄弟结点),如果存在则将 completeWork.silbling 赋值给 wip 并返回,继续执行 performUnitOfWork 并开启新一轮 ‘递’ 的流程,如果 completeWork.silbling 不存在,并且 completeWork.return(父节点)存在,则继续 do while 循环,直到 completeWork.return 为 null (也是 completeWork 为 HostRoot)。

completeWork

image.png completeWork 主要的工作就是将 flags 进行冒泡。对应的操作是在 bubbleProperties 函数中完成。

completeWork 详解

可以看到根据节点的类型,会进行不同的处理,但是最终都执行了 bubbleProperties (冒泡)。

bubbleProperties

在这个函数中,会收集当前节点下所有子节点的副作用以及优先级。最终将收集到的数据赋值给当前节点对应的属性,这个过程被叫做属性的冒泡.

  • subtreeFlags:代表子节点中的副作用
  • childLanes:代表子节点的优先级 举个例子:

image.png
这样一个组件结构,比如 div 上有个更新(也就是 div 的 FiberNode 上的flag 为 Update),在 App 执行完 bubbleProperties 后,这个 Update 会冒泡到 App 上(App 的 subtreeFlags 为 Update)。然后每一级的组件都会执行这个方法,最终 div 的这个更新 Update 会一直冒泡到 HostRoot(App 的 subtreeFlags 也为 Update)。优先级的冒泡也是同样的流程。 下面是具体的代码: image.png

为什么要进行冒泡(bubbleProperties)

1、React 18 的 suspense 的功能需要。 2、能够通过任意 Fiber 都可以知道该 Fiber 的子树是否需要执行副作用。

总结

render 的流程是从根节点开始以 DFS 的顺序构建 Fiber 树。整个流程中从 HostRoot 通过 beginWork 方法生成下一级 FiberNode 并打上对应的 flag开始(‘递’),一直到 HostRoot 通过 completeWork 对 flag 进行冒泡结束,最终就得到了一颗带有 flag 标记的 Fiber 树。

最后

感谢大家的阅读,有不对的地方也欢迎大家指出来。

参考

  • React 18.2.0 源码
  • React 设计原理 —— 卡颂