专栏首页前端干货和生活感悟React源码解析之renderRoot概览

React源码解析之renderRoot概览

前言: 由于个人能力和精力有限,外加 renderRoot 的内容太多,里面的每个 function 都可以单独拿出来讲,所以本篇文章的目的是帮助大家了解 renderRoot 大概做了哪些事,而这些事具体的内容,以后会一个个单独拿出来解析。

在哪里用到React源码解析之scheduleWork(上)中:

export function scheduleUpdateOnFiber(){  
  xxx  
  xxx  
  xxx  
  if (expirationTime === Sync) {    
    if( 第一次render ){      
      xxx  
      //初始化root,调用workLoop进行循环单元更新      
      let callback = renderRoot(root, Sync, true);    
    }else{      
      xxx    
    }  
  }
}

一、renderRoot 主要的作用: (1) 调用 workLoop 进行循环单元更新 (2) 捕获错误并进行处理 (3) 走完流程后,根据workInProgressRoot的不同状态来进行不同的操作

源码:

function renderRoot(
  root: FiberRoot,
  expirationTime: ExpirationTime,
  isSync: boolean,
): SchedulerCallback | null {
  invariant(
    (executionContext & (RenderContext | CommitContext)) === NoContext,
    'Should not already be working.',
  );

  if (enableUserTimingAPI && expirationTime !== Sync) {
    const didExpire = isSync;
    stopRequestCallbackTimer(didExpire);
  }

  if (root.firstPendingTime < expirationTime) {
    // If there's no work left at this expiration time, exit immediately. This
    // happens when multiple callbacks are scheduled for a single root, but an
    // earlier callback flushes the work of a later one.
    return null;
  }

  if (isSync && root.finishedExpirationTime === expirationTime) {
    // There's already a pending commit at this expiration time.
    // TODO: This is poorly factored. This case only exists for the
    // batch.commit() API.
    return commitRoot.bind(null, root);
  }
  // 听说是useEffect的调用
  flushPassiveEffects();

  // If the root or expiration time have changed, throw out the existing stack
  // and prepare a fresh one. Otherwise we'll continue where we left off.

  /*nextRoot =》 workInProgressRoot*/
  /*nextRenderExpirationTime =》 renderExpirationTime*/
  //workInProgressRoot 指接下来要更新的节点
  //renderExpirationTime 指接下来更新节点的过期时间
  //意思就是当前要更新的节点并非是队列中要更新的节点,也就是说被新的高优先级的任务给打断了
  if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
    //重置调度队列,并从root节点(新的高优先级的节点)开始调度
    /*resetStack <=> prepareFreshStack */
    prepareFreshStack(root, expirationTime);
    //将调度优先级高的interaction加入到interactions中
    startWorkOnPendingInteractions(root, expirationTime);
  } 
  //应该是当已经接收一个低优先级的要更新的节点时所进行的操作
  else if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
    // We could've received an update at a lower priority while we yielded.
    // We're suspended in a delayed state. Once we complete this render we're
    // just going to try to recover at the last pending time anyway so we might
    // as well start doing that eagerly.
    // Ideally we should be able to do this even for retries but we don't yet
    // know if we're going to process an update which wants to commit earlier,
    // and this path happens very early so it would happen too often. Instead,
    // for that case, we'll wait until we complete.
    if (workInProgressRootHasPendingPing) {
      // We have a ping at this expiration. Let's restart to see if we get unblocked.
      prepareFreshStack(root, expirationTime);
    } else {
      const lastPendingTime = root.lastPendingTime;
      if (lastPendingTime < expirationTime) {
        // There's lower priority work. It might be unsuspended. Try rendering
        // at that level immediately, while preserving the position in the queue.
        return renderRoot.bind(null, root, lastPendingTime);
      }
    }
  }

  // If we have a work-in-progress fiber, it means there's still work to do
  // in this root.
  if (workInProgress !== null) {
    const prevExecutionContext = executionContext;
    executionContext |= RenderContext;
    let prevDispatcher = ReactCurrentDispatcher.current;
    if (prevDispatcher === null) {
      // The React isomorphic package does not include a default dispatcher.
      // Instead the first renderer will lazily attach one, in order to give
      // nicer error messages.
      prevDispatcher = ContextOnlyDispatcher;
    }
    ReactCurrentDispatcher.current = ContextOnlyDispatcher;
    let prevInteractions: Set<Interaction> | null = null;
    if (enableSchedulerTracing) {
      prevInteractions = __interactionsRef.current;
      __interactionsRef.current = root.memoizedInteractions;
    }
    //绑定 currentFiber,也标志着开始执行 workloop
    startWorkLoopTimer(workInProgress);

    // TODO: Fork renderRoot into renderRootSync and renderRootAsync
    //如果是同步的话
    if (isSync) {
      //如果更新时间是异步的话
      if (expirationTime !== Sync) {
        // An async update expired. There may be other expired updates on
        // this root. We should render all the expired work in a
        // single batch.

        //将所有过期的时间分批次处理
        const currentTime = requestCurrentTime();
        if (currentTime < expirationTime) {
          // Restart at the current time.
          executionContext = prevExecutionContext;
          resetContextDependencies();
          ReactCurrentDispatcher.current = prevDispatcher;
          if (enableSchedulerTracing) {
            __interactionsRef.current = ((prevInteractions: any): Set<
              Interaction,
            >);
          }
          return renderRoot.bind(null, root, currentTime);
        }
      }
    } else {
      // Since we know we're in a React event, we can clear the current
      // event time. The next update will compute a new event time.

      //清除currentEventTime
      currentEventTime = NoWork;
    }

    do {
      try {
        //执行每个节点的更新
        if (isSync) {
          workLoopSync();
        } else {
          //判断是否需要继续调用performUnitOfWork
          workLoop();
        }

        break;
      }
      //捕获异常,并处理
      catch (thrownValue)
      {
        // Reset module-level state that was set during the render phase.
        //重置状态
        resetContextDependencies();
        resetHooks();

        const sourceFiber = workInProgress;
        /*nextUnitOfWork <=> sourceFiber*/
        //如果sourceFiber是存在的,那么 React 可以判断错误的原因
        //如果sourceFiber是不存在的,说明是未知错误
        if (sourceFiber === null || sourceFiber.return === null) {
          // Expected to be working on a non-root fiber. This is a fatal error
          // because there's no ancestor that can handle it; the root is
          // supposed to capture all errors that weren't caught by an error
          // boundary.
          //重置调度队列,并从root节点(新的高优先级的节点)开始调度
          prepareFreshStack(root, expirationTime);
          executionContext = prevExecutionContext;
          //抛出错误
          throw thrownValue;
        }
        //记录error被捕获前,渲染所花费的时间
        //这样可以避免在渲染挂起(暂停)的情况下,Profiler的时间会不准确

        //Profiler:测量渲染一个 React 应用多久渲染一次以及渲染一次的“代价”。
        //它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。
        if (enableProfilerTimer && sourceFiber.mode & ProfileMode) {
          // Record the time spent rendering before an error was thrown. This
          // avoids inaccurate Profiler durations in the case of a
          // suspended render.
          stopProfilerTimerIfRunningAndRecordDelta(sourceFiber, true);
        }
        //获取父节点
        const returnFiber = sourceFiber.return;
        //抛出可预期的错误
        throwException(
          root,
          returnFiber,
          sourceFiber,
          thrownValue,
          renderExpirationTime,
        );
        //完成对sourceFiber的渲染,
        //但是因为已经是报错的,所以不会再渲染sourceFiber的子节点了
        workInProgress = completeUnitOfWork(sourceFiber);
      }
    } while (true);

    executionContext = prevExecutionContext;
    //重置状态
    resetContextDependencies();
    ReactCurrentDispatcher.current = prevDispatcher;
    if (enableSchedulerTracing) {
      __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
    }
    //如果仍有正在进程里的任务
    if (workInProgress !== null) {
      // There's still work left over. Return a continuation.
      //停止计时
      stopInterruptedWorkLoopTimer();
      if (expirationTime !== Sync) {
        //开始调度callback的标志
        startRequestCallbackTimer();
      }
      //绑定 this
      return renderRoot.bind(null, root, expirationTime);
    }
  }

  // We now have a consistent tree. The next step is either to commit it, or, if
  // something suspended, wait to commit it after a timeout.
  // 至此,保证了 fiber 树的每个节点的状态都是一致的。接下来会执行 commit 步骤/或者是又有新的任务被挂起了,等待挂起结束再去 commit
  stopFinishedWorkLoopTimer();

  root.finishedWork = root.current.alternate;
  root.finishedExpirationTime = expirationTime;
  //判断当前节点是否被阻止commit
  const isLocked = resolveLocksOnRoot(root, expirationTime);
  //如果有,则退出
  if (isLocked) {
    // This root has a lock that prevents it from committing. Exit. If we begin
    // work on the root again, without any intervening updates, it will finish
    // without doing additional work.
    return null;
  }

  // Set this to null to indicate there's no in-progress render.
  //将workInProgressRoot以告诉 react 没有正在 render 的进程
  workInProgressRoot = null;
  //根据workInProgressRoot的不同状态来进行不同的操作
  switch (workInProgressRootExitStatus) {
    case RootIncomplete: {
      invariant(false, 'Should have a work-in-progress.');
    }
    // Flow knows about invariant, so it compains if I add a break statement,
    // but eslint doesn't know about invariant, so it complains if I do.
    //对下面 eslint 注释的解释,可不看
    // eslint-disable-next-line no-fallthrough
    case RootErrored: {
      // An error was thrown. First check if there is lower priority work
      // scheduled on this root.
      const lastPendingTime = root.lastPendingTime;
      if (lastPendingTime < expirationTime) {
        // There's lower priority work. Before raising the error, try rendering
        // at the lower priority to see if it fixes it. Use a continuation to
        // maintain the existing priority and position in the queue.
        return renderRoot.bind(null, root, lastPendingTime);
      }
      if (!isSync) {
        // If we're rendering asynchronously, it's possible the error was
        // caused by tearing due to a mutation during an event. Try rendering
        // one more time without yiedling to events.
        prepareFreshStack(root, expirationTime);
        scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
        return null;
      }
      // If we're already rendering synchronously, commit the root in its
      // errored state.
      return commitRoot.bind(null, root);
    }
    case RootSuspended: {
      // We have an acceptable loading state. We need to figure out if we should
      // immediately commit it or wait a bit.

      // If we have processed new updates during this render, we may now have a
      // new loading state ready. We want to ensure that we commit that as soon as
      // possible.
      const hasNotProcessedNewUpdates =
        workInProgressRootLatestProcessedExpirationTime === Sync;
      if (hasNotProcessedNewUpdates && !isSync) {
        // If we have not processed any new updates during this pass, then this is
        // either a retry of an existing fallback state or a hidden tree.
        // Hidden trees shouldn't be batched with other work and after that's
        // fixed it can only be a retry.
        // We're going to throttle committing retries so that we don't show too
        // many loading states too quickly.
        let msUntilTimeout =
          globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
        // Don't bother with a very short suspense time.
        if (msUntilTimeout > 10) {
          if (workInProgressRootHasPendingPing) {
            // This render was pinged but we didn't get to restart earlier so try
            // restarting now instead.
            prepareFreshStack(root, expirationTime);
            return renderRoot.bind(null, root, expirationTime);
          }
          const lastPendingTime = root.lastPendingTime;
          if (lastPendingTime < expirationTime) {
            // There's lower priority work. It might be unsuspended. Try rendering
            // at that level.
            return renderRoot.bind(null, root, lastPendingTime);
          }
          // The render is suspended, it hasn't timed out, and there's no lower
          // priority work to do. Instead of committing the fallback
          // immediately, wait for more data to arrive.
          root.timeoutHandle = scheduleTimeout(
            commitRoot.bind(null, root),
            msUntilTimeout,
          );
          return null;
        }
      }
      // The work expired. Commit immediately.
      return commitRoot.bind(null, root);
    }
    case RootSuspendedWithDelay: {
      if (!isSync) {
        // We're suspended in a state that should be avoided. We'll try to avoid committing
        // it for as long as the timeouts let us.
        if (workInProgressRootHasPendingPing) {
          // This render was pinged but we didn't get to restart earlier so try
          // restarting now instead.
          prepareFreshStack(root, expirationTime);
          return renderRoot.bind(null, root, expirationTime);
        }
        const lastPendingTime = root.lastPendingTime;
        if (lastPendingTime < expirationTime) {
          // There's lower priority work. It might be unsuspended. Try rendering
          // at that level immediately.
          return renderRoot.bind(null, root, lastPendingTime);
        }

        let msUntilTimeout;
        if (workInProgressRootLatestSuspenseTimeout !== Sync) {
          // We have processed a suspense config whose expiration time we can use as
          // the timeout.
          msUntilTimeout =
            expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) - now();
        } else if (workInProgressRootLatestProcessedExpirationTime === Sync) {
          // This should never normally happen because only new updates cause
          // delayed states, so we should have processed something. However,
          // this could also happen in an offscreen tree.
          msUntilTimeout = 0;
        } else {
          // If we don't have a suspense config, we're going to use a heuristic to
          // determine how long we can suspend.
          const eventTimeMs: number = inferTimeFromExpirationTime(
            workInProgressRootLatestProcessedExpirationTime,
          );
          const currentTimeMs = now();
          const timeUntilExpirationMs =
            expirationTimeToMs(expirationTime) - currentTimeMs;
          let timeElapsed = currentTimeMs - eventTimeMs;
          if (timeElapsed < 0) {
            // We get this wrong some time since we estimate the time.
            timeElapsed = 0;
          }

          msUntilTimeout = jnd(timeElapsed) - timeElapsed;

          // Clamp the timeout to the expiration time.
          // TODO: Once the event time is exact instead of inferred from expiration time
          // we don't need this.
          if (timeUntilExpirationMs < msUntilTimeout) {
            msUntilTimeout = timeUntilExpirationMs;
          }
        }

        // Don't bother with a very short suspense time.
        if (msUntilTimeout > 10) {
          // The render is suspended, it hasn't timed out, and there's no lower
          // priority work to do. Instead of committing the fallback
          // immediately, wait for more data to arrive.
          root.timeoutHandle = scheduleTimeout(
            commitRoot.bind(null, root),
            msUntilTimeout,
          );
          return null;
        }
      }
      // The work expired. Commit immediately.
      return commitRoot.bind(null, root);
    }
    case RootCompleted: {
      // The work completed. Ready to commit.
      if (
        !isSync &&
        workInProgressRootLatestProcessedExpirationTime !== Sync &&
        workInProgressRootCanSuspendUsingConfig !== null
      ) {
        // If we have exceeded the minimum loading delay, which probably
        // means we have shown a spinner already, we might have to suspend
        // a bit longer to ensure that the spinner is shown for enough time.
        const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
          workInProgressRootLatestProcessedExpirationTime,
          expirationTime,
          workInProgressRootCanSuspendUsingConfig,
        );
        if (msUntilTimeout > 10) {
          root.timeoutHandle = scheduleTimeout(
            commitRoot.bind(null, root),
            msUntilTimeout,
          );
          return null;
        }
      }
      return commitRoot.bind(null, root);
    }
    default: {
      invariant(false, 'Unknown root exit status.');
    }
  }
}

解析: (1) 前面的三个ifflushPassiveEffects()不用去看

(2) 如果render的时候,有个更高优先级的任务插进来要执行的话,需执行prepareFreshStack,重置调度队列,并从root节点(新的高优先级的节点)开始调度

(3) else if (workInProgressRootExitStatus === RootSuspendedWithDelay)的部分不看,应该是当已经接收一个低优先级的要更新的节点时所进行的操作

(4) 当该节点上面仍有未执行的任务时,执行startWorkLoopTimer(),绑定currentFiber,也标志着开始执行workloop

(5) if (isSync) { xxx } else { xxx }可不看

(6) 调用workLoop()/workLoopSync()进行循环单元的更新,这个方法以后会详细解析

(7) 捕获异常,并处理,重点是throwException方法,用来抛出可预期的错误,以后解析

(8) 根据workInProgressRoot的不同状态来进行不同的操作,也就是当root被处理完,根据它的状态,来判断后续是进行commit、阻止commit并重新render、等待一段时间再去commit、还是报错。

GitHub: https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberWorkLoop.js

本文分享自微信公众号 - webchen(webchen1995)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • React Hook form 表单校验

    原生input,form多好啊。也可以结合ant 等的表单进行使用,更多的校验规则也可以指定别的库。

    我已经洗完澡了
  • React全局消息组件

    我已经洗完澡了
  • React - 入门:前导、环境、目录、原理

    版本v16之后,对其底层的核心算法进行了重构,引入了底层的新引擎React Fiber(16版本以后的react)

    xing.org1^
  • 完全理解 redux(从零实现一个 redux)

    记得开始接触 react 技术栈的时候,最难理解的地方就是 redux。全是新名词:reducer、store、dispatch、middleware 等等,我...

    石燕平
  • 看完这篇,你也能把 React Hooks 玩出花

    React v16.7.0-alpha 中第一次引入了 Hooks 的概念,在 v16.8.0 版本被正式发布。React Hooks 在 React 中只是对...

    石燕平
  • 没钱买KEGG怎么办?REACTOME开源通路更强大

    之前搜集免费生物AI插图时简单提到了通路数据库Reactome(https://reactome.org/), 那些精美的生物插图只能算是该数据库附赠的小礼品,...

    生信宝典
  • React-Hook实现数据获取管理和滚动获取

    本来是想要暴露出的handlerSetUrl 通过改变url进行下一页的获取,但是老是出现错误。

    我已经洗完澡了
  • 前端代码打包优化 (一)

    前端资源打包在每个项目中都会有涉及,从如何优化构建速度和优化构建输出代码来说明一些方法。

    随心助手
  • 使用ReactHook和context实现登录状态的共享

    使用react hook 和应用上下文context进行一个自定义的hook的开发。

    我已经洗完澡了
  • 大前端时代,浅谈JavaScript开发重型跨平台应用以及架构

    不知不觉,九月就要过去,由于这个月工作上,被C++折磨得很难受,而且其他时间都在学习,所以没有时间写文章,好在技术提升很大。今天准备好好谈一谈重型应用的架构以及...

    Nealyang

扫码关注云+社区

领取腾讯云代金券