带你提前理解 React 的下一步:Concurrent Mode 与 Suspense

点击上方“IT平头哥联盟”,选择“置顶或者星标”

你的关注意义重大!

10 月刚在 Las Vegas 结束的 React Conf 2019带来许多关于 Concurrent Mode、Suspense for Data Fetching 的消息,如果你对于这些议题感到好奇,但还没有时间去吸收,那这篇文章或许值得一读。

当然除了继续往下把这篇文看完以外,如果你有充足的时间,推荐也花一点时间把官方的五篇文章看过。

以下会按照这样的编排來介绍:

  • 什么是 Concurrent Mode?

  • Suspense for Data Fetching

  • 什么是 Transition?

  • 决定 Suspense 的揭露方式

  • 要如何试用 Concurrent Mode?

  • 什么是 Concurrent Mode?

说到这个就不得不提到 React 16 时,React Team 曾经把 React 整个框架重写过,整个计划「Fiber」耗时一年多才完成,其实就是为了 Concurrent Mode 所铺的路。我还特別翻到以前在 Modern Web 2017 演讲的PPT,三年也是好快就过了!(当时看到 Fiber Ready 相当感动啊)

那 Fiber 是怎么一回事呢?

为了让 Render / Reconciliation 的过程更为弹性,React Team 决定把这个一次搞定 Render 整个 Tree 的步骤切成一个个更小的步骤,让整个过程可以暂停、可以放弃也可以 Concurrent 的执行。

而 Fiber 就是 Render / Reconciliation 时的最小单位。(如果对 Reconciler 完全没概念的话推荐 Sophie Alpert 的这个介绍)

或许大部分的 React 开发者都遇到在输入框打字时,因为 State 改变造成Render 而挡住了输入框的立即更新,这个互动通常会让使用者觉得卡卡的,这个就是中断、暂停 Render 能解決的问题。

熟悉 Git 版本控制的人不妨直接用 Git 来思考 React 的运行方式,React 可以在不同的 Branch 上 Concurrent 去处理不同 State变动造成的 Render,而这些 Render 的结果可以被 Merge,也可以直接被放弃。

简而言之,React 就是要针对不同的装置能力(CPU)跟网络速度(IO)提供最优化的使用者体验。

在 React Conf 的 Keynote,Tom Occhino 提到使用者体验才是 React 的使命,我很喜欢这句原话:

A great developer experience only matters if it’s in service of delivering a great user experience. - Tom Occhino Suspense for Data Fetching

Suspense 是一个让还没准备好可以 Render 的 UI 可以显示为 Loading 状态的功能,那为什么这边要特別强调是 for Data Fetching 呢?因为 React 早先已经支持 Suspense 了,但只有包括程序代码载入的部分:

constProfilePage= React.lazy(() => import('./ProfilePage'));
// Show a spinner while the profile is loading
<Suspense fallback={<Spinner/>}>
<ProfilePage/>
</Suspense>

而 Suspense for Data Fetching则堪称是这个系列的最终章,可以说是从 2016 年开始 Fiber 计划后最后一个明确的目标。

在这边需要先来了解一下官方所提出的三种获取资料的方式:

  • Approach 1: Fetch-on-Render

  • Approach 2: Fetch-Then-Render

  • Approach 3: Render-as-You-Fetch (using Suspense)

Fetch-on-Render

使用 componentDidMount 或是 useEffect 去获取资料就属于这种,这是理论上效率、体验最差的,Render 后才去呼叫 API,例如下面这样:

useEffect(() => {
  fetchSomething().then(result => setState(result));
}, []);

而且会因为一层一层的 Render,造成获取资料时的 Waterfall。

Fetch-Then-Render

这是 Facebook 的 Relay 框架或者是说GraphQL 体系比较容易做到的事,首先必须让资料被静态的定义好。(如果不太懂 GraphQL 可以完全略过这段或是加入 GraphQL Taiwan 询问)

例如使用 GraphQL 的 Fragment,这样你才能在 Render 前就知道 Component 需要什么数据。并且让 Fragment 被 Compose 起來,就能避免获取资料時的 Waterfall。

Render-as-You-Fetch (using Suspense)

这应该会是未来推荐的做法,在 Render 之前尽早的开始获取数据,并立刻的开始 Render 下一个页面,这时资料若处于未 Ready 的状态,那就会 throw Promise 并进入 Suspense 的状态,等到 Promise Resolve 后,React 会进行 Retry(这时候资料已经 Ready 了)。

functionProfilePage() {
return(
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails/>
</Suspense>
);
}
functionProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return<h1>{user.name}</h1>;
}

Resource 是个未来还很有可能会改的东西,基本上可以先不用了解,只要知道他这样read,可能会 throw Promise 出去给 React接这样就夠了。

另外带來的好处 — 解決 Race Condition

以前传统的方式在 componentDidMount 或是 useEffect 去抓资数据的时候,Render 跟获取数据的 Promise 本身是脱钩的:

useEffect(() => {
  fetchUser(id).then(u => setUser(u));
}, [id]);

这样的程序若在 Promise 还没 Resolve 的情況下就进行下一次的 Render,就会造成 Race Condition,因为这个 Effect 沒有被好好的 Cleanup,做干净点是要去取消 Fetch 以及它所造成的 setState 效果,但这样写清楚又很麻烦,很容易出錯。

在 Suspense for Data Fetching 的情況下,这个获取数据的 Promise 跟 Render 是挂钩一起的,就不会有这个 Effect 没完成需要取消的状况了。

什么是 Transition?

Transition 就是指切换页面的那个Transition。

为什么要特別提到这个呢?因为这在使用者体验上其实扮演举足轻重的角色。

不知道大家有沒有类似的经验,在一个已经 Render 很完整的一個页面,点了一个按钮跳页面后,那瞬间回到一个 Loading 状态,数据来了后东西才又显示出來,这中间花的时间有长有短,短得有的甚至就是一個闪烁。

以官方提供的范例来说,原本好好的 Home Page 一但切到 Profile Page,原本的画面就不见了,剩下一個大大的 Loading

在这边我们需要讨论一个状况,如果我们在跳转页面时,让原本的页面暂留一下子,来刻意地跳过中间那个有点糟的 Loading 状态,那会不会更好呢?

用 useTransition 来改善换页面的体验

React 提供了一个方式来处理这个问题,就是利用新的內建 Hook useTransition() :

importReact, { useState, useTransition } from'react';
functionApp() {
const[resource, setResource] = useState(initialResource);
const[startTransition, isPending] = useTransition({
    timeoutMs: 3000,
});
// ...

我们简单的来看一下这个 Hook 的参数与返回值:

startTransition 是个 Function,可以用来告诉 React 哪些 State Update 可以延后生效。

isPending 是个 Boolean 值,代表 Transition 是否正在进行。这是要用来在原先的页面显示 Loading 提示,不然停在原本的页面也会让使用者以为网页失去响应。

timeoutMs 则是设定一个 Pending 的时间上限,超过了时间无论页面有多糟都是直接进行 State Update。

可以假设我们原本是这样在 onClick 里面去 setState 的:

<button
onClick={() => {
    const nextUserId = getNextId(resource.userId);
    setResource(fetchProfileData(nextUserId));
  }}
>

可以把里面的 State Update 用 startTransition 包起來,表示这段延后生效也沒关系:

<button
onClick={() => {
    startTransition(() => {
      const nextUserId = getNextId(resource.userId);
      setResource(fetchProfileData(nextUserId));
    });
  }}
>

或许大家看到这边会很疑惑,startTransition 到底在干什么?

沒关系,我刚看完也是满脸问号、一头雾水,直到我去翻了一下原始代码。

我的理解就是,包在 startTransition 里面的这段 Code 会被立刻执行,包括这个 fetchProfileData 的部分,但这个 State Update 会被用特別的 Priority放进Scheduler。

看不懂也沒关系,我们可以直接来看看它的效果,记得要回去看一下上面那張 GIF 比較一下:

这个功能帶来的结论就是下面这张图,我们要用 useTransition 来 Hold 住话面(Pending)避免走向直接切换画面所造成的 UI 倒退的狀況(Receded)。

决定 Suspense 的呈现方式

有時候,我们会有超过一个以上的 Suspense 在页面上,如果秀出超过一个Loading,有時候会蛮尷尬的,这時候可以用 SuspenseList 把它们包起來,并指定 tail 为 collapsed,这样 Loading 就只出现一个了:

<SuspenseListtail="collapsed">
  <Suspense fallback={<h1>Loading...</h1>}>
</Suspense>
  <Suspense fallback={<h1>Loading...</h1>}>
</Suspense>
</SuspenseList>

另一个有趣的 prop 是 revealOrder,可以用来決定呈现的順序。

来看一下 React Conf 上的 Demo,这是一个一般的版本,所有图片片參差的出現:

下面这个是 revealOrder 为 forwards 的效果,图片会从左到右,从上到下的显示:

这个是 revealOrder 为 together 的效果,所有图片一起出現:

看完这个就能知道要怎么样用这个功能来改善使用者体验了。

要如何试用 Concurrent Mode?

Concurrent Mode 目前不存在于 stable 的 release 之中,要试用的话必须安装experimental 的版本:

npm install react@experimental react-dom@experimental

除此之外,你还需要把 ReactDOM.render 改成 ReactDOM.createRoot(...).render

importReactDOMfrom'react-dom';
ReactDOM.createRoot(
  document.getElementById('root')
).render(<App/>);

如果觉得这个改动太大了,他有提供了一个介于中间的 Blocking Mode,可以用来渐进式的 Migrate 到 Concurrent Mode,虽然缺乏 useTrainsition、useDefferedValue 等等功能但比 Lagacy Mode 更接近 Concurrent Mode。(用 ReactDOM.createBlockingRoot(...).render即可使用)

以下是功能的对照表:

出自 https://reactjs.org/docs/concurrent-mode-adoption.html

没错不要怀疑,React 就是这么的狠,把你我正在 Production 上使用的版本直接成为 Legacy Mode。

总结

Concurrent Mode 到目前为止都还在实验阶段,但可以看到 React Team 不惜花个四五年也要完成它的决心。至于要等到大部分的组件、Component 都能兼容 Concurrent Mode 也是另一个长期抗战的。

虽然不是每个开发者都需要去关注使用者不同裝置上的载入、切换的体验,但是这种事关 Reconciliation 的行为改变的原理,还是会推荐研究比較深入的 React 开发者必须了解一下。

关于本文 作者:@林承澤 原文:https://medium.com/@chentsulin/理解-react-的下一步-concurrent-mode-與-suspense-327b8a3df0fe

-  end  -

❤️ 看完两件事

如果你觉得这篇内容对你有所帮助,我想邀请你帮我两个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

  2. 关注公众号「IT平头哥联盟」,一起进步,一起成长!

推荐阅读

面试官问:TCP为啥要3次握手和4次挥手?握两次手不行吗?

面试经常被问的web安全问题

为什么说Suspense是一种巨大的突破?

immutability-helper被React推荐不可变数据的辅助工具!

看文吃瓜:React遭遇V8性能崩溃的故事

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值