开始
我们都遇到过这样一种场景:当渲染一个组件需要时间比较长的时候,页面就会白屏,直到组件渲染完成才结束。通常我们的做法是...没有什么好的办法
这里指的渲染组件,不是指组件完整显示数据。完整显示数据还需要请求接口,等待接口的请求时间。而我们讨论的不是这个。我们讨论的是jsx -> FunctionComponent -> fiber DOM -> 真实DOM的这段时间,请求数据是发生在真实DOM挂载之后的事情。
如果组件不是很复杂,那么可能只需要一瞬间,也可能长到被用户感知到。
就像下面这段代码:
const Title = () => {
const sum = fib(40);
return <div>sum: {sum}</div>;
};
// 斐波那契
const fib = (n) => {
if (n <= 2) return 1;
return fib(n - 1) + fib(n - 2);
};
在渲染DOM之前需要先计算斐波那契数列的前40个数字之和(大概需要1.3S)
父组件需要1.3S才能将其渲染出来。如果这个组件占据页面的大部分显示,那就会出现长时间的空白。这不太好
我们有什么办法可以解决这个问题吗?
父组件设置一个变量来检测组件是否加载完成,如果没有加载完成,就显示另外一个DOM?
嗯这是一个思路。
但实际情况是不可行的。因为一个组件生成虚拟DOM的前提是它的所有子组件已经生成了虚拟DOM,所以当一个组件渲染时间很长的话,会阻塞整个fiber树的生成。更别提用什么loading组件来替换了。
suspence的基本使用方法
还好有办法。Suspence官方介绍的一个API,可以实现当子组件没有加载完成的时候,替换成另外一个暂时的组件。这个暂时的组件可以是loading,也可以是骨架屏。
我们来看看如何使用的
除了上面创建的Title组件,我们还需要一个父组件
const Title = React.lazy(() => import("./Title"));
function App() {
return (
<div className="App">
<Suspense fallback={<div>loading ... </div>}>
<Title />
</Suspense>
</div>
);
}
首先会出现这样的:
过了一秒之后,结果就出来了:
用法很简单,效果也符合预期:这是实现链接:测试Suspence的使用场景
通过React.lazy把Title动态加载进来,并用Suspence包裹,Suspence接收一个属性:fallback。fallback的作用是当Suspence子组件没有渲染完成的时候,就渲染传入fallback的组件
Suspence的实现原理
捕获子组件抛出的异常,如果这个异常是promise,就替换成了fallback组件。当这个promise状态成功之后,就渲染出子组件
我们利用这个特性,来试试异步转同步的写法😁
首先准备好一个提供数据的API
const getName = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("zora");
}, 1000);
});
};
一秒后得到结果
然后再写一个包装promise的函数
const handleFetch = (promise) => {
let value = null;
let reason = null;
let status = "pending";
//get value of promise
const result = promise
.then((res) => {
value = res;
status = "fulfilled";
})
.catch((err) => {
reason = err;
status = "rejected";
});
return () => {
if (status === "fulfilled") {
return value;
} else if (status === "rejected") {
return reason;
} else if (status === "pending") {
throw result;
}
};
};
handleFetch函数接收一个promise作为参数,如果这个promise的状态还是pending,就抛出一个新的promise给Suspence。如果状态确定了,就返回promise中的值
下面是一个获取name的组件
import { getName, handleFetch } from "./service";
const namePromise = handleFetch(getName);
const Name = () => {
const name = namePromise();
return <div>my grilfirend's name is: {name}</div>;
};
组件会输出namePromise的返回结果
在App组件中加载Name组件
function App() {
const Title = lazy(() => import("./Title"));
return (
<div className="App">
<Suspense fallback={<div>loading ... </div>}>
<Title />
</Suspense>
<Suspense fallback={<div>wait a name...</div>}>
<Name />
</Suspense>
</div>
);
}
这里并没有使用lazy来加载Name组件
这个是效果:
我们可以看到,当获取name的请求没有返回结果,Suspence就会渲染fallback中的组件
OK,我们来捋一捋过程
-
首先Name组件会发出获取name的请求,并且在组件内部调用
namePromise(),当请求没有返回结果的时候,这个方法会throw 一个promise -
Suspence会catch子组件的异常,如果识别到异常时promise,就会渲染fallback,而不会渲染Name组件。这样也就避免了Name直接输出
namePromise()返回结果的错误。当然,如果异常不是promise,Suspence会继续向外抛出
-
之后Suspence会调用catch到的promise的then方法,一旦promise的状态确定,就像Name组件渲染出来,而这时,
namePromise()return的值是一个可输出值--'zora' -
完毕
这样的写法看起来很酷
Name组件中发起请求的动作不要放在函数组件内部,会重复发起请求。借用hooks也无济于事。 因为该组件会被反复地挂载、卸载。不过发起请求放在父组件,是个不错的选择
这是实现链接:借助Suspence异步变同步
总结
- Suspence的官方推荐场景
- 借助Suspence的特性,将发起请求的异步写法改成同步