大年初二,确定不利用时间自我提升?

490 阅读13分钟

引言

作为一个天天用 React 开发的人,某天突发奇想,凭我的技术能力,居然只停留在用的方向,而不去研究它的源码?这怎么能说的过去(手动狗头🐶),于是,就踏上了研究 React 源码的道路。

大纲

为了尽可能讲的清晰易懂,打算采用总分总的形式来讲解,同时辅助截图来说明问题,下面先贴一张流程图。这是 React 从初始化到页面渲染出来的整个过程,有不对的欢迎在评论区指出。另外本人用的教材是卡老师的 React 设计原理, 如果文章中出现了雷同的,纯属我觉得卡老师写的比我好~

React的文件结构

GitHub - facebook/react: The library for web and native user interfaces.

分析的源码是 React18,React18 主要有三大部分构成,分别是reconciler,scheduler,react。

本次源码用的是卡老师分析编译出来的 js 版本,调试起来方便且读起来也好读一些。下面就按照流程图上的大纲串讲一遍,有些深度的计划分为不同的文章讲解。

createRoot

这是 React调用的第一个函数,下面一起来看 createRoot 做了什么

function createRoot(){
  // ...
  var root = createContainer(
    container,
    ConcurrentRoot,
    hydrate,
    hydrationCallbacks,
    isStrictMode
  );
  var rootContainerElement =
    container.nodeType === COMMENT_NODE ? container.parentNode : container;
  listenToAllSupportedEvents
  return new ReactDOMRoot(root);
}

源码我精简了一部分,主要就是三大部分,调用了 createContainer,创建了一个rootContainerElement,处理合成事件,以及返回了一个new ReactDOMRoot(root);

createContainer

createContainer内部调用了createFiberRoot,所以直接看这个方法,这个方法也很简单

function createFiberRoot(){
  var root = new FiberRootNode(containerInfo, tag, hydrate);
  // stateNode is any.

  var uninitializedFiber = createHostRootFiber(tag, isStrictMode);
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  {
    var _initialState = {
      element: null,
    };
    uninitializedFiber.memoizedState = _initialState;
  }
  initializeUpdateQueue(uninitializedFiber);
  return root;



}
  1. 创建了一个 FiberRootNode 类型的 root,这个变量很重要!!!快记到小本本上。
  2. 创建了一个 HostRootFiber
  3. root.current 指向uninitializedFiber
  4. uninitializedFiberstateNode指向 root
  5. 初始化一个initializeUpdateQueue
    1. 这个方法内部生成了一个 queue 的对象
  1. 返回 root

FiberRootNode 长这样:

贴一段源码,感兴趣的打开看看

    function FiberRootNode(containerInfo, tag, hydrate) {
      this.tag = tag;
      this.containerInfo = containerInfo;
      this.pendingChildren = null;
      this.current = null;
      this.pingCache = null;
      this.finishedWork = null;
      this.timeoutHandle = noTimeout;
      this.context = null;
      this.pendingContext = null;
      this.isDehydrated = hydrate;
      this.callbackNode = null;
      this.callbackPriority = NoLane;
      this.eventTimes = createLaneMap(NoLanes);
      this.expirationTimes = createLaneMap(NoTimestamp);
      this.pendingLanes = NoLanes;
      this.suspendedLanes = NoLanes;
      this.pingedLanes = NoLanes;
      this.expiredLanes = NoLanes;
      this.mutableReadLanes = NoLanes;
      this.finishedLanes = NoLanes;
      this.entangledLanes = NoLanes;
      this.entanglements = createLaneMap(NoLanes);

      {
        this.mutableSourceEagerHydrationData = null;
      }

      {
        this.effectDuration = 0;
        this.passiveEffectDuration = 0;
      }

      {
        this.memoizedUpdaters = new Set();
        var pendingUpdatersLaneMap = (this.pendingUpdatersLaneMap = []);

        for (var i = 0; i < TotalLanes; i++) {
          pendingUpdatersLaneMap.push(new Set());
        }
      }

      {
        switch (tag) {
          case ConcurrentRoot:
            this._debugRootType = hydrate ? "hydrateRoot()" : "createRoot()";
            break;

          case LegacyRoot:
            this._debugRootType = hydrate ? "hydrate()" : "render()";
            break;
        }
      }
    }

HostFiberNode

互相指向

 function initializeUpdateQueue(fiber) {
      var queue = {
        baseState: fiber.memoizedState,
        firstBaseUpdate: null,
        lastBaseUpdate: null,
        shared: {
          pending: null,
          interleaved: null,
          lanes: NoLanes,
        },
        effects: null,
      };
      fiber.updateQueue = queue;
    }

生成一个 queue 对象,然后 fiber 的 updateQueue 指向queue。updateQueue是一个更新队列,后续会根据 updateQueue 进行更新。

listenToAllSupportedEvents

这个大概讲一下就是 React 用事件委托的方式将所有事件处理都委托到了 root 的 dom 节点上,自己为了抹平浏览器的差异,实现了一套事件的派发机制,由于比较复杂,计划单独出一篇文章讲一下。

ReactDOMRoot

function ReactDOMRoot(internalRoot) {
      this._internalRoot = internalRoot;
    }

这个方法也很简单,刚才不是生成一个FiberRootNode 吗,这个操作就是将_internalRoot赋值为 FiberRootNode,然后生成一个 ReactDOMRoot 实例。

总结

createRoot 方法生成了一个 FiberRootNode 以及 HostRootFiber,同时通过 current 和 stateNode 互相连接,然后实现了一套自己的事件合成机制,最后返回了一个ReactDOMRoot 对象实例。

Render

render 的代码比较简单,重点关注一下 updateContainer 就 OK

function render(){
   updateContainer(children, root, null, null);
}

updateContainer

这个方法是比较重点的一个方法,分别获取了事件优先级,lane 优先级,标记了 render 以及获取了上下文对象,同时启动了一个 scheduler 的调度。

function updateContainer(){
   var eventTime = requestEventTime();
   var lane = requestUpdateLane(current$1);
   var context = getContextForSubtree(parentComponent);
   var root = scheduleUpdateOnFiber(current$1, lane, eventTime);

}

requestEventTime

function requestEventTime() {
      if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
        // We're inside React, so it's fine to read the actual time.
        return now();
      } // We're not inside React, so we may be in the middle of a browser event.

      if (currentEventTime !== NoTimestamp) {
        // Use the same start time for all updates until we enter React again.
        return currentEventTime;
      } // This is the first update since React yielded. Compute a new start time.

      currentEventTime = now();
      return currentEventTime;
    }
  1. 通过上下文判断,如果处于 render 上下文或 commit 上下文,就返回当前的时间。
  2. 默认兜底返回当前时间。

requestUpdateLane

function requestUpdateLane(fiber) {
      // Special cases
      var mode = fiber.mode;

      if ((mode & ConcurrentMode) === NoMode) {
        return SyncLane;
      } else if (
        (executionContext & RenderContext) !== NoContext &&
        workInProgressRootRenderLanes !== NoLanes
      ) {
        // This is a render phase update. These are not officially supported. The
        // old behavior is to give this the same "thread" (lanes) as
        // whatever is currently rendering. So if you call `setState` on a component
        // that happens later in the same render, it will flush. Ideally, we want to
        // remove the special case and treat them as if they came from an
        // interleaved event. Regardless, this pattern is not officially supported.
        // This behavior is only a fallback. The flag only exists until we can roll
        // out the setState warning, since existing code might accidentally rely on
        // the current behavior.
        return pickArbitraryLane(workInProgressRootRenderLanes);
      }
  1. 通过 mode 进行判断,如果是不是并发更新(React18新特性),返回异步优先级
  2. 否则返回最高优先级。

这块的思想有点复杂,后续降到 lane 的时候会着重讲这一块,现在知道这个函数返回的是最高优先级即可。

总结

updateContainer 函数生成了当前的任务时间以及任务优先级,然后还启动了一个调度任务。

scheduleUpdateOnFiber

这个函数主要的一个任务是通过ensureRootIsScheduled函数实际的去调度任务,准备进入reconcile阶段。

ensureRootIsScheduled

这个函数也是 React 中非常有灵魂的函数之一,下面直接看代码。主要有几个重要的函数,分别是markStarvedLanesAsExpired(用于标记饥饿任务) , scheduleMicrotask(调度异步任务) ,这种任务是不可以被打断的,一条路走到底。scheduleCallback$1(以并发模式调度任务,任务期间可打断,可恢复)

function ensureRootIsScheduled(root, currentTime) {
  markStarvedLanesAsExpired(root, currentTime); // Determine the next lanes to work on, and their priority.

  var nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
  );
  if (newCallbackPriority === SyncLane) {
    // Special case: Sync React callbacks are scheduled on a special
    // internal queue
    if (root.tag === LegacyRoot) {
      if (ReactCurrentActQueue$1.isBatchingLegacy !== null) {
        ReactCurrentActQueue$1.didScheduleLegacyUpdate = true;
      }
      /*KaSong*/ logHook(
        "scheduleCallback",
        "legacySync",
        performSyncWorkOnRoot.name
      );
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      /*KaSong*/ logHook(
        "scheduleCallback",
        "sync",
        performSyncWorkOnRoot.name
      );
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }

    {
      // Flush the queue in a microtask.
      if (ReactCurrentActQueue$1.current !== null) {
        // Inside `act`, use our internal `act` queue so that these get flushed
        // at the end of the current scope even when using the sync version
        // of `act`.
        ReactCurrentActQueue$1.current.push(flushSyncCallbacks);
      } else {
        /*KaSong*/ logHook(
          "scheduleCallback",
          "microtask",
          flushSyncCallbacks.name
        );
        scheduleMicrotask(flushSyncCallbacks);
      }
    }

    newCallbackNode = null;
  } else {
    var schedulerPriorityLevel;

    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediatePriority;
        break;

      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingPriority;
        break;

      case DefaultEventPriority:
        schedulerPriorityLevel = NormalPriority;
        break;

      case IdleEventPriority:
        schedulerPriorityLevel = IdlePriority;
        break;

      default:
        schedulerPriorityLevel = NormalPriority;
        break;
    }
    /*KaSong*/ logHook(
      "scheduleCallback",
      "concurrent",
      performConcurrentWorkOnRoot.name,
      schedulerPriorityLevel
    );
    newCallbackNode = scheduleCallback$1(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root)
    );
  }
}

markStarvedLanesAsExpired

Scheduler会存在饥饿问题,这个函数是用来标记哪些任务处于饥饿,处于饥饿的就不可打断了。这块也放到 Scheduler 章节单独说把。

    function markStarvedLanesAsExpired(root, currentTime) {
      // TODO: This gets called every time we yield. We can optimize by storing
      // the earliest expiration time on the root. Then use that to quickly bail out
      // of this function.
      var pendingLanes = root.pendingLanes;
      var suspendedLanes = root.suspendedLanes;
      var pingedLanes = root.pingedLanes;
      var expirationTimes = root.expirationTimes; // Iterate through the pending lanes and check if we've reached their
      // expiration time. If so, we'll assume the update is being starved and mark
      // it as expired to force it to finish.

      var lanes = pendingLanes;

      while (lanes > 0) {
        var index = pickArbitraryLaneIndex(lanes);
        var lane = 1 << index;
        var expirationTime = expirationTimes[index];

        if (expirationTime === NoTimestamp) {
          // Found a pending lane with no expiration time. If it's not suspended, or
          // if it's pinged, assume it's CPU-bound. Compute a new expiration time
          // using the current time.
          if (
            (lane & suspendedLanes) === NoLanes ||
            (lane & pingedLanes) !== NoLanes
          ) {
            // Assumes timestamps are monotonically increasing.
            expirationTimes[index] = computeExpirationTime(lane, currentTime);
          }
        } else if (expirationTime <= currentTime) {
          // This lane expired
          root.expiredLanes |= lane;
          /*KaSong*/ logHook("expiredLanes", lane);
        }

        lanes &= ~lane;
      }
    }

scheduleMicrotask

首先会根据newCallbackPriority(赛道模型)判断当前更新属于什么更新,如果属于异步更新,会进入scheduleMicrotask调度。下面来看一下它的源码:

var scheduleMicrotask =
  typeof queueMicrotask === "function"
  ? queueMicrotask
  : typeof localPromise !== "undefined"
  ? function (callback) {
    return localPromise
      .resolve(null)
      .then(callback)
      .catch(handleErrorInNextTick);
  }
  : scheduleTimeout; // TODO: Determine the best fallback here.

可以看到这个函数其实就是一个queueMicrotask,但是queueMicrotask可能有些环境没有,所以为了兼容各种环境,写了兼容代码。

function flushSyncCallbacks() {
  if (!isFlushingSyncQueue && syncQueue !== null) {
    // Prevent re-entrance.
    isFlushingSyncQueue = true;
    var i = 0;
    var previousUpdatePriority = getCurrentUpdatePriority();

    try {
      var isSync = true;
      var queue = syncQueue; // TODO: Is this necessary anymore? The only user code that runs in this
      // queue is in the render or commit phases.

      setCurrentUpdatePriority(DiscreteEventPriority);

      for (; i < queue.length; i++) {
        var callback = queue[i];

        do {
          callback = callback(isSync);
        } while (callback !== null);
      }

      syncQueue = null;
      includesLegacySyncCallbacks = false;
    } catch (error) {
      // If something throws, leave the remaining callbacks on the queue.
      if (syncQueue !== null) {
        syncQueue = syncQueue.slice(i + 1);
      } // Resume flushing in the next tick

      scheduleCallback(ImmediatePriority, flushSyncCallbacks);
      throw error;
    } finally {
      setCurrentUpdatePriority(previousUpdatePriority);
      isFlushingSyncQueue = false;
    }
  }

  return null;
}

scheduleMicrotask(flushSyncCallbacks);

将一个调度任务变成微任务 push 进微任务队列。flushSyncCallbacks主要的作用是取出syncQueue里面的调度任务挨个进行调度,可以看到是一个循环走到底,所以是不可打断的。

总结

这个阶段属于 Schedule 调度阶段,在 React18 存在并发更新且有着可中断,可恢复的特性,会存在着饥饿任务的问题,为了解决这个问题,React 会在每次更新之前进行调度任务的标记,然后通过 lane 进行判断,如果是异步更新,走异步更新的任务调度流程,如果是并发更新走并发更新的调度流程。

performSyncWorkOnRoot

这个阶段是reconcile调和阶段,主要的作用就是生成 Fiber 树,然后渲染到页面上。

function performSyncWorkOnRoot(root){
  // ... 
  // 构建离屏的 Fiber 树
  var exitStatus = renderRootSync(root, lanes);
  // 进入 commit 提交阶段
  commitRoot(root);
}

主要有两个函数,第一个是将虚拟 dom 变成 React 中的 Fiber 结构,然后生成 Fiber 树。第二个函数是将构建出来的Fiber 树呈现在页面上。

renderRootSync

这个函数处理了一些边界条件,主要生效的函数是performUnitOfWork,下面来直接分析performUnitOfWork函数。

function performUnitOfWork(){
  // ...
  next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
  // ...
  completeUnitOfWork(unitOfWork);
}

主要就是两部分,第一部分是 beginWork,第二部分是completeUnitOfWork,下面分别看下这两个函数做了什么。

beginWork

function beginWork(){

  // ...
  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; // Resolve outer props first, then resolve inner props.

      var _resolvedProps3 = resolveDefaultProps(_type2, _unresolvedProps3);

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

          if (outerPropTypes) {
            checkPropTypes(
              outerPropTypes,
              _resolvedProps3, // Resolved for outer only
              "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
      );
    }
  }
}

这里主要就是通过workInProgress的 tag 生成不同的 Fiber 节点。然后将生成的节点赋值给 next,用来判断是否遍历到叶子节点。具体怎么生成的后续单独展开讲一下吧~

completeUnitOfWork

function completeUnitOfWork(){
  next = completeWork(current, completedWork, subtreeRenderLanes);
}

switch (workInProgress.tag) {
  case IndeterminateComponent:
  case LazyComponent:
  case SimpleMemoComponent:
  case FunctionComponent:
  case ForwardRef:
  case Fragment:
  case Mode:
  case Profiler:
  case ContextConsumer:
  case MemoComponent:
    bubbleProperties(workInProgress);
    return null;

  case ClassComponent: {
    var Component = workInProgress.type;

    if (isContextProvider(Component)) {
      popContext(workInProgress);
    }

    bubbleProperties(workInProgress);
    return null;
  }

  case HostRoot: {
    var fiberRoot = workInProgress.stateNode;

    popHostContainer(workInProgress);
    popTopLevelContextObject(workInProgress);
    resetWorkInProgressVersions();

    if (fiberRoot.pendingContext) {
      fiberRoot.context = fiberRoot.pendingContext;
      fiberRoot.pendingContext = null;
    }

    if (current === null || current.child === null) {
      // If we hydrated, pop so that we can delete any remaining children
      // that weren't hydrated.
      var wasHydrated = popHydrationState(workInProgress);

      if (wasHydrated) {
        // If we hydrated, then we'll need to schedule an update for
        // the commit side-effects on the root.
        markUpdate(workInProgress);
      } else if (!fiberRoot.isDehydrated) {
        // Schedule an effect to clear this container at the start of the next commit.
        // This handles the case of React rendering into a container with previous children.
        // It's also safe to do for updates too, because current.child would only be null
        // if the previous render was null (so the container would already be empty).
        workInProgress.flags |= Snapshot;
      }
    }

    updateHostContainer(current, workInProgress);
    bubbleProperties(workInProgress);
    return null;
  }

  case HostComponent: {
    popHostContext(workInProgress);
    var rootContainerInstance = getRootHostContainer();
    var type = workInProgress.type;

    // 如果是复用的节点,那么会走到这里,在 这里创建一个更新队列,进行数据的更新。 然后将副作用冒泡之后,直接 return
    // 如果 current= null 说明是新创建的节点,走创建流程即可。
    if (current !== null && workInProgress.stateNode != null) {
      updateHostComponent(
        current,
        workInProgress,
        type,
        newProps,
        rootContainerInstance
      );

      if (current.ref !== workInProgress.ref) {
        markRef(workInProgress);
      }
    } else {
      if (!newProps) {
        if (workInProgress.stateNode === null) {
          throw new Error(
            "We must have new props for new mounts. This error is likely " +
            "caused by a bug in React. Please file an issue."
          );
        } // This can happen when we abort work.

        bubbleProperties(workInProgress);
        return null;
      }

      var currentHostContext = getHostContext(); // TODO: Move createInstance to beginWork and keep it on a context
      // "stack" as the parent. Then append children as we go in beginWork
      // or completeWork depending on whether we want to add them top->down or
      // bottom->up. Top->down is faster in IE11.

      var _wasHydrated = popHydrationState(workInProgress);

      if (_wasHydrated) {
        // TODO: Move this and createInstance step into the beginPhase
        // to consolidate.
        if (
          prepareToHydrateHostInstance(
            workInProgress,
            rootContainerInstance,
            currentHostContext
          )
        ) {
          // If changes to the hydrated node need to be applied at the
          // commit-phase we mark this as such.
          markUpdate(workInProgress);
        }
      } else {
        var instance = createInstance(
          type,
          newProps,
          rootContainerInstance,
          currentHostContext,
          workInProgress
        );
        appendAllChildren(instance, workInProgress, false, false);
        workInProgress.stateNode = instance; // Certain renderers require commit-time effects for initial mount.
        // (eg DOM renderer supports auto-focus for certain elements).
        // Make sure such renderers get scheduled for later work.

        if (
          finalizeInitialChildren(
            instance,
            type,
            newProps,
            rootContainerInstance
          )
        ) {
          markUpdate(workInProgress);
        }
      }

      if (workInProgress.ref !== null) {
        // If there is a ref on a host node we need to schedule a callback
        markRef(workInProgress);
      }
    }

    bubbleProperties(workInProgress);
    return null;
  }

  case HostText: {
    var newText = newProps;

    if (current && workInProgress.stateNode != null) {
      var oldText = current.memoizedProps; // If we have an alternate, that means this is an update and we need
      // to schedule a side-effect to do the updates.

      updateHostText(current, workInProgress, oldText, newText);
    } else {
      if (typeof newText !== "string") {
        if (workInProgress.stateNode === null) {
          throw new Error(
            "We must have new props for new mounts. This error is likely " +
            "caused by a bug in React. Please file an issue."
          );
        } // This can happen when we abort work.
      }

      var _rootContainerInstance = getRootHostContainer();

      var _currentHostContext = getHostContext();

      var _wasHydrated2 = popHydrationState(workInProgress);

      if (_wasHydrated2) {
        if (prepareToHydrateHostTextInstance(workInProgress)) {
          markUpdate(workInProgress);
        }
      } else {
        workInProgress.stateNode = createTextInstance(
          newText,
          _rootContainerInstance,
          _currentHostContext,
          workInProgress
        );
      }
    }

    bubbleProperties(workInProgress);
    return null;
  }

  case SuspenseComponent: {
    popSuspenseContext(workInProgress);
    var nextState = workInProgress.memoizedState;

    {
      if (nextState !== null && nextState.dehydrated !== null) {
        // We might be inside a hydration state the first time we're picking up this
        // Suspense boundary, and also after we've reentered it for further hydration.
        var _wasHydrated3 = popHydrationState(workInProgress);

        if (current === null) {
          if (!_wasHydrated3) {
            throw new Error(
              "A dehydrated suspense component was completed without a hydrated node. " +
              "This is probably a bug in React."
            );
          }

          prepareToHydrateHostSuspenseInstance(workInProgress);
          bubbleProperties(workInProgress);

          {
            if ((workInProgress.mode & ProfileMode) !== NoMode) {
              var isTimedOutSuspense = nextState !== null;

              if (isTimedOutSuspense) {
                // Don't count time spent in a timed out Suspense subtree as part of the base duration.
                var primaryChildFragment = workInProgress.child;

                if (primaryChildFragment !== null) {
                  // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
                  workInProgress.treeBaseDuration -=
                    primaryChildFragment.treeBaseDuration;
                }
              }
            }
          }

          return null;
        } else {
          // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
          // state since we're now exiting out of it. popHydrationState doesn't do that for us.
          resetHydrationState();

          if ((workInProgress.flags & DidCapture) === NoFlags) {
            // This boundary did not suspend so it's now hydrated and unsuspended.
            workInProgress.memoizedState = null;
          } // If nothing suspended, we need to schedule an effect to mark this boundary
          // as having hydrated so events know that they're free to be invoked.
          // It's also a signal to replay events and the suspense callback.
          // If something suspended, schedule an effect to attach retry listeners.
          // So we might as well always mark this.

          workInProgress.flags |= Update;
          bubbleProperties(workInProgress);

          {
            if ((workInProgress.mode & ProfileMode) !== NoMode) {
              var _isTimedOutSuspense = nextState !== null;

              if (_isTimedOutSuspense) {
                // Don't count time spent in a timed out Suspense subtree as part of the base duration.
                var _primaryChildFragment = workInProgress.child;

                if (_primaryChildFragment !== null) {
                  // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
                  workInProgress.treeBaseDuration -=
                    _primaryChildFragment.treeBaseDuration;
                }
              }
            }
          }

          return null;
        }
      }
    }

    if ((workInProgress.flags & DidCapture) !== NoFlags) {
      // Something suspended. Re-render with the fallback children.
      workInProgress.lanes = renderLanes; // Do not reset the effect list.

      if ((workInProgress.mode & ProfileMode) !== NoMode) {
        transferActualDuration(workInProgress);
      } // Don't bubble properties in this case.

      return workInProgress;
    }

    var nextDidTimeout = nextState !== null;
    var prevDidTimeout = false;

    if (current === null) {
      popHydrationState(workInProgress);
    } else {
      var prevState = current.memoizedState;
      prevDidTimeout = prevState !== null;
    }
    // an effect to toggle the subtree's visibility. When we switch from
    // fallback -> primary, the inner Offscreen fiber schedules this effect
    // as part of its normal complete phase. But when we switch from
    // primary -> fallback, the inner Offscreen fiber does not have a complete
    // phase. So we need to schedule its effect here.
    //
    // We also use this flag to connect/disconnect the effects, but the same
    // logic applies: when re-connecting, the Offscreen fiber's complete
    // phase will handle scheduling the effect. It's only when the fallback
    // is active that we have to do anything special.

    if (nextDidTimeout && !prevDidTimeout) {
      var _offscreenFiber = workInProgress.child;
      _offscreenFiber.flags |= Visibility; // TODO: This will still suspend a synchronous tree if anything
      // in the concurrent tree already suspended during this render.
      // This is a known bug.

      if ((workInProgress.mode & ConcurrentMode) !== NoMode) {
        // TODO: Move this back to throwException because this is too late
        // if this is a large tree which is common for initial loads. We
        // don't know if we should restart a render or not until we get
        // this marker, and this is too late.
        // If this render already had a ping or lower pri updates,
        // and this is the first time we know we're going to suspend we
        // should be able to immediately restart from within throwException.
        var hasInvisibleChildContext =
          current === null &&
          workInProgress.memoizedProps.unstable_avoidThisFallback !==
          true;

        if (
          hasInvisibleChildContext ||
          hasSuspenseContext(
            suspenseStackCursor.current,
            InvisibleParentSuspenseContext
          )
        ) {
          // If this was in an invisible tree or a new render, then showing
          // this boundary is ok.
          renderDidSuspend();
        } else {
          // Otherwise, we're going to have to hide content so we should
          // suspend for longer if possible.
          renderDidSuspendDelayIfPossible();
        }
      }
    }

    var wakeables = workInProgress.updateQueue;

    if (wakeables !== null) {
      // Schedule an effect to attach a retry listener to the promise.
      // TODO: Move to passive phase
      workInProgress.flags |= Update;
    }

    bubbleProperties(workInProgress);

    {
      if ((workInProgress.mode & ProfileMode) !== NoMode) {
        if (nextDidTimeout) {
          // Don't count time spent in a timed out Suspense subtree as part of the base duration.
          var _primaryChildFragment2 = workInProgress.child;

          if (_primaryChildFragment2 !== null) {
            // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
            workInProgress.treeBaseDuration -=
              _primaryChildFragment2.treeBaseDuration;
          }
        }
      }
    }

    return null;
  }

  case HostPortal:
    popHostContainer(workInProgress);
    updateHostContainer(current, workInProgress);

    if (current === null) {
      preparePortalMount(workInProgress.stateNode.containerInfo);
    }

    bubbleProperties(workInProgress);
    return null;

  case ContextProvider:
    // Pop provider fiber
    var context = workInProgress.type._context;
    popProvider(context, workInProgress);
    bubbleProperties(workInProgress);
    return null;

  case IncompleteClassComponent: {
    // Same as class component case. I put it down here so that the tags are
    // sequential to ensure this switch is compiled to a jump table.
    var _Component = workInProgress.type;

    if (isContextProvider(_Component)) {
      popContext(workInProgress);
    }

    bubbleProperties(workInProgress);
    return null;
  }

  case SuspenseListComponent: {
    popSuspenseContext(workInProgress);
    var renderState = workInProgress.memoizedState;

    if (renderState === null) {
      // We're running in the default, "independent" mode.
      // We don't do anything in this mode.
      bubbleProperties(workInProgress);
      return null;
    }

    var didSuspendAlready =
      (workInProgress.flags & DidCapture) !== NoFlags;
    var renderedTail = renderState.rendering;

    if (renderedTail === null) {
      // We just rendered the head.
      if (!didSuspendAlready) {
        // This is the first pass. We need to figure out if anything is still
        // suspended in the rendered set.
        // If new content unsuspended, but there's still some content that
        // didn't. Then we need to do a second pass that forces everything
        // to keep showing their fallbacks.
        // We might be suspended if something in this render pass suspended, or
        // something in the previous committed pass suspended. Otherwise,
        // there's no chance so we can skip the expensive call to
        // findFirstSuspended.
        var cannotBeSuspended =
          renderHasNotSuspendedYet() &&
          (current === null || (current.flags & DidCapture) === NoFlags);

        if (!cannotBeSuspended) {
          var row = workInProgress.child;

          while (row !== null) {
            var suspended = findFirstSuspended(row);

            if (suspended !== null) {
              didSuspendAlready = true;
              workInProgress.flags |= DidCapture;
              cutOffTailIfNeeded(renderState, false); // If this is a newly suspended tree, it might not get committed as
              // part of the second pass. In that case nothing will subscribe to
              // its thenables. Instead, we'll transfer its thenables to the
              // SuspenseList so that it can retry if they resolve.
              // There might be multiple of these in the list but since we're
              // going to wait for all of them anyway, it doesn't really matter
              // which ones gets to ping. In theory we could get clever and keep
              // track of how many dependencies remain but it gets tricky because
              // in the meantime, we can add/remove/change items and dependencies.
              // We might bail out of the loop before finding any but that
              // doesn't matter since that means that the other boundaries that
              // we did find already has their listeners attached.

              var newThenables = suspended.updateQueue;

              if (newThenables !== null) {
                workInProgress.updateQueue = newThenables;
                workInProgress.flags |= Update;
              } // Rerender the whole list, but this time, we'll force fallbacks
              // to stay in place.
              // Reset the effect flags before doing the second pass since that's now invalid.
              // Reset the child fibers to their original state.

              workInProgress.subtreeFlags = NoFlags;
              resetChildFibers(workInProgress, renderLanes); // Set up the Suspense Context to force suspense and immediately
              // rerender the children.

              pushSuspenseContext(
                workInProgress,
                setShallowSuspenseContext(
                  suspenseStackCursor.current,
                  ForceSuspenseFallback
                )
              ); // Don't bubble properties in this case.

              return workInProgress.child;
            }

            row = row.sibling;
          }
        }

        if (renderState.tail !== null && now() > getRenderTargetTime()) {
          // We have already passed our CPU deadline but we still have rows
          // left in the tail. We'll just give up further attempts to render
          // the main content and only render fallbacks.
          workInProgress.flags |= DidCapture;
          didSuspendAlready = true;
          cutOffTailIfNeeded(renderState, false); // Since nothing actually suspended, there will nothing to ping this
          // to get it started back up to attempt the next item. While in terms
          // of priority this work has the same priority as this current render,
          // it's not part of the same transition once the transition has
          // committed. If it's sync, we still want to yield so that it can be
          // painted. Conceptually, this is really the same as pinging.
          // We can use any RetryLane even if it's the one currently rendering
          // since we're leaving it behind on this node.

          workInProgress.lanes = SomeRetryLane;
        }
      } else {
        cutOffTailIfNeeded(renderState, false);
      } // Next we're going to render the tail.
    } else {
      // Append the rendered row to the child list.
      if (!didSuspendAlready) {
        var _suspended = findFirstSuspended(renderedTail);

        if (_suspended !== null) {
          workInProgress.flags |= DidCapture;
          didSuspendAlready = true; // Ensure we transfer the update queue to the parent so that it doesn't
          // get lost if this row ends up dropped during a second pass.

          var _newThenables = _suspended.updateQueue;

          if (_newThenables !== null) {
            workInProgress.updateQueue = _newThenables;
            workInProgress.flags |= Update;
          }

          cutOffTailIfNeeded(renderState, true); // This might have been modified.

          if (
            renderState.tail === null &&
            renderState.tailMode === "hidden" &&
            !renderedTail.alternate &&
            !getIsHydrating() // We don't cut it if we're hydrating.
          ) {
            // We're done.
            bubbleProperties(workInProgress);
            return null;
          }
        } else if (
          // The time it took to render last row is greater than the remaining
          // time we have to render. So rendering one more row would likely
          // exceed it.
          now() * 2 - renderState.renderingStartTime >
          getRenderTargetTime() &&
          renderLanes !== OffscreenLane
        ) {
          // We have now passed our CPU deadline and we'll just give up further
          // attempts to render the main content and only render fallbacks.
          // The assumption is that this is usually faster.
          workInProgress.flags |= DidCapture;
          didSuspendAlready = true;
          cutOffTailIfNeeded(renderState, false); // Since nothing actually suspended, there will nothing to ping this
          // to get it started back up to attempt the next item. While in terms
          // of priority this work has the same priority as this current render,
          // it's not part of the same transition once the transition has
          // committed. If it's sync, we still want to yield so that it can be
          // painted. Conceptually, this is really the same as pinging.
          // We can use any RetryLane even if it's the one currently rendering
          // since we're leaving it behind on this node.

          workInProgress.lanes = SomeRetryLane;
        }
      }

      if (renderState.isBackwards) {
        // The effect list of the backwards tail will have been added
        // to the end. This breaks the guarantee that life-cycles fire in
        // sibling order but that isn't a strong guarantee promised by React.
        // Especially since these might also just pop in during future commits.
        // Append to the beginning of the list.
        renderedTail.sibling = workInProgress.child;
        workInProgress.child = renderedTail;
      } else {
        var previousSibling = renderState.last;

        if (previousSibling !== null) {
          previousSibling.sibling = renderedTail;
        } else {
          workInProgress.child = renderedTail;
        }

        renderState.last = renderedTail;
      }
    }

    if (renderState.tail !== null) {
      // We still have tail rows to render.
      // Pop a row.
      var next = renderState.tail;
      renderState.rendering = next;
      renderState.tail = next.sibling;
      renderState.renderingStartTime = now();
      next.sibling = null; // Restore the context.
      // TODO: We can probably just avoid popping it instead and only
      // setting it the first time we go from not suspended to suspended.

      var suspenseContext = suspenseStackCursor.current;

      if (didSuspendAlready) {
        suspenseContext = setShallowSuspenseContext(
          suspenseContext,
          ForceSuspenseFallback
        );
      } else {
        suspenseContext =
          setDefaultShallowSuspenseContext(suspenseContext);
      }

      pushSuspenseContext(workInProgress, suspenseContext); // Do a pass over the next row.
      // Don't bubble properties in this case.

      return next;
    }

    bubbleProperties(workInProgress);
    return null;
  }

  case ScopeComponent: {
    break;
  }

  case OffscreenComponent:
  case LegacyHiddenComponent: {
    popRenderLanes(workInProgress);
    var _nextState = workInProgress.memoizedState;
    var nextIsHidden = _nextState !== null;

    if (current !== null) {
      var _prevState = current.memoizedState;
      var prevIsHidden = _prevState !== null;

      if (
        prevIsHidden !== nextIsHidden &&
        newProps.mode !== "unstable-defer-without-hiding" && // LegacyHidden doesn't do any hiding — it only pre-renders.
        workInProgress.tag !== LegacyHiddenComponent
      ) {
        workInProgress.flags |= Visibility;
      }
    }

    if (
      !nextIsHidden ||
      (workInProgress.mode & ConcurrentMode) === NoMode
    ) {
      bubbleProperties(workInProgress);
    } else {
      // Don't bubble properties for hidden children unless we're rendering
      // at offscreen priority.
      if (includesSomeLane(subtreeRenderLanes, OffscreenLane)) {
        bubbleProperties(workInProgress);

        {
          // Check if there was an insertion or update in the hidden subtree.
          // If so, we need to hide those nodes in the commit phase, so
          // schedule a visibility effect.
          if (
            workInProgress.tag !== LegacyHiddenComponent &&
            workInProgress.subtreeFlags & (Placement | Update) &&
            newProps.mode !== "unstable-defer-without-hiding"
          ) {
            workInProgress.flags |= Visibility;
          }
        }
      }
    }

    return null;
  }
}

主要是通过completeWork进行生成,源码里可以看到熟悉的代码,也是一个 switch 循环,通过判断tag 进入不同的组件生成逻辑,然后根据不同组件的特性,生成对应的真实的dom节点初始化 style 以及 className 等。

commitRoot

上述的阶段主要是根据虚拟 dom 生成真实的Fiber树,但是此时 Fiber 树还存在于内存中,需要把 Fiber 树渲染到屏幕上。同时React 提供了很多副作用 hook,比如 useEffect,useLayoutEffect,useState。有些 hook 也需要在 commit 阶段进行处理。

function commitRoot(){
  // ...
  commitRootImpl(root, previousUpdateLanePriority);
  // ...
}

function commitRootImpl(){
  // ...
  var shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
          root,
          finishedWork
        );
   commitMutationEffects(root, finishedWork, lanes);
   commitLayoutEffects(finishedWork, root, lanes);
}

commit 阶段主要分为三个阶段,分别是beforeMutationEffectscommitMutationEffectscommitLayoutEffects。这三个阶段主要是处理了组件中的副作用(useEffect、useLayoutEffect、useState等) ,同时进行缓存树的切换,屏幕中就渲染出了真实的 dom。具体每个阶段分别做了什么,后续章节会讲到。

总结

该阶段主要有两个小阶段,分别是:

  1. 根据虚拟 dom 生成 Fiber 树,同时根据Fiber 去生成对应 Fiber 对应的真实 dom 节点,处理 style 和 className 属性等。
  2. 通过生成的 Fiber 树,去处理组件中的副作用,然后将真实的 dom 节点渲染到页面上。

最后总结

React 的大概工作流程就是上诉这些关键节点,总结下来总共分为以下几个部分:

  1. 初始化相关,包括初始化 FiberRootNode 和 HostFiber 以及对事件进行处理。
  2. Schedule 调度阶段,进行任务的调度。
  3. reconcile阶段,根据虚拟 dom 生成对应的Fiber 节点,并根据不同的组件类型生成对应的真实 dom 节点。
  4. commit 阶段,处理副作用和将内存中构建的 Fiber 树渲染到屏幕上。

后续一些具体的流程我会分为不同的文章分别进行讲解,敬请期待~