React 源码之 "递" 阶段 mount 时流程

341 阅读7分钟

我们之前了解到:

  • render 阶段开始的函数是 renderRootSync
  • commit 阶段开始的函数是 commitRoot image.png 注意:这里所说的render阶段不是render调用函数。而是指渲染阶段,在调用render函数之后的执行。

接下来会来介绍render阶段中都做了哪些工作。我们之前知道render阶段是实现可中断的递归(时间切片),从而提升性能。 那递归就分为递阶段、归阶段两部分:

  • 递阶段执行的方法是 beginWork
  • 归阶段执行的方法是 completeWork

image.png 接下来是对他们进行研究,看看到底干了些啥玩儿意(ying),哈哈哈哈,东北话。

首先我们的App.js文件中代码如下:

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

mount

大概流程

Fiber双缓存架构在mount时不存在current Fiber树,而是在update时存在一颗current Fiber树。

所以beginWork和completeWork在mount时和update时会有不同。

接下来看看初次进入beginWork的时候都做了什么: 在你的项目中看这个文件:/react_origin_code_debugger/react/build/node_modules/react-dom/cjs/react-dom.development.js 里面的beginWork代码如下:

function beginWork(current, workInProgress, renderLanes) {
  // workInProgressRoot: FiberNode
  // current: FiberNode
  // 首次进入 beginWork 的节点,可以看到 current.tag === 3
  // current.tag === 3 对应的是 HostRoot--表示当前对应的根节点
  // 再次进入的时候 current === null
  {
    if (workInProgress._debugNeedsRemount && current !== null) {
      return remountFiber(
      current, 
      workInProgress, 
      createFiberFromTypeAndProps(workInProgress.type, workInProgress.key, workInProgress.pendingProps, workInProgress._debugOwner || null, workInProgress.mode, workInProgress.lanes));
    }
  }

  if (current !== null) {
    var oldProps = current.memoizedProps;
    var newProps = workInProgress.pendingProps;
    if (oldProps !== newProps || hasContextChanged() || (
     workInProgress.type !== current.type )) {
      didReceiveUpdate = true;
    } else {
      var hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);

      if (!hasScheduledUpdateOrContext && 
      (workInProgress.flags & DidCapture) === NoFlags) {
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
      }

      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      } else {
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  }

  workInProgress.lanes = NoLanes;
  // 先忽略这里
  switch (workInProgress.tag) {...}
  {
    {
      throw Error( "Unknown unit of work tag (" + workInProgress.tag + "). This error is likely caused by a bug in React. Please file an issue." );
    }
  }
}

image.png 首次进入 beginWork 的节点,可以看到 current.tag 的值为 3。current.tag: 3 对应的是 HostRoot--表示当前对应的根节点(FiberNode)

tag的表示含义可以在项目中这里看到:/react_origin_code_debugger/react/packages/react-reconciler/src/ReactWorkTags.js

如果没有本地可以去这里找一下:github.com/sunkuangdon… 也可以直接clone下来,本地运行起来。

当我们再次进入到 beginWork 的时候,current === null,下图可以看到 image.png

我们之前Fiber架构的双缓存架构中介绍过:对于首屏渲染,只有在当前应用的根节点存在 current Fiber,而其他节点只存在 workInProgress Fiber。

下图中,可以看到当前的 workInProgress Fiber 是 App: image.png

首次进入的时候 workInProgress fiber 与 current Fiber 是一样的。如果不信,可以打debugger再看一眼,这里就不展示了。

可见之前的 Fiber双缓存架构 是正确的分析。哈哈哈哈,先夸一波~

第三次进入 beginWork 的时候,workInProgress Fiber 是 div: image.png

第四次进入 beginWork 的时候,workInProgress Fiber 应该是header了,然后按照App.js的层级进行递进。

到了 img 没有子节点,而是有兄弟节点 p 和 a。这时候从 img 节点开始,跳出 beginWork 函数,执行 completeWork 函数。

completeWork 函数的 workInProgress Fiber 是 img,从而进行对img兄弟节点的查找。 image.png

img 有一个兄弟节点 p,然后会进入 p 节点的 beginWork

image.png

p节点有三个子节点,再次进入 beginWork 会看到子节点的文本节点:Edit

image.png

而文本节点 Edit 没有子节点,会进入 completeWork 函数,当前节点为 Edit 文本节点:

image.png

然后 会进入 code 节点的 beginWork 函数,再次进入你会发现此时不是beginWork了,而是completeWork函数。但是 code 明明有一个子节点:src/App.js。

这是因为React对于只有唯一一个文本子节点的节点,进行了优化处理。在这种情况下,文本节点将不会再生成自己的Fiber节点。

image.png

在接下来,code执行完 completeWork 之后,会查找生成code的兄弟节点,这里是:and save to reload. 文本节点。

image.png

在执行,肯定是and save to reload. 文本节点的 completeWork,执行完之后会进入父节点的 completeWork,也就是 p 节点的 completeWork。

当 p 节点的 completeWork 执行完之后,会进入 p 节点兄弟节点 a 的 beginWork:

image.png

a 节点的唯一子节点是 Learn React文本节点,所以这个文本节点不会存在自己的Fiber节点。所以接下来会进入 a节点的 completeWork,然后一点点的往上,进入 header节点的 completeWork...最后进入的是:FiberNode 节点,当前的根节点。

image.png

此时 Render 阶段就完成了,进入 commit 阶段。

beginWork 的具体工作

上面是大概的流程,接下来看看 beginWork 函数到底做了啥?

我们知道第一次进入 beginWork 的是当前应用的根Fiber节点,他的 tag === 3。我们以 div 这个Fiber 节点来进行分析。

首先 beginWork 会根据不同的 tag 进入不同的 case,上面的case被忽略了,这里单独拿出来进行分析:

  switch (workInProgress.tag) {
    case IndeterminateComponent:
      {
        return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
      }

    case LazyComponent:
      {
        var elementType = workInProgress.elementType;
        return mountLazyComponent(current, workInProgress, elementType, renderLanes);
      }

    case FunctionComponent:
      {
        var Component = workInProgress.type;
        var unresolvedProps = workInProgress.pendingProps;
        var resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps);
        return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderLanes);
      }

    case ClassComponent:
      {
        var _Component = workInProgress.type;
        var _unresolvedProps = workInProgress.pendingProps;

        var _resolvedProps = workInProgress.elementType === _Component ? _unresolvedProps : resolveDefaultProps(_Component, _unresolvedProps);

        return updateClassComponent(current, workInProgress, _Component, _resolvedProps, renderLanes);
      }

    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);

    case HostComponent:
      return updateHostComponent$1(current, workInProgress, renderLanes);

    case HostText:
      return updateHostText$1(current, workInProgress);

    case SuspenseComponent:
      return updateSuspenseComponent(current, workInProgress, renderLanes);

    case HostPortal:
      return updatePortalComponent(current, workInProgress, renderLanes);

    case ForwardRef:
      {
        var type = workInProgress.type;
        var _unresolvedProps2 = workInProgress.pendingProps;

        var _resolvedProps2 = workInProgress.elementType === type ? _unresolvedProps2 : resolveDefaultProps(type, _unresolvedProps2);

        return updateForwardRef(current, workInProgress, type, _resolvedProps2, renderLanes);
      }

    case Fragment:
      return updateFragment(current, workInProgress, renderLanes);

    case Mode:
      return updateMode(current, workInProgress, renderLanes);

    case Profiler:
      return updateProfiler(current, workInProgress, renderLanes);

    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);

    case ContextConsumer:
      return updateContextConsumer(current, workInProgress, renderLanes);

    case MemoComponent:
      {
        var _type2 = workInProgress.type;
        var _unresolvedProps3 = workInProgress.pendingProps;

        var _resolvedProps3 = resolveDefaultProps(_type2, _unresolvedProps3);

        {
          if (workInProgress.type !== workInProgress.elementType) {
            var outerPropTypes = _type2.propTypes;

            if (outerPropTypes) {
              checkPropTypes(outerPropTypes, _resolvedProps3,
              'prop', getComponentNameFromType(_type2));
            }
          }
        }

        _resolvedProps3 = resolveDefaultProps(_type2.type, _resolvedProps3);
        return updateMemoComponent(current, workInProgress, _type2, _resolvedProps3, renderLanes);
      }

    case SimpleMemoComponent:
      {
        return updateSimpleMemoComponent(current, workInProgress, workInProgress.type, workInProgress.pendingProps, renderLanes);
      }

    case IncompleteClassComponent:
      {
        var _Component2 = workInProgress.type;
        var _unresolvedProps4 = workInProgress.pendingProps;

        var _resolvedProps4 = workInProgress.elementType === _Component2 ? _unresolvedProps4 : resolveDefaultProps(_Component2, _unresolvedProps4);

        return mountIncompleteClassComponent(current, workInProgress, _Component2, _resolvedProps4, renderLanes);
      }

    case SuspenseListComponent:
      {
        return updateSuspenseListComponent(current, workInProgress, renderLanes);
      }

    case ScopeComponent:
      {

        break;
      }

    case OffscreenComponent:
      {
        return updateOffscreenComponent(current, workInProgress, renderLanes);
      }

    case LegacyHiddenComponent:
      {
        return updateLegacyHiddenComponent(current, workInProgress, renderLanes);
      }

    case CacheComponent:
      {
        {
          return updateCacheComponent(current, workInProgress, renderLanes);
        }
      }
  }

image.png

div 是一个 HostComponent,所以他会进入:updateHostComponent$1 函数

image.png

updateHostComponent$1 函数首先会复制很多变量,其中var isDirectTextChild = shouldSetTextContent(type, nextProps)是为了检测:当前Fiber节点是否只有唯一一个文本子节点,从而进行优化(将不会为文本子节点创建Fiber节点)。

image.png

接下来会进入 reconcileChildren 方法,在执行这个方法之前,当前 workInProgress.child === null,所以 reconcileChildren 这个方法会为当前的Fiber节点创建他的子Fiber节点。

其实他的函数名已经告诉我们了,当前的执行阶段是render阶段,render阶段是运行在协调器(reconciler)中的....

image.png

进入 reconcileChildren ,下图中能够看到,通过判断 current 是否等于 null 来进入mountChildFibers 或者 reconcileChildFibers

这俩方法有什么区别呢?

image.png

我们进入项目中:/react_origin_code_debugger/react/packages/react-reconciler/src/ReactChildFiber.old.js文件里面。下图中能够看见,这两个函数其实都是 ChildReconciler 函数调用的返回值,只不过 ChildReconciler 接受的参数不同

image.png

ChildReconciler 参数的含义:是否追踪副作用。

  • 我们以 deleteChild 函数为例,能够看到如果为false说明不追踪副作用,直接return。
  • 如果追踪副作用,deletions 中会追加 childToDelete

image.png

render 阶段会为具体要执行的 DOM 操作打上标记,具体去执行这些 DOM 操作是 commit 阶段。 下图就是对 DOM 操作打上的部分标记

比如:

  • Placement:Fiber节点对应的DOM节点需要插入在页面中。
  • Deletion:Fiber节点对应的DOM节点需要删除。 image.png

为什要用二进制的方式进行标记?

如果有一个Fiber节点对应的DOM节点,首先需要插入到页面中,然后需要更新他的属性,那么他就需要同时存在Placement、Update两个标记。利用 |= 操作运算符可以让一个变量同时拥有两个操作。

如何将DOM插入到页面中,会在下一篇中讲解。

接下来看:reconcileChildren 函数中 reconcileChildFibers 方法。

reconcileChildFibers 方法中他会判断当前 newChild 的类型,根据类型不同作出不同处理。

总结

当某一个Fiber节点进入 beginWork 时,他最终的目的是创建当前Fiber节点的第一个子Fiber节点。

  • 首先,会判断当前Fiber节点的类型,进入不同的 Update 的逻辑;
  • 然后在 Update 的逻辑中,他会判断当前workInProgress Fiber 是否存在对应的 current Fiber,来决定是否标记 EffectTag。(reconcileChildren)。
  • 接下来会进入 reconcile (reconcileChildFibers) 的逻辑,会判断当前Fiber 的 child 节点是什么类型,来执行不同的创建操作。最终会创建一个子Fiber节点 (FiberNode)

上面的括号中是执行的部分关键函数。注意每一次 beginWork 方法的执行,都只会创建一个Fiber节点,哪怕里面有Array类型。