流程概览

在首次执行 ReactDOM.render

  1. 创建一个FiberRootNode节点以及RootFiber节点,并且将FiberRootNode的current属性指向RootFiber(这两者之间的关系在之前有提到,这里不多赘述)。

image.png

  1. 进入到render阶段,此阶段会生成一棵 workInProgress Fiber 树,并且在构建此树的过程中会尝试通过Fiber节点的 alternate 属性复用旧节点的属性。
  2. 构建 workInProgress树 完毕后,进入 commit阶段 ,此阶段会根据workInProgress树来进行应用DOM节点的更新。

后续ReactDOM.render 或者组件更新的过程中,由于已经存在FiberRootNode与RootFiber,上述过程的第1步会被忽略,直接进行第2,3步的操作。

JSX的本质

在React当中,JSX会被babel插件 babel-transform-react-jsx 编译为以 React.createElement 方法创建的对象。此方法的内容如下:

  1. export function createElement(type, config, children) {
  2. let propName;
  3. // Reserved names are extracted
  4. const props = {};
  5. let key = null;
  6. let ref = null;
  7. let self = null;
  8. let source = null;
  9. if (config != null) {
  10. if (hasValidRef(config)) {
  11. ref = config.ref;
  12. }
  13. if (hasValidKey(config)) {
  14. key = '' + config.key;
  15. }
  16. self = config.__self === undefined ? null : config.__self;
  17. source = config.__source === undefined ? null : config.__source;
  18. // Remaining properties are added to a new props object
  19. for (propName in config) {
  20. if (
  21. hasOwnProperty.call(config, propName) &&
  22. !RESERVED_PROPS.hasOwnProperty(propName)
  23. ) {
  24. props[propName] = config[propName];
  25. }
  26. }
  27. }
  28. // Children can be more than one argument, and those are transferred onto
  29. // the newly allocated props object.
  30. const childrenLength = arguments.length - 2;
  31. if (childrenLength === 1) {
  32. props.children = children;
  33. } else if (childrenLength > 1) {
  34. const childArray = Array(childrenLength);
  35. for (let i = 0; i < childrenLength; i++) {
  36. childArray[i] = arguments[i + 2];
  37. }
  38. props.children = childArray;
  39. }
  40. // Resolve default props
  41. if (type && type.defaultProps) {
  42. const defaultProps = type.defaultProps;
  43. for (propName in defaultProps) {
  44. if (props[propName] === undefined) {
  45. props[propName] = defaultProps[propName];
  46. }
  47. }
  48. }
  49. return ReactElement(
  50. type,
  51. key,
  52. ref,
  53. self,
  54. source,
  55. ReactCurrentOwner.current,
  56. props,
  57. );
  58. }

此方法的任务主要如下:

  1. 获取config参数中的ref和key属性
  2. 将其余 非保留属性 复制到变量props中
  3. 将传入的children参数转换成数组
  4. 获取默认参数赋值给变量props
  5. 返回 ReactElement 函数的调用结果

ReactElement 的代码如下:

  1. const ReactElement = function(type, key, ref, self, source, owner, props) {
  2. const element = {
  3. // This tag allows us to uniquely identify this as a React Element
  4. $$typeof: REACT_ELEMENT_TYPE,
  5. // Built-in properties that belong on the element
  6. type: type,
  7. key: key,
  8. ref: ref,
  9. props: props,
  10. // Record the component responsible for creating this element.
  11. _owner: owner,
  12. };
  13. return element;
  14. };

本质上就是返回一个 $$typeof 属性为 REACT_ELEMENT_TYPE 的对象,此对象含有 typekeyrefprops 等相关属性。

由此我们可以得出结论
JSX在React当中本质为一个 ReactElement 对象,只不过在通过 ReactElement 函数创建前,会首先经过 React.createElement 方法对传入的参数进行 预处理

JSX与Fiber树的关系

在流程概览中说到,无论是初始化还是后续更新,React都会生成一棵workInProgress Fiber树。那么创建workInProgress Fiber节点的 依据就是JSX对应的ReactElement对象(以下称JSX对象) ,创建时会对比JSX对象和 current Fiber树 中对应的Fiber节点,根据对比的结果生成新的 workInProgress Fiber节点

render阶段工作流程

之前说到,旧版的协调器为 Stack Reconciler ,而新版的协调器为 Fiber Reconciler 。旧版的之所以称之为Stack Reconciler,是因为它是通过 函数递归调用 的方式进行深入优先遍历,数据结构是,这种遍历方式是无法中断的。而新版的Fiber Reconciler,其数据结构为链表,可以通过循环的方式进行深入优先遍历,这种遍历方式是可以被中断的。
而由于Fiber树的遍历也是深入优先遍历,因此 对于Fiber树的每个节点来说,都会存在“递”和“归”两个阶段 ,以下从分别从这两个阶段进行分析,而两阶段将分别从mount时和update时进行分析。

“递”阶段(beginWork)

递阶段的主要函数为 beginWork

  1. function beginWork(
  2. current: Fiber | null, // 当前的Fiber节点
  3. workInProgress: Fiber, // 正在被构建的Fiber节点
  4. renderLanes: Lanes, // Scheduler 调度相关
  5. ): Fiber | null {}

beginWork内有两部分的处理逻辑,如果 current !== null 为true,则进入 update 的处理逻辑,反之则进入mount的处理逻辑。

mount处理逻辑

这部分会通过 swtich(workInProgress.tag) 根据不同的Fiber节点类型进入不同的处理逻辑。比较常用的类型为:

  • html元素节点:HostComponent
  • 文本节点:HostText
  • 函数组件:FunctionComponent
  • 类组件:ClassComponent

全部类型可在 ReactWorkTag.js 中找到。
除了HostText外,其余三种类型最终都会调用 reconcileChildren 函数。这个函数会生成workInProgress Fiber节点的子Fiber节点,对应内容如下:

  1. export function reconcileChildren(
  2. current: Fiber | null,
  3. workInProgress: Fiber,
  4. nextChildren: any,
  5. renderLanes: Lanes,
  6. ) {
  7. if (current === null) {
  8. workInProgress.child = mountChildFibers(
  9. workInProgress,
  10. null,
  11. nextChildren,
  12. renderLanes,
  13. );
  14. } else {
  15. workInProgress.child = reconcileChildFibers(
  16. workInProgress,
  17. current.child,
  18. nextChildren,
  19. renderLanes,
  20. );
  21. }
  22. }

其中,函数的nextChildren参数即为JSX节点,mount时会进入 mountChildFibers 逻辑,update时会进入 reconcileChildFibers 逻辑。定义如下:

  1. export const reconcileChildFibers = ChildReconciler(true);
  2. export const mountChildFibers = ChildReconciler(false);

可以知道这两个函数共用一套逻辑,不同的是传入 ChildReconcilershouldTrackSideEffects 参数,ChildReconciler会根据此参数来判定当前是否需要处理副作用(DOM节点的增删改等等),处理副作用的具体方法就是 标记effectTag

update处理逻辑

进行update时,构建workInProgress Fiber树时会通过 current.alternate 得到对应的根节点,然后在此节点的基础上 复用 current Fiber树的若干属性,包括其child节点(这个复用的过程在 createWorkInProgress 中),workInProgress Fiber树将会在此的基础上进行修改。

如果Fiber节点 没有发生变化(参数、context没变化)而且 判定Fiber节点没有需要的更新 (这部分待完成),那么beginWork的逻辑会走向 bailoutOnAlreadyFinishedWork 这个函数,即复用已完成的工作,包括复用对应的current Fiber的子节点,然后直接返回子Fiber,不会对当前Fiber节点做任何处理。

如果Fiber节点发生变化,那么就会走mount时同样的逻辑,进行自身节点的更新以后,最终走向 reconcileChildren 逻辑生成子Fiber节点,进入下一轮循环。

总结

无论是mount阶段还是update阶段的处理逻辑,其最终目标都是要 生成当前Fiber节点的子节点,然后返回 。这个子节点有可能是直接复用,也有可能是重新生成,生成子节点对应的函数为 reconcileChildren

“归”阶段(completeWork)

此阶段的主要函数为 completeWork
completeWork函数内mount和update走同样的逻辑,都是根据不同类型的workInProgress Fiber节点(tag属性),走不同的逻辑。

mount处理逻辑

针对HostComponent的Fiber节点,主要的任务有三:

  1. 生成新的DOM节点createInstance
  2. 添加子Fiber节点对应的DOM节点appendAllChildren
  3. 为DOM节点添加DOM属性finalizeInitialChildren

值得一提的是 appendAllChildren ,其代码如下:

  1. appendAllChildren = function(
  2. parent: Instance,
  3. workInProgress: Fiber,
  4. needsVisibilityToggle: boolean,
  5. isHidden: boolean,
  6. ) {
  7. // We only have the top Fiber that was created but we need recurse down its
  8. // children to find all the terminal nodes.
  9. let node = workInProgress.child;
  10. while (node !== null) {
  11. if (node.tag === HostComponent || node.tag === HostText) {
  12. appendInitialChild(parent, node.stateNode);
  13. } else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
  14. appendInitialChild(parent, node.stateNode.instance);
  15. } else if (node.tag === HostPortal) {
  16. // If we have a portal child, then we don't want to traverse
  17. // down its children. Instead, we'll get insertions from each child in
  18. // the portal directly.
  19. } else if (node.child !== null) {
  20. // 针对函数组件/类组件等深层嵌套的情况,会逐步深入得到HostComponent
  21. // 然后将DOM节点全部插入到parent中
  22. node.child.return = node;
  23. node = node.child;
  24. continue;
  25. }
  26. if (node === workInProgress) {
  27. return;
  28. }
  29. while (node.sibling === null) {
  30. if (node.return === null || node.return === workInProgress) {
  31. return;
  32. }
  33. // 从深层嵌套的函数组件或者类组件中返回
  34. node = node.return;
  35. }
  36. node.sibling.return = node.return;
  37. node = node.sibling;
  38. }
  39. };

根据我们之前提到的Fiber数据结构可知,Fiber的子节点是以 链表 的形式存在。所以在这个函数中会通过循环遍历链表的形式来遍历所有的子Fiber节点,其中指向下一个节点的属性为 sibling 。一个值得注意的细节是 node.sibling.return = node.return; 这行代码,Fiber节点的其他子节点(非第一个)是在这里完成添加 return

由于每个HostComponent都会执行appendAllChildren操作,这就保证了每个HostComponent完成completeWork后其stateNode是一棵完整的DOM树,包含所有子Fiber对应的DOM节点。

update处理逻辑

对于 HostText ,如果已经存在对应的stateNode,那么就会直接进入到 标记effectTag 的环节,如果前后Text不一致,就会将workInProgress Fiber的effectTag属性打上Update的标签

对于 HostComponent ,则会进入到对属性的处理中,处理过程在 diffProperties 中,此函数会返回一个包含属性修改内容的数组 updatePayload ,其偶数项为propKey,奇数项为propValue,然后赋值给workInProgress Fiber的updateQueue属性,在commit阶段会根据这个属性进行DOM节点的修改。此外,同样需要给workInProgress Fiber打上Update的effectTag。

为了避免在commit阶段重复遍历Fiber树,React在此阶段通过将有effectTag的Fiber节点以 链表 的形式组织起来。具体代码如下(在completeUnitOfWork函数中):

  1. if (
  2. returnFiber !== null &&
  3. // Do not append effects to parents if a sibling failed to complete
  4. (returnFiber.flags & Incomplete) === NoFlags
  5. ) {
  6. // Append all the effects of the subtree and this fiber onto the effect
  7. // list of the parent. The completion order of the children affects the
  8. // side-effect order.
  9. if (returnFiber.firstEffect === null) {
  10. returnFiber.firstEffect = completedWork.firstEffect;
  11. }
  12. if (completedWork.lastEffect !== null) {
  13. if (returnFiber.lastEffect !== null) {
  14. returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
  15. }
  16. returnFiber.lastEffect = completedWork.lastEffect;
  17. }
  18. // If this fiber had side-effects, we append it AFTER the children's
  19. // side-effects. We can perform certain side-effects earlier if needed,
  20. // by doing multiple passes over the effect list. We don't want to
  21. // schedule our own side-effect on our own list because if end up
  22. // reusing children we'll schedule this effect onto itself since we're
  23. // at the end.
  24. const flags = completedWork.flags;
  25. // Skip both NoWork and PerformedWork tags when creating the effect
  26. // list. PerformedWork effect is read by React DevTools but shouldn't be
  27. // committed.
  28. if (flags > PerformedWork) {
  29. if (returnFiber.lastEffect !== null) {
  30. returnFiber.lastEffect.nextEffect = completedWork;
  31. } else {
  32. returnFiber.firstEffect = completedWork;
  33. }
  34. returnFiber.lastEffect = completedWork;
  35. }
  36. }

一个Fiber节点有 firstEffectlastEffect 两个属性,分别指向 副作用链表 的头和尾。这个链表包含着当前Fiber节点中所有有副作用的子节点。上面的处理逻辑主要的工作就是将当前Fiber节点的副作用链表及当前Fiber节点整合到父Fiber节点的副作用链表当中。下面来分析细节:

  1. 将当前Fiber节点的副作用链表 拼接 到父Fiber节点的副作用链表末尾(9-17行)
  2. 如果当前Fiber节点有副作用,那么同样拼接到父Fiber节点的副作用链表的末尾

通过副作用链表的层层向上传递,最终将在rootFiber节点中获得一条包含Fiber树中所有副作用的副作用链表。

总结

  • mount处理逻辑:生成DOM节点,添加DOM属性,添加子Fiber的DOM节点
  • update处理逻辑:给Fiber打上相关的effectTag,并且整合到父Fiber节点的副作用链表中,以此逐层传递到rootFiber节点当中

    双缓存机制的运作流程

    Fiber架构概览当中说过React的双缓存指的是两棵树 current Fiber 树和 workInProgress Fiber 树。

双缓存的运行可以分为三个阶段:

  1. 首屏渲染
  2. 第一次更新
  3. 第二次更新

以下代码为例:

  1. function App() {
  2. const [num, setNum] = useState(0)
  3. const onClick = () => {
  4. setNum(state => state + 1)
  5. }
  6. return (
  7. <div onClick={onClick}>
  8. <p>{num}</p>
  9. </div>
  10. );
  11. }

首屏渲染

image.png
上述图片中,左侧为current Fiber树,右侧为workInProgress Fiber树。

首先,React会通过 createWorkInProgress 函数依据current Fiber的rootFiber 创建 workInProgress Fiber树的rootFiber,此外还会再两者之间通过 alternate 属性建立相互连接。

然后,由于是首屏渲染,剩下的Fiber节点没法根据current Fiber创建,所以剩余的Fiber节点都是通过其他的函数新建的(createFiberFromElement等等)。

值得注意的是,针对只有一个文本节点的节点,React并不会为那个文本节点独立创建一个新的Fiber。

第一次更新

image.png
在第一次更新中,current Fiber树即为首屏渲染中创建的workInProgress Fiber树。

由于已经存在对应的current Fiber节点,所以此次更新会使用 createWorkInProgress创建 App 以及 p 的Fiber节点,在两者之间建立连接,而由于rootFiber已经存在,所以会被 复用

第二次更新

image.png
第二次更新中,依然会使用 createWorkInProgress 获取Fiber节点,此次过程中,由于所有节点都已经存在,所以构建过程中所有节点都能 复用

总结

从上面的三个阶段可以得知,双缓存机制真正完全生效是在第二次更新及以后,因为此次更新后两个Fiber树的结构才构建完全。