内容简介:前端应用达到一定规模时(比如bundle size以MB为单位),势必面临代码拆分的强需求:运行时再去动态加载一些代码块,比如非首屏业务组件,以及日历、地址选择、评论等重磅组件最方便的动态加载方式是还处于stage3,但已经被各大打包工具(
一.代码拆分
前端应用达到一定规模时(比如bundle size以MB为单位),势必面临代码拆分的强需求:
Code-Splitting is a feature supported by bundlers like Webpack and Browserify (via factor-bundle) which can create multiple bundles that can be dynamically loaded at runtime.
运行时再去动态加载一些代码块,比如非首屏业务组件,以及日历、地址选择、评论等重磅组件
最方便的动态加载方式是还处于stage3,但已经被各大打包工具( webpack 、 rollup 等)广泛支持的 tc39/proposal-dynamic-import :
import('../components/Hello').then(Hello => { console.log(<Hello />); });
相当于( setTimeout
模拟异步加载组件):
new Promise(resolve => setTimeout(() => resolve({ // 来自另一个文件的函数式组件 default: function render() { return <div>Hello</div> } }), 3000 ) ).then(({ default: Hello }) => { // 拿到组件了,然后呢? console.log(<Hello />); });
当然,拆出去只是前一半, 拿到手的组件怎样渲染出来 则是后一半
二.条件渲染
不依赖框架支持的话,可以通过条件渲染的方式把动态组件挂上去:
class MyComponent extends Component { constructor() { super(); this.state = {}; // 动态加载 import('./OtherComponent').then(({ default: OtherComponent }) => { this.setState({ OtherComponent }); }); } render() { const { OtherComponent } = this.state; return ( <div> {/* 条件渲染 */} { OtherComponent && <OtherComponent /> } </div> ); } }
此时对应的用户体验是,首屏 OtherComponent
还没回来,过了一会儿布局抖了一下冒出来了,存在几个问题:
-
对父组件有侵入性(
state.OtherComponent
) -
布局抖动体验不佳
框架不提供支持的话,这种侵入性似乎不可避免(总得有组件去做条件渲染,就总要添这些显示逻辑)
抖动的话,加loading解决,但容易出现 遍地天窗 (好几处loading都在转圈)的体验问题,所以loading一般不单针对某个原子组件,而是组件树上的一块区域整体显示loading(这块区域里可能含有本能立即显示的组件),这种场景下,loading需要加到祖先组件上去,并且显示逻辑变得很麻烦(可能要等好几个动态组件都加载完毕才隐藏)
所以,想要避免条件渲染带来的侵入性,只有靠框架提供支持,这正是 React.lazy API的由来。而为了解决后两个问题,我们希望把loading显示逻辑放到祖先组件上去,也就是 Suspense 的作用
三.React.lazy
React.lazy()
把条件渲染细节挪到了框架层, 允许把动态引入的组件当普通组件用
,优雅地消除了这种侵入性:
const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( <div> <OtherComponent /> </div> ); }
动态引入的 OtherComponent
在用法上与普通组件完全一致,只是存在引入方式上的差异(把 import
换成 import()
并用 React.lazy()
包起来):
import OtherComponent from './OtherComponent'; // 改为动态加载 const OtherComponent = React.lazy(() => import('./OtherComponent'));
要求 import()
必须返回一个会 resolve
ES Module的Promise,并且这个ES Module里 export default
了合法的React组件:
// ./OtherComponent.jsx export default function render() { return <div>Other Component</div> }
类似于:
const OtherComponent = React.lazy(() => new Promise(resolve => setTimeout(() => resolve( // 模拟ES Module { // 模拟export default default: function render() { return <div>Other Component</div> } } ), 3000 ) ));
P.S. React.lazy()
暂时还不支持SSR,建议用 React Loadable
四.Suspense
React.Suspense
也是一种虚拟组件(类似于 Fragment
,仅用作类型标识),用法如下:
const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> </div> ); }
Suspense
子树中只要存在还没回来的Lazy组件,就走 fallback
指定的内容。 这不正是可以提升到任意祖先级的loading吗?
You can place the Suspense component anywhere above the lazy component. You can even wrap multiple lazy components with a single Suspense component.
Suspense
组件可以放在(组件树中)Lazy组件上方的任意位置,并且下方可以有多个Lazy组件。对应到loading场景,就是这两种能力:
-
支持loading提升
-
支持loading聚合
4行业务代码就能实现loading最佳实践, 相当漂亮的特性
P.S.没被 Suspense
包起来的Lazy组件会报错:
Uncaught Error: A React component suspended while rendering, but no fallback UI was specified.
算是从框架层对用户体验提出了强要求
五.具体实现
function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> { return { $$typeof: REACT_LAZY_TYPE, _ctor: ctor, // 组件加载状态 _status: -1, // 加载结果,Component or Error _result: null, }; }
记下传入的组件加载器,返回带(加载)状态的Lazy组件描述对象:
// _status取值 export const Pending = 0; export const Resolved = 1; export const Rejected = 2;
初始值 -1
被摸过之后会变成 Pending
,具体如下:
// beginWork() // mountLazyComponent() // readLazyComponentType() function readLazyComponentType(lazyComponent) { lazyComponent._status = Pending; const ctor = lazyComponent._ctor; const thenable = ctor(); 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; } }, ); lazyComponent._result = thenable; throw thenable; }
注意最后的 throw
,没错, 为了打断子树渲染,这里直接抛错出去
,路子有些狂野:
function renderRoot(root, isYieldy) { do { try { workLoop(isYieldy); } catch (thrownValue) { // 处理错误 throwException(root, returnFiber, sourceFiber, thrownValue, nextRenderExpirationTime); // 找到下一个工作单元,Lazy父组件或兄弟组件 nextUnitOfWork = completeUnitOfWork(sourceFiber); continue; } } while (true); }
最后会被 长达230行
的 throwException
兜住:
function throwException() { if ( value !== null && typeof value === 'object' && typeof value.then === 'function' ) { // This is a thenable. const thenable: Thenable = (value: any); // 接下来大致做了4件事 // 1.找出祖先所有Suspense组件的最早超时时间(有可能已超时) // 2.找到最近的Suspense组件,找不到的话报那个错 // 3.监听Pending组件,等到不Pending了立即调度渲染最近的Suspense组件 // Attach a listener to the promise to "ping" the root and retry. let onResolveOrReject = retrySuspendedRoot.bind( null, root, workInProgress, sourceFiber, pingTime, ); if (enableSchedulerTracing) { onResolveOrReject = Schedule_tracing_wrap(onResolveOrReject); } thenable.then(onResolveOrReject, onResolveOrReject); // 4.挂起最近的Suspense组件子树,不再往下渲染 } }
P.S.注意,第3步 thenable.then(render, render)
在 React.lazy(() => resolvedImportPromise)
的场景 并不会闪fallback内容
,这与浏览器任务机制有关,具体见macrotask与microtask
(收集结果时)回到最近的Suspense组件,发现有Pending后代就会去渲染 fallback
:
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { if ( primaryChildExpirationTime !== NoWork && primaryChildExpirationTime >= renderExpirationTime ) { // The primary children have pending work. Use the normal path // to attempt to render the primary children again. return updateSuspenseComponent( current, workInProgress, renderExpirationTime, ); } } function updateSuspenseComponent( current, workInProgress, renderExpirationTime, ) { // 渲染fallback const nextFallbackChildren = nextProps.fallback; const primaryChildFragment = createFiberFromFragment( null, mode, NoWork, null, ); const fallbackChildFragment = createFiberFromFragment( nextFallbackChildren, mode, renderExpirationTime, null, ); next = fallbackChildFragment; return next; }
以上,差不多就是整个过程了(能省略的细节都略掉了)
六.意义
We’ve built a generic way for components to suspend rendering while they load async data, which we call suspense. You can pause any state update until the data is ready, and you can add async loading to any component deep in the tree without plumbing all the props and state through your app and hoisting the logic. On a fast network, updates appear very fluid and instantaneous without a jarring cascade of spinners that appear and disappear. On a slow network, you can intentionally design which loading states the user should see and how granular or coarse they should be, instead of showing spinners based on how the code is written. The app stays responsive throughout.
初衷是为logading场景提供优雅的通用解决方案,允许组件树挂起等待(即延迟渲染)异步数据,意义在于:
-
符合最佳用户体验:
-
避免布局抖动(数据回来之后冒出来一块内容),当然,这是加loading或skeleton的好处,与Suspense关系不很大
-
区别对待不同网络环境(数据返回快的话压根不会出现loading)
-
-
优雅:不用再为了加子树loading而提升相关状态和逻辑,从状态提升与组件封装性的抑郁中解脱了
-
灵活:loading组件与异步组件(依赖异步数据的组件)之间没有组件层级关系上的强关联,能够灵活控制loading粒度
-
通用:支持等待异步数据时显示降级组件(loading只是一种最常见的降级策略,fallback到缓存数据甚至广告也不是不可以)
参考资料
以上所述就是小编给大家介绍的《React Suspense》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- React Suspense
- React Suspense 尝鲜
- 深度理解 React Suspense(附源码解析)
- 基于React.Suspense和React.lazy的前端性能优化
- React Concurrent Mode 之 Suspense 实践
- React Suspense 提供 Redux 的替代方案
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
大象无形:虚幻引擎程序设计浅析
罗丁力、张三 / 电子工业出版社 / 2017-4 / 65
《大象无形:虚幻引擎程序设计浅析》以两位作者本人在使用虚幻引擎过程中的实际经历为参考,包括三大部分:使用C++语言进行游戏性编程、了解虚幻引擎本身底层结构与渲染结构、编写插件扩展虚幻引擎。提供了不同于官方文档内容的虚幻引擎相关细节和有效实践。有助于读者一窥虚幻引擎本身设计的精妙之处,并能学习到定制虚幻引擎所需的基础知识,实现对其的按需定制。 《大象无形:虚幻引擎程序设计浅析》适合初步了解虚幻......一起来看看 《大象无形:虚幻引擎程序设计浅析》 这本书的介绍吧!