查看原文
其他

【第1793期】理解 React 的下一步:Concurrent Mode 与 Suspense

林承澤 前端早读课 2019-12-04

前言

【第1773期】19 个来自 2019 React Conf 的总结 

今日早读文章由@林承澤分享。

正文从这开始~~

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 了,但只有包括程序代码载入的部分:

  1. const ProfilePage = React.lazy(() => import('./ProfilePage'));


  2. // Show a spinner while the profile is loading

  3. <Suspense fallback={<Spinner />}>

  4. <ProfilePage />

  5. </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,例如下面这样:

  1. useEffect(() => {

  2. fetchSomething().then(result => setState(result));

  3. }, []);

而且会因为一层一层的 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 了)。

  1. function ProfilePage() {

  2. return (

  3. <Suspense fallback={<h1>Loading profile...</h1>}>

  4. <ProfileDetails />

  5. </Suspense>

  6. );

  7. }


  8. function ProfileDetails() {

  9. // Try to read user info, although it might not have loaded yet

  10. const user = resource.user.read();

  11. return <h1>{user.name}</h1>;

  12. }

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

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

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

  1. useEffect(() => {

  2. fetchUser(id).then(u => setUser(u));

  3. }, [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() :

  1. import React, { useState, useTransition } from 'react';

  2. function App() {

  3. const [resource, setResource] = useState(initialResource);

  4. const [startTransition, isPending] = useTransition({

  5. timeoutMs: 3000,

  6. });

  7. // ...

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

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

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

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

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

  1. <button

  2. onClick={() => {

  3. const nextUserId = getNextId(resource.userId);

  4. setResource(fetchProfileData(nextUserId));

  5. }}

  6. >

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

  1. <button

  2. onClick={() => {

  3. startTransition(() => {

  4. const nextUserId = getNextId(resource.userId);

  5. setResource(fetchProfileData(nextUserId));

  6. });

  7. }}

  8. >

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

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

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

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

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

决定 Suspense 的呈现方式

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

  1. <SuspenseList tail="collapsed">

  2. <Suspense fallback={<h1>Loading...</h1>}>

  3. </Suspense>

  4. <Suspense fallback={<h1>Loading...</h1>}>

  5. </Suspense>

  6. </SuspenseList>

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

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

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

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

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

要如何试用 Concurrent Mode?

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

  1. npm install react@experimental react-dom@experimental

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

  1. import ReactDOM from 'react-dom';


  2. ReactDOM.createRoot(

  3. document.getElementById('root')

  4. ).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

为你推荐


【第1567期】为什么说Suspense是一种巨大的突破?


【第1322期】Vue CLI 3 配置中 Modern mode 是什么

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存