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 结构存储在 updateQueueconst 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 一样。

更多 react 源码请点击这里更多源码

suspense源码分析相关推荐

  1. 推荐一个 React 技术揭秘的项目,自顶向下的 React 源码分析

    大家好,我是你们的 猫哥,那个不喜欢吃鱼.又不喜欢喵 的超级猫 ~ just-react 这本书的宗旨是打造一本严谨.易懂的 React 源码分析教程. 为了达到这个目标,在行文上,本书会遵循: 不预 ...

  2. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  3. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  4. SpringBoot-web开发(二): 页面和图标定制(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...

  5. SpringBoot-web开发(一): 静态资源的导入(源码分析)

    目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...

  6. Yolov3Yolov4网络结构与源码分析

    Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...

  7. ViewGroup的Touch事件分发(源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...

  8. View的Touch事件分发(二.源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,先来看简单的View的touch事件分发. 主要分析View的dispatchTouchEvent()方法和onTou ...

  9. MyBatis原理分析之四:一次SQL查询的源码分析

    上回我们讲到Mybatis加载相关的配置文件进行初始化,这回我们讲一下一次SQL查询怎么进行的. 准备工作 Mybatis完成一次SQL查询需要使用的代码如下: Java代码   String res ...

  10. [转]slf4j + log4j原理实现及源码分析

    slf4j + log4j原理实现及源码分析 转载于:https://www.cnblogs.com/jasonzeng888/p/6051080.html

最新文章

  1. 解决Android图库不识别.nomedia的问题
  2. python_面向对象
  3. GIS中的拓扑关系和ArcGIS中的拓扑
  4. 深入理解java虚拟机(全章节完整)
  5. 阿里云天池平台官方出品!从0到1层层拆解天池大赛赛题 | 文末送书
  6. java 类无法实例_Java无法从类实例访问类方法
  7. 张勇谈组织架构调整:领导者要善于“从后排把人往前拔”
  8. [python、flask] - POST请求
  9. Silverlight 2 Beta 1版本缺陷列表
  10. arm汇编指令集_1. 从0开始学ARM安装Keil MDK uVision集成开发环境
  11. django-模型类管理器-create方法-models属性
  12. 把设备分享给每个Docker Container
  13. 弘辽科技:开庭了辛巴被诉“永久封号”
  14. 图解谷歌地球使用入门、谷歌地球COM API 开发入门、谷歌地球使用的初步协议分析
  15. 第一次使用公有云需要注意啥
  16. 互换性与测量技术基础知识点总结
  17. 幸福三月丨盐城北大青鸟女神节快乐!
  18. 网安、ctf常用网址
  19. ftp服务器备份手机文件,ftp服务器文件自动备份
  20. 关于访问接口时前后端都报错:404 (Not Found)

热门文章

  1. laaS平台架构介绍
  2. 一种最低级的按键状态机
  3. TD-LTE无线帧中Tf和Ts是什么?
  4. 三级等保 MySQL8.0.24审计日志功能开启
  5. 什么是地理信息系统(GIS)?
  6. jq 数字转中文数字_Jquery 字符串转数字
  7. php pecl_http,安装 PHP 的 PECL HTTP 扩展
  8. html 设置不同字体,在html中怎么设置一行字两个不同样式的字体
  9. 手机百度云如何打开doc
  10. Microsoft Visual Studio - 代码格式化设置项