在前面一篇文中,我们知道,如果直接使用 use 获取未直接 resolve 的 Promise 中的值,会抛出一个异常。
1
const
_api3
=
()
=>
{
2
return new
Promise
(
resolve
=>
{
3
resolve
({ value:
'_api3 '
})
4
})
5
}
6
7
// bad: get an error
8
const
result
=
use
(
_api3
())
在实践中,大多数情况都是这种并没有直接得到 Promise resolve 的结果状态,那我们应该怎么办呢?这个时候我们可以利用 Suspense 来解决这个问题。
Suspense 可以捕获 use 无法读取到数据时抛出的异常,然后此时会在页面上渲染回退组件 fallback
1
<
Suspense
fallback
={
<
Skeleton
/>
}
>
2
<
Message
promise
={
promise
}
/>
3
</
Suspense
>
接下来看一个简单的演示案例。在这个例子中,为了让 Suspense 捕获更小范围的组件,我们单独定义了一个子组件 Message
来使用 use 获取 promise 中的数据。
10
import
{ Suspense }
from
'react '
20
import
Message
from
'./message '
30
import
Skeleton
from
'components/ui/skeleton '
40
import
{ getMessage }
from
'./api '
50
60
export default function
Page
()
{
70
const
promise
=
getMessage
()
80
return
(
90
<
Suspense
fallback
={
<
Skeleton
/>
}
>
10
<
Message
promise
={
promise
}
/>
11
</
Suspense
>
12
)
13
}
14
在开发中更常见的场景是使用 use 读取异步 promise,主要就是接口请求 。
1
<
Suspense
fallback
={
<
Loading
/>
}
>
2
<
Albums
/>
3
</
Suspense
>
在 React 19 中,use(promise)
被设计成完全符合 Suspense 规范的 hook,因此我们可以轻松的结合他们两者来完成页面开发。当 use(promise)
读取数据失败时,会抛出一个异常交给 Suspense 捕获,此时 Suspense 会渲染 fallback
回退组件。当请求成功之后,组件会重新渲染,此时 use(promise)
则可以读取到正确的值。
我们来梳理一下代码流程。
首先,我们定义好一个用于接口请求的函数,该函数执行返回 promise
10
import
{ createRandomMessage }
from
'@/utils '
;
20
30
var
requestOptions
:
RequestInit
=
{
40
method:
'GET '
,
50
redirect:
'follow '
60
};
70
80
const
url
=
'https://randomuser.me/api/?results=2 &inc=name,gender,email,nat,picture &noinfo '
90
10
export async function
getMessage
() {
11
await
fetch
(url, requestOptions)
12
return
{ value:
createRandomMessage
()}
13
}
然后我们定义一个子组件 Message,该子组件接受一个 promise 作为参数。然后在子组件内部,我们使用 use 读取该 promise 中的值。
10
import
{ Tent }
from
'lucide-react '
20
import
{ use }
from
'react '
30
import
{ getMessage }
from
'./api '
40
50
const
Message
=
(
props
:
{
promise
:
ReturnType
<
typeof
getMessage >})
=>
{
60
const
message
=
use
(props.promise);
70
return
(
80
<
div
className
=
'flex border border-gray-200 p-4 rounded items-start '
>
90
<
Tent
/>
10
<
div
className
=
'flex-1 ml-3 '
>
11
<
div
>React introduction </
div
>
12
<
div
className
=
'text-sm leading-6 mt-2 text-gray-600 '
>
13
{
message.value
}
14
</
div
>
15
</
div
>
16
</
div
>
17
)
18
}
19
20
export default
Message
有了这个子组件之后,我们使用 Suspense 包裹捕获该组件的错误,防止错误溢出到更高层级的组件。
10
import
{ Suspense }
from
'react '
20
import
Skeleton
from
'components/ui/skeleton '
30
import
Message
from
'./message '
40
import
{ getMessage }
from
'./api '
50
60
export default function
Page
()
{
70
const
promise
=
getMessage
()
80
return
(
90
<
Suspense
fallback
={
<
Skeleton
/>
}
>
10
<
Message
promise
={
promise
}
/>
11
</
Suspense
>
12
)
13
}
14
完整代码及演示
10
import
{ Suspense }
from
'react '
20
import
Skeleton
from
'components/ui/skeleton '
30
import
Message
from
'./message '
40
import
{ getMessage }
from
'./api '
50
60
export default function
Page
()
{
70
const
promise
=
getMessage
()
80
return
(
90
<
Suspense
fallback
={
<
Skeleton
/>
}
>
10
<
Message
promise
={
promise
}
/>
11
</
Suspense
>
12
)
13
}
14
当 Message 组件首次渲染时,由于直接读取 promise 导致报错,Suspense 捕获到该异常后,会渲染 fallback
中设置的组件。此时我们设置了一个骨架屏 Skeleton 组件,大家可以多次点击 reload 按钮查看演示效果。
因此,这个案例的视觉表现应该为:首先渲染 Skeleton 组件。然后请求成功之后,渲染 Message 组件。
Suspense 提供了一个加载数据的标准。在源码中,Suspense 的子组件被称为 primary
。
当 react 在 beginWork 的过程中(diff 过程),遇到 Suspense
时,首先会尝试加载 primary
组件。如果 primary
组件只是一个普通组件,那么就顺利渲染完成。
如果 primary
组件是一个包含了 use 读取异步 promise 的组件,它会在首次渲染时,抛出一个异常
。react 捕获到该异常之后,发现是一个我们在语法中约定好的 promise,那么就会将其 then
的回调函数保存下来,并将下一个 next
beginWork 的组件重新指定为 Suspense
。
此时 promise 在请求阶段,因此再次 beginWork Suspense 组件时,会跳过 primary
的执行而直接渲染 fallback
当 primary
中的 promise 执行完成时「resolve」,会执行刚才保存的 then
方法,此时会触发 Suspense
再次执行「调度一个更新任务」。由于此时 primary
中的 promise 已经 resolve,因此此时就可以拿到数据直接渲染 primary
组件。
整个流程可以简单表示为:
1
Suspense
-
>
2
primary
-
>
3
Suspense
-
>
4
fallback
-
>
5
waiting
-
>
resolve
() -
>
6
Suspense
-
>
7
primary
-
>
当 primary
为普通组件时,会直接渲染普通组件,如下案例所示。
10
import
React, { Suspense }
from
'react '
;
20
import
Skeleton
from
'components/ui/skeleton '
30
import
{ createRandomMessage }
from
'@/utils '
40
import
Message
from
'./message '
50
60
export default function
Demo03
()
{
70
return
(
80
<
Suspense
fallback
={
<
Skeleton
/>
}
>
90
<
Message
message
={
createRandomMessage
()
}
/>
10
</
Suspense
>
11
)
12
}
13
在前面我们 结合 use 与 Suspense 实现了一个初始化加载的案例。该案例的视觉表现是在初始化时,首先显示 Skeleton 组件,请求成功之后,显示 Message 组件。
刷新页面时重新请求数据渲染,请求过程中显示骨架屏组件 Skeleton
核心代码与演示效果如下,点击刷新按钮重复观察执行效果
10
import
{ Suspense }
from
'react '
20
import
Skeleton
from
'components/ui/skeleton '
30
import
Message
from
'./message '
40
import
{ getMessage }
from
'./api '
50
60
export default function
Page
()
{
70
const
promise
=
getMessage
()
80
return
(
90
<
Suspense
fallback
={
<
Skeleton
/>
}
>
10
<
Message
promise
={
promise
}
/>
11
</
Suspense
>
12
)
13
}
14
这里我们需要关注的是,对比以前必须要借助 state
useEffect
的实现方式,体会一下差别
10
import
React, { useEffect, useState }
from
'react '
20
import
Skeleton
from
'components/ui/skeleton '
30
import
Message
from
'./message '
40
import
{ getMessage }
from
'./api '
50
60
export default function
Demo04
()
{
70
const
[
content
,
update
]
=
useState
({ value:
''
})
80
const
[
loading
,
setLoading
]
=
useState
(
true
)
90
10
useEffect
(()
=>
{
11
getMessage
().
then
(
res
=>
{
12
update
(res)
13
setLoading
(
false
)
14
})
15
}, []);
16
17
if
(loading) {
18
return
<
Skeleton
/>
19
}
20
21
return
<
Message
message
={
content.value
}
/>
22
}
23
可以很明显的看出,新的方式使用 use + Suspense ,代码更加简洁。
除此之外,在严格模式下,开发环境组件首次加载会执行两次,因此我们还需要想额外的办法防止重复执行,代码会变得更加冗余。一个很明显的差别就是 Suspense + use
的方式会自动帮助我们弃用第二次的请求数据。而使用 useEffect
则需要我们自己来处理防止重复请求的逻辑。
与老版本使用 state
+ useEffect
完成首页初始化的需求相比,新的开发方式更加的简洁,代码舒适度更高。
不过,在以前的开发方式中,我们可以通过自定义 hook 的方式,把状态与 useEffect
封装成自定义 hook.
10
function
useFetch
() {
20
const
[
content
,
update
]
=
useState
({value:
''
})
30
const
[
loading
,
setLoading
]
=
useState
(
true
)
40
50
useEffect
(()
=>
{
60
api
().
then
(
res
=>
{
70
setLoading
(
false
)
80
update
(res)
90
})
10
}, [])
11
12
return
{content, loading}
13
}
最终在应用组件中也可以写出非常类似的非常简洁的代码。
10
function
Index
() {
20
const
{
content
,
loading
}
=
useFetch
()
30
40
if
(loading) {
50
return
<
Skeleton
/>
60
}
70
80
return
(
90
<
Message
message
={
content.value
}
/>
10
)
11
}
这是我们之前版本的最佳实践。注意体会他们之间的区别。相似,但却不同。我们后续会列举更多案例,尽可能用新的开发思路去复现开发过程中会出现的场景。除此之外,Suspense 的实现方案,还能够更好的与并发 API 结合使用,这是老版本实现方案并不具备的优势,在后续的章节中我们会进一步学习。