一、前面的话
在react中为我们提供了一个非常有用的组件,那就是<Suspense/>
,他可以包裹一个异步组件,当这个异步组件处于pending
状态的时候会展示一个过渡的UI,当异步组件处于resolved
状态的时候会显示真正的UI,我们来看一下如何使用Suspense
和 react提供的lazy
结合起来达到异步加载状态的目的
1 | JSX复制代码import { lazy , Suspense } from 'react'; |
它的效果如下:
接下来我们就来一步一步看一下这究竟是怎么做到这一点的!
二、lazy懒加载组件
要先从lazy
这个api开始说起,根据上面的内容,LazyComponent
是由lazy
这个调用返回的结果,它能够被直接渲染,在没有Suspense加持的情况下,也是可以异步渲染出组件的,如下所示
1 | js复制代码const LazyComponent = React.lazy(() => import('./LazyComponent.js')); |
效果如下:
我们看一下lazy
的实现原理
1 | js复制代码function lazy(ctor) { |
根据我提供的注释我们可以看到,其实lazy就是返回了一个REACT_LAZY_TYPE
类型的ReactElement节点,并且用一个状态机记录了当前的这个节点处于什么样的状态,引用者传进来的函数引用
这里要重点分析一下()=> import('xxxx')
,import('xxx')
是ES6提供的一种异步加载模块的方式,他会返回一个Promise
,因此可以使用.then
获取异步加载所得到的数据
接下来我们看一下lazyInitializer
的实现
1 | js复制代码function lazyInitializer(payload) { |
经过分析我们会发现lazyInitializer
会根据payload
的状态来采取不同的行为:
- 如果是初始化状态
在这里它会执行用户传进来的函数,得到一个Promise,并且开始调用这个Promise,得到异步的结果,并且标记自己处于Pedning
状态,然后抛出错误 - 如果
Resolved
的状态那么就判断这个得到的值是否合法,合法就返回给调用者
但不用担心,此时我们分析了这个函数如果执行的话,直到现在用户只是调用了lazy
,这个函数还没到执行的时候,现在用户仅仅只是得到了一个lazy
类型的ReactElement
类型的节点
而真正让这个函数执行得地方还是得在render
阶段,当调和到lazy
类型的节点的时候,会执行mountLazyComponent
1 | js复制代码function mountLazyComponent( |
根据我们上面的分析,在调用lazyInitializer
函数的时候,如果是第一次调用,会进入第一种情况,状态还是初始化的状态,因此会执行异步函数,得到一个正在调用的Promise
,然后会调用.then
获取它的结果,然后将其保存在payload
中,然后将状态置为Pending
,最后抛出错误,所以后面的逻辑都不用看了,第一次在这里会抛出错误,阻塞后面的代码,整个render阶段被迫提前结束
如果提前结束了render阶段
,那么后面该如何运行呢?
原来当lazy
类型的render过程中,准确的来说应该是beginWork
中因为第一次执行init
函数导致抛出错误,阻塞了后面的过程,react会提前结束beginWork
环节,然后react会捕获这个错误,还记得那个workLoop
么?它是这样子的:
1 | js复制代码do { |
因此实际上react并不会因为抛出了这个错误就完蛋了,甚至这个错误是刻意抛出的,为的就是在handleError
中捕获它,然后做不同的逻辑处理
在handleError
中会基于抛出错误的节点开始提前进入completeWork
,然后将整棵树标记为未完成的状态,最后因为上层函数拿到这个是否调和完整棵树的状态,决定是否进行commit
流程
结果就是这棵树没有完成,因此不会进行commit
阶段,第一次render
因为lazy
类型组件的存在就这样匆匆结束了
那现相信大家和我有同样的问题,那react是怎么重启render
的呢? ,因为在平常开发中lazy组件也是可以渲染出组件的呀,所以一定有一个重启render的过程才能做到。
原来在handleError
的过程中有一个这样的过程,如果发现了抛出错误的参数是一个Promise
的话,就会认定他是一个懒加载的情况,然后做出重启的操作,正巧我们init
抛出错误的信息刚好是一个Promise
,而重启的操作如下:
1 | js复制代码function throwException(value){// 这个value就是错误信息 |
那么attachPingListener
发生了什么呢?
1 | js复制代码简化一下就是这样的 |
看到了吗,如果这个Promise
的状态一旦从Pending
状态变成其他状态,就会执行这个pingSuspendedRoot
,它里面就藏着重新发起调度的ensureRootIsScheduled
逻辑,然后会把更新流程重走一遍,从render
到commit
,最终就呈现出了UI。
这里需要注意的一点就是当重启的这一次render
阶段其实也会遇到lazy
类型的节点,那它还会抛出错误吗?
其实是不会的,因为这一次来到lazy
节点时,执行的init
函数会发现状态已经被修改为Resolved
的状态了, 会直接返回结果,然后返回的结果通常来说是一个组件,就是异步加载的组件,把它作为子组件再继续构建fiber树
1 | js复制代码function mountLazyComponent( |
至此lazy类型的组件原理我们就分析完了,它其实利用的是
react
强大的异常捕获机制,以及Promise
灵敏的状态机来实现的,我画个图给大家总结一下
三、Suspense原理
当我们分析了上面的lazy
类型的组件之后Suspense
就很好学习了
Suspense
本质上就是一个ReactElement
类型的对象,没啥好说的;关键要看在render
阶段react如何处理这种类型的fiber组件的,下面一起来看一下
初始化
在初始化时仅仅只是创建了fiber,然后继续调和子组件,由于他的组件就是lazy
类型的组件,因此还是回到上面的逻辑,lazy
组件会抛错啊,因此第一次render
阶段终止了,但是在handleError
处理错误的时候,因为它被Suspense
包裹着,因此逻辑会有不同
1 | js复制代码function throwException(value){ // 这个value就是错误信息 |
实际上这个逻辑和lazy还是一样的,就是监听Promise
的状态,在Promise
有结果的时候再重启一次render
,这一点是一致的,通过这个机制可以确保当异步组件加载完成后react运行时能够知道在此时更新页面,呈现出最新的UI
但是我们知道从效果上来看,在有Suspense
包裹的时候,在异步组件加载过程中应该会立马展示一个过渡UI,也就是fallback
对应的参数,而需要做到这一点需要发起一次调度啊,也就是说需要经历一个render
+commit
才能做到啊
过渡fiber节点
原来这一切的一切在第一次render的时候就有准备了,在第一次构建fiber树的时候,假设我们的组件是下面这样的
1 | js复制代码<Suspense fallback={...}> |
那么实际上在构建fiber
树的时候会有这样的fiber
结构
因此它并不是每个组件对应一个fiber
节点,Suspense
对应的实际上是有2个fiber节点,当我们知道这一点之后,当做了监听完的动作之后,我们再回到外层看一下,会执行一个completeUnitOfWork
的动作,这个动作实际上在上面我们讲到的只有lazy
的情况也会执行,只不过在只有lazy
组件的时候它会一直调和到root
节点,导致workInProgress
为null
,而在有Suspense
会表现的有所不同
因为这是由于出现了异常导致的completeUnitOfWork
,因此不会走正常的completeWork
,而是走unwindWork(current, completedWork);
在unwindWork
向上归并的时候,如果遇到有Suspense
节点的情况会保留这个Suspense
节点的信息,实际上就是不会一直往上走到root节点,而是将workInProgress
指向这个Suspense
的fiber节点,然后就退出completeWork
的流程,然后我们再来看一下render
阶段的引擎函数
1 | js复制代码do { |
handleError
结束后还会继续接着render,在上面提到的只有lazy组件的情况下,因为workInProgress
不存在所以直接break
退出了render
流程,而在Suspense
组件存在的情况下,会继续从这个Suspense
开始继续render
这一次render
就会直接调和fallback
的内容,这一次根本就不会遇到lazy
类型的组件了,直到整棵fiber树调和完成,然后接着正常进行commit
流程,所以用户看到的就是带有fallback
的UI界面
等到异步组件重新加载完成后,会重新执行一次render
+ commit
构建出含有异步组件的界面
小结:
以上就是Suspense,主要是react在拥有Suspense
类型的组件的过程中做了处理,使其多了一次默认的render
+commit
的流程,从而使用户能够看到含有过渡状态的UI,我依然用一个图来给大家总结一下
四、最后的话
本篇文章中,我们从源码的角度分析了Suspense
和lazy
组件的原理,并且梳理了它们的流程,接下来我们继续学习react的新特性,吃透react。
后面的文章我们会依然会深入剖析react的源码,学习react的设计思想,如果你也对react相关技术感兴趣请订阅我的《深入理解react》专栏,笔者争取至少月更一篇,我们一起进步,有帮助的话希望朋友点个赞支持下,多谢多谢!
本文转载自: 掘金