【React源码笔记16】- suspense

React | 2020-08-25 22:33:02 919次 1次

Suspense 的核心概念与 error boundaries 非常相似,error boundaries 在 React 16 中引入,允许在应用程序内的任何位置捕获未捕获的异常,然后在组件树中展示跟错误信息相关的组件。以同样的方式,Suspense 组件从其子节点捕获任何抛出的Promises,不同之处在于对于 Suspense 我们不必使自定义组件充当边界,Suspense 组件就是那个边界;而在 error boundary中,我们需要为边界组件定义(componentDidCatch)方法--摘抄此处


一、节点创建

使用的 suspence 组件就是 REACT_SUSPENSE_TYPE(Symbol.for('react.suspense')),就是指定的一种特殊节点类型,和 div 是一个意思。

在 createFiberFromTypeAndProps 方法创建节点类型时,找到 REACT_SUSPENSE_TYPE 类型,执行 createFiberFromSuspense 创建 suspence 类型的 fiber 节点:

export function createFiberFromSuspense(
  pendingProps: any,
  mode: TypeOfMode,
  expirationTime: ExpirationTime,
  key: null | string,
) {
  const fiber = createFiber(SuspenseComponent, pendingProps, key, mode);

  fiber.type = REACT_SUSPENSE_TYPE;
  fiber.elementType = REACT_SUSPENSE_TYPE;

  fiber.expirationTime = expirationTime;
  return fiber;
}

 回顾:节点类型的创建,在初始只会创建出根节点的 fiber,后续的创建在 beginWork 入口,进入 reconcile 过程,会判断节点可复用性,然后不能复用的就通过 createFiberFromTypeAndProps 创建新节点。


二、render阶段

suspense 组件类型执行大致过程如下:

                    beginWork 
                     |
                    updateSuspenseComponent
                         |
                         ...
                         mountChildFibers
                         ...
                         |
                        reconcileChildFibers

在 completeWork 中针对 SuspenseComponent 组件,执行的操作主要是进行是否需要更新标记:

workInProgress.effectTag |= Update;

举例:

export default () => (
  <Suspense maduation={1000} fallback="loading data">
    <SuspenseComp /> // 这个组件内一些逻辑会 throw promise
  </Suspense>
)

beginWork 中的 SuspenseComponent 分支在 fallback 数据渲染之前会执行两次,原因在下面 handleError 方法中提及。


第一次执行

    workInProgress.child = mountChildFibers  就是单纯的创建出子节点


第二次

            fallbackChildFragment  -- fallback 对应的内容

        

            primaryChildFragment  -- suspense 节点创建的  return-->   同时 wip(suspense) 还保留着

        

            创建关系

            fallbackChildFragment.return = workInProgress;

            primaryChildFragment.sibling = fallbackChildFragment; 

            workInProgress.child = primaryChildFragment;

        

            return fallbackChildFragment


两次流程之后,界面初始出现传入的 fallback 的内容,然后进入对子节点的处理过程,等待组件的数据加载成功后被重新调度,渲染新数据。



suspense 子节点处理

通过 suspense 包裹的组件中,如果这个子组件有 throw 的逻辑,则会进入 catch 的过程:

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

在这里抛出错误之后,回退到 performSyncWorkOnRoot 中:

function performSyncWorkOnRoot (){
...
    do {
      try {
        workLoopSync();
        break;
      } catch (thrownValue) {
        handleError(root, thrownValue);
      }
    } while (true);
...
}

继续执行 handleError :

function handleError(root, thrownValue) {
  do {
    try {
      ...
      // 继续执行
      throwException(...);
      // 这里完成时   会将wip设置为自己的父节点  也就是 suspense 节点
      workInProgress = completeUnitOfWork(workInProgress);
    } catch (yetAnotherThrownValue) {
      ...
      continue
    }
    // Return to the normal work loop.
    return;
  } while (true);
}

错误机制中的 completeUnitOfWork 会将 wip 设置为父节点,在这里的场景是回到 suspense 节点,所以在 beginWork 那里断点时会发现连续两次都是进入 SuspenseComponent 分支。

继续执行 throwException,这里会将抛出的 promise 放入子组件的 updateQueue

function throwException(
  root: FiberRoot,
  returnFiber: Fiber,
  sourceFiber: Fiber,
  value: mixed,
  renderExpirationTime: ExpirationTime,
) {
  ...
  if (
    value !== null &&
    typeof value === 'object' &&
    typeof value.then === 'function'
  ) {
    // This is a thenable.
    const thenable: Thenable = (value: any);

    ...
    do {
      if (
        workInProgress.tag === SuspenseComponent &&
        shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary)
      ) {
        // 一个 set 结构存储在 updateQueue
        const thenables: Set<Thenable> = (workInProgress.updateQueue: any);
        if (thenables === null) {
          const updateQueue = (new Set(): any);
          updateQueue.add(thenable);
          // 第一次新增
          workInProgress.updateQueue = updateQueue;
        } else {
          // 追加
          thenables.add(thenable);
        }
           ...
          // 同步设置
          sourceFiber.expirationTime = Sync;

          return;
        }

        ...
      workInProgress = workInProgress.return;
    } while (workInProgress !== null);
    
  }
  ...
}

接下来就是如何执行这些更新。


三、commit阶段

commitWork 方法中进入 SuspenseComponent ,最终要重新调度:

commitWork 
      |
    SuspenseComponent 
          |
       attachSuspenseRetryListeners
            |
          thenables.forEach
              |
              retryTimedOutBoundary
                |
               retryTimedOutBoundary
                |
                ensureRootIsScheduled 开始调度

主要是在这个阶段拿到这些抛出的 promise 对象并执行:

function attachSuspenseRetryListeners(finishedWork: Fiber) {
  ...
  const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
  if (thenables !== null) {
   // 置空队列
    finishedWork.updateQueue = null;
    let retryCache = finishedWork.stateNode;
    // 缓存优化
    if (retryCache === null) {
      retryCache = finishedWork.stateNode = new PossiblyWeakSet();
    }
    thenables.forEach(thenable => {
      // 拿到 调度的 回调,等待组件中 resolve 之后 重新调度
      let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
      if (!retryCache.has(thenable)) {
        retryCache.add(thenable);
        // resolve 之后执行,传入调度的回调
        thenable.then(retry, retry);
      }
    });
  }
}
function retryTimedOutBoundary(...) {
  ...
  ensureRootIsScheduled(root);
  ...
}

等待组件中数据加载成功重新发起调度,再次进入 beginWork--SuspenseComponent,此时直接做的事情就是 reconcileChildFibers 过程,并清空 memoizedState

...
primaryChild = reconcileChildFibers()

workInProgress.memoizedState = null;
return workInProgress.child = primaryChild;
...

此时在 completeWork 中针对 SuspenseComponent 组件,会对之前渲染的 fallback 组件标记删除,对新的渲染数据标记更新。

suspense 的理解目前就这么多吧,其中的很多细节没有深入研究,其背后产生的原理依据 algebrac effects(代数效应)。


四、lazy原理

通过上面的理解,再来看 lazy 就很简单了。

声明

export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
  let lazyType = {
    $$typeof: REACT_LAZY_TYPE,
    _ctor: ctor,
    // React uses these fields to store the result.
    _status: -1,
    _result: null,
  };

  return lazyType;
}

执行

beginWork 中对应 mountLazyComponent

let Component = readLazyComponentType(elementType);
// type 和 tag 一并修改掉
workInProgress.type = Component;
const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));

核心是 readLazyComponentType 方法来读取传入的组件状态是否加载完毕:

export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
  initializeLazyComponentType(lazyComponent);
  // 没有加载完毕  直接抛出 和自己写抛出错误是一样的
  if (lazyComponent._status !== Resolved) {
    throw lazyComponent._result;
  }
  // 加载完毕就返回一个正确的结果 
  return lazyComponent._result;
}

看下这个 _result 怎么来的:

export function initializeLazyComponentType(
  lazyComponent: LazyComponent<any>,
): void {
  if (lazyComponent._status === Uninitialized) {
    lazyComponent._status = Pending;
    // 这个就是传入的 () => import(...) 编译后就是一个thenable 对象
    const ctor = lazyComponent._ctor;
    const thenable = ctor();
    // 赋值
    lazyComponent._result = thenable;
    thenable.then(
      moduleObject => {
        if (lazyComponent._status === Pending) {
          const defaultExport = moduleObject.default;
          lazyComponent._status = Resolved;
          // 成功
          lazyComponent._result = defaultExport;
        }
      },
      error => {
        if (lazyComponent._status === Pending) {
          lazyComponent._status = Rejected;
          // 失败
          lazyComponent._result = error;
        }
      },
    );
  }
}

lazy 原理和直接在组件中抛出一个 thenable 一样。

1人赞

分享到: