React Suspense 源码解读

React Suspense 源码解读

halo大家好,我是132,今天给大家带来一篇 Suspense 的源码解读的文章

前置知识

在读源码之前,先写用例:

      const OtherComponent = React.lazy(() => {
        return new Promise(resolve =>
          setTimeout(
            () =>
              resolve({
                default: () => <div>hello world</div>
              }),
            1000
          )
        )
      })
      function App() {
        return (
          <React.Suspense fallback={<div>Loading...</div>}>
            <OtherComponent />
          </React.Suspense>
        )
      }
      ReactDOM.render(<App />, document.getElementById('root'))

这个用例中,异步组件使用 promise 模拟,然后读源码之前,我们要知道

这个 promise 肯定是在一个地方 throw 出去,再另一个地方 catch 到

源码解读

找一个切入点,直接搜 .then,因为既然在 throw 了,那么肯定要 then 一下

    try {
      return beginWork$1(current$$1, unitOfWork, expirationTime);
    } catch (originalError) {
      if (originalError !== null && typeof originalError === 'object' && typeof originalError.then === 'function') {
        // 继续抛
        throw originalError;
      }

第一次是在 beginWork 的时候 catch 到,然后继续抛出

beginWork 就是更新组件,如果这个时候继续抛出去,说明这里没有被打断任务

下一个

function initializeLazyComponentType(lazyComponent) {
  if (lazyComponent._status === Uninitialized) {
    lazyComponent._status = Pending;
    var ctor = lazyComponent._ctor;
    var thenable = ctor();
    lazyComponent._result = thenable;
    thenable.then(function (moduleObject) {
      if (lazyComponent._status === Pending) {
        var defaultExport = moduleObject.default;

        lazyComponent._status = Resolved;
        lazyComponent._result = defaultExport;
      }
    }, function (error) {
      if (lazyComponent._status === Pending) {
        lazyComponent._status = Rejected;
        lazyComponent._result = error;
      }
    });
  }
}

第二次是 lazy 的组件,这个组件做的事情很简单,就是把 resolve 的结果保存一下,没什么用,继续

if (value !== null && typeof value === 'object' && typeof value.then === 'function') {

    var thenable = value;
    checkForWrongSuspensePriorityInDEV(sourceFiber);
    var hasInvisibleParentBoundary = hasSuspenseContext(suspenseStackCursor.current, InvisibleParentSuspenseContext); // Schedule the nearest Suspense to re-render the timed out view.

    var _workInProgress = returnFiber;

    do {
      if (_workInProgress.tag === SuspenseComponent && shouldCaptureSuspense(_workInProgress, hasInvisibleParentBoundary)) {
        var thenables = _workInProgress.updateQueue;

        if (thenables === null) {
          var updateQueue = new Set();
          updateQueue.add(thenable);
          _workInProgress.updateQueue = updateQueue;
        } else {
          thenables.add(thenable);
        } 

        if ((_workInProgress.mode & BlockingMode) === NoMode) {
          _workInProgress.effectTag |= DidCapture; 

          sourceFiber.effectTag &= ~(LifecycleEffectMask | Incomplete);

          if (sourceFiber.tag === ClassComponent) {
            var currentSourceFiber = sourceFiber.alternate;

            if (currentSourceFiber === null) {

              sourceFiber.tag = IncompleteClassComponent;
            } else {

              var update = createUpdate(Sync, null);
              update.tag = ForceUpdate;
              enqueueUpdate(sourceFiber, update);
            }
          } 

          sourceFiber.expirationTime = Sync;

          return;
        }

找到了,重点就在这里,我们看到,它把这个 thenable,放到 updateQueue 里了

updateQueue 一看就是用来更新的

到这里,我们已经搞懂了 suspense 的执行机制

1. 事先 throw 2. 在 completeWork 之前 catch 住,然后添加到 updateQueue 里

然后 updateQueue 会批量更新……

也就是说,和我一开始想的不一样,它没有打断任务,他只是加到 updateQueue 里面,然后触发更新,rerender

但是虽然是同样是 updateQueue,这个 updateQueue 里面装着的是 promise,更新起来肯定也不一样,我们继续看

thenables.forEach(function (thenable) {
      var retry = resolveRetryThenable.bind(null, finishedWork, thenable);

      if (!retryCache.has(thenable)) {
        if (enableSchedulerTracing) {
          if (thenable.__reactDoNotTraceInteractions !== true) {
            retry = unstable_wrap(retry);
          }
        }

        retryCache.add(thenable);
        thenable.then(retry, retry);
      }
    });

终于找到执行的地方了……这个循环其实只是执行了 resolveRetryThenable 函数传了俩参数,但是这不是我想要的

我想要的是,重新调度的函数,所以继续

function retryTimedOutBoundary(boundaryFiber, retryTime) {
  if (retryTime === NoWork) {
    var suspenseConfig = null; 

    var currentTime = requestCurrentTimeForUpdate();
    retryTime = computeExpirationForFiber(currentTime, boundaryFiber, suspenseConfig);
  } // TODO: Special case idle priority?


  var root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime);

  if (root !== null) {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, retryTime);
  }
}

呜呜呜终于找到了……………………源码解读到此结束

总结

好像也没啥总结的,总的来说,比想象中简单得多,甚至比 fre 之前的实现要简单……

就是 catch 到 promise 然后添加到更新队列,然后 then 的时候 rerender 组件

好的那问题来了,为什么既然组件 rerender 了,不会抖屏呢??

因为 Suspense 不是普通组件……是的你没有看错,它是个标记组件,只执行一次,拿到 fallback 和 children,然后从第二次开始,它就只是换指针

而且这个标记不仅不是组件,重点它还一直在那里占位

然后 rerender 的时候,走到它这里,它只需要把 child 指针变化一下,就没什么事了

编辑于 2019-12-20

文章被以下专栏收录