React Concurrent 模式搶先預覽下篇: useTransition 的平行世界

上篇文章介紹了 Suspense, 那麼這篇文章就講講它的好搭檔 useTransition。若是你是 React 的粉絲,這兩篇文章必定不能錯過。html

咱們知道 React 內部作了翻天覆地的優化,外部也提供了一些緊湊的新 API,這些 API 主要用來優化用戶體驗。React 官方用一篇很長的文檔《Concurrent UI Patterns 》 專門來介紹這一方面的動機和創造,其中的主角就是 useTransitionreact


相關文章git



本文大綱github


React 用’平行宇宙‘來比喻這個 useTransition 這個 API。What?shell

用 Git 分支來比喻會更好理解一點, 以下圖,React 能夠從當前視圖(能夠視做 Master) 分支中 Fork 出來一個新的分支(尚且稱爲 Pending),在這個新分支上進行更新,同時 Master 保持響應和更新,這兩個分支就像'平行宇宙',二者互不干擾。當 Pending 分支準備'穩當',再合併(提交)到 Master分支。redux


useTransition 就像一個時光隧道, 讓組件進入一個平行宇宙,在這個平行宇宙中等待異步狀態(異步請求、延時、whatever)就緒。固然組件也不能無限期待在平行宇宙,useTranstion 能夠配置超時時間,若是超時了,就算異步狀態未就緒也會被強制拉回現實世界。回到現實世界後,React 會當即對組件 Pengding 的變動進行合併,呈如今用戶面前。瀏覽器

所以,你能夠認爲在Concurrent 模式下, React 組件有三種狀態:緩存


  • Normal - 正常狀態下的組件
  • Suspense - 因異步狀態而掛起的組件
  • Pending - 進入平行宇宙的組件。對應的也有 Pending 的'狀態變動',這些變動 React 不會當即提交到用戶界面,而是緩存着,等待 Suspense 就緒或超時。

你可能還不太能理解, 不要緊,繼續往下讀。bash



應用場景是什麼?

'平行宇宙'有什麼用? 咱們不講代碼或者架構層次的東西。單從 UI 上看: 在某些 UI 交互場景,咱們並不想立刻將變動當即應用到頁面上架構

🔴好比你從一個頁面切換到另外一個頁面,新頁面可能須要一些時間才能加載完成,其實咱們更樂於稍微停留在上一個頁面,保持一些操做響應, 好比咱們能夠取消,或者進行其餘操做,而給我看一個什麼都沒有的空白頁面或者空轉加載狀態符, 感受在作無謂的等待

這種交互場景其實很是常見,眼前的例子就是瀏覽器:


僞裝我買得起 AirPods


還有咱們經常使用的 Github:

國外某著名交友網站


好比我想點擊買個 AirPods,瀏覽器會停留在上一個頁面,直到下一個頁面的請求得到響應或者超時。另外瀏覽器會經過地址欄的加載指示符提示請求狀況。這種交互設計,比直接切換過去,展現一個空白的頁面要好得多. 頁面能夠保持用戶響應, 也能夠隨時取消請求,保留在原來的頁面。

固然, Tab 切換時另一種交互場景,咱們但願它立刻切換過去, 不然用戶會以爲點擊不起做用。

'平行宇宙',還有一個好處: 🔴咱們假設大部分狀況下,數據請求都是很是快的,這時候其實沒有必要展現加載狀態,這會致使頁面閃爍和抖動。其實經過短暫的延時,能夠來減小加載狀態的展現頻率

另外,🔴useTransition 也能夠用於包裹低優先級更新。 從目前的狀況看,React 並無意願暴露過多的 Concurrent 模式的底層細節。若是你要調度低優先級的更新,只能使用 useTransition。



useTransition 登場


如上圖,咱們先按照 React 官方文檔的描述來定義頁面的各類狀態。它提到頁面加載有如下三個階段:

① 過渡階段(Transition)

指的是頁面未就緒,等待加載關鍵數據的階段。按照不一樣的展現策略,頁面能夠有如下兩種狀態:

  • ⚛️退化(Receded)。立刻將頁面切換過去,展現一個大大的加載指示器或者空白頁面。'退化'是什麼意思? 按照 React 的說法是,頁面本來有內容,如今變爲無內容狀態,這是一種退化,或者說歷史的'退步'。

  • ⚛️待定(Pending)。這是 useTransition 要達到的狀態,即停留在當前頁面,讓當前頁面保持響應。在關鍵數據準備就緒時進入 Skeleton(骨架屏) 狀態, 亦或者等待超時回退到 Receded 狀態。


② 加載階段(Loading)

指的是關鍵數據已經準備就緒,能夠開始展現頁面的骨架或者框架部分。這個階段有一個狀態:

  • ⚛️骨架(Skeleton)。關鍵數據已經加載完畢,頁面展現了主體的框架。

③就緒階段(Done)

指的是頁面徹底加載完畢。這個階段有一個狀態:

  • ⚛️完成(Complete) 頁面徹底呈現


傳統的 React 中,當咱們變動狀態進入一個新屏幕時,經歷的是 🔴Receded -> Skeleton -> Complete 路徑。在此以前要實現 🔴Pending -> Skeleton -> Complete 這種加載路徑比較困難。 useTransition 能夠改變這個局面。


接下來簡單模擬一個頁面切換,先來看默認狀況下是如何加載的:

function A() {
  return <div className="letter">A</div>;
}

function B() {
  // ⚛️ 延遲加載2s,模擬異步數據請求
  delay("B", 2000);
  return <div className="letter">B</div>;
}

function C() {
  // ⚛️ 延遲加載4s,模擬異步數據請求
  delay("C", 4000);
  return <div className="letter">C</div>;
}

// 頁面1
function Page1() {
  return <A />; } // 頁面2 function Page2() { return ( <> <B /> <Suspense fallback={<div>Loading... C</div>}> <C /> </Suspense> </> ); } function App() { const [showPage2, setShowPage2] = useState(false); // 點擊切換到頁面2 const handleClick = () => setShowPage2(true) return ( <div className="App"> <div> <button onClick={handleClick}>切換</button> </div> <div className="page"> <Suspense fallback={<div>Loading ...</div>}> {!showPage2 ? <Page1 /> : <Page2 />} </Suspense> </div> </div> ); } 複製代碼

看一下運行效果:

點擊切換後,咱們會立刻看到一個大大的 Loading...,接着 2s 後 B 加載完畢,再等待 2s 後 C 加載完畢。這個過程就是 Receded -> Skeleton -> Complete


如今有請 useTransition 隆重登場 🎉,只需對上面的代碼進行的簡單改造:

// ⚛️ 導入 useTransition
import React, { Suspense, useState, useTransition } from "react";

function App() {
  const [showPage2, setShowPage2] = useState(false);
  // ⚛️ useTransition 接收一個超時時間,返回一個startTransition 函數,以及一個 pending
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const handleClick = () =>
    // ⚛️ 將可能觸發 Suspense 掛起的狀態變動包裹在 startTransition 中
    startTransition(() => {
      setShowPage2(true);
    });

  return (
    <div className="App"> <div> <button onClick={handleClick}>切換</button> {/* ⚛️ pending 表示處於待定狀態, 你能夠進行一些輕微的提示 */} {pending && <span>切換中...</span>} </div> <div className="page"> <Suspense fallback={<div>Loading ...</div>}> {!showPage2 ? <Page1 /> : <Page2 />} </Suspense> </div> </div>
  );
}
複製代碼

useTransition Hook 的API比較簡潔,有4個須要關鍵的點:

  • timeoutMs, 表示切換的超時時間(最長在平行宇宙存在的時間),useTransition 會讓 React 保持在當前頁面,直到被觸發 Suspense 就緒或者超時。

  • startTransition, 將可能觸發頁面切換(嚴格說是觸發 Suspense 掛起)的狀態變動包裹在 startTransition 下,實際上 startTransition 提供了一個'更新的上下文'。 下一節咱們會深刻探索這裏面的細節

  • pending, 表示正處於待定狀態。咱們能夠經過這個狀態值,適當地給用戶一下提示。

  • Suspense, useTransition 實現過渡狀態必須和 Suspense 配合,也就是 startTransition 中的更新必須觸發任意一個 Suspense 掛起。


看一下實際的運行效果吧!


能夠在這個 CodeSandbox 查看運行效果

這個效果徹底跟本節開始的'第一張圖'同樣: React 會保留在當前頁面,pending 變爲了true,接着 B 先就緒,界面立刻切換過去。整個過程符合 Pending -> Skeleton -> Complete 的路徑。

startTransition 中的變動一旦觸發 Suspense,React 就會將變動標記的 Pending 狀態, React會延後 ’提交‘ 這些變動。因此實際上並無開頭說的平行宇宙, 那麼高大上和神奇,React 只不過是延後了這些變動的提交。咱們界面上看到的只不過是舊的或者未被 Pending 的狀態,React 在後臺進行了預渲染

注意,React 只是暫時沒有提交這些變動,不說明 React ’卡死了‘,處於Pending 狀態的組件還會接收用戶的響應,進行新的狀態變動,新的狀態更新也能夠覆蓋或終止 Pending 狀態。


總結一下進入和退出 Pending 狀態的條件:

  • 進入Pending 狀態首先須要將 狀態變動 包裹在 startTransition 下,且這些更新會觸發 Suspense 掛起
  • 退出 Pending 狀態有三種方式: ① Suspense 就緒;② 超時;③ 被新的狀態更新覆蓋或者終止


useTransition 原理初探

這一節,咱們深刻探索一下 useTransition,可是方式不是去折騰源碼,而是把它當成一個黑盒,經過幾個實驗來加深你對 useTransition 的理解。

useTransition 的前身是 withSuspenseConfig, Sebmarkbage 在今年五月份提的一個PR 中引進了它。

從命名上看,它不過是想配置一下 Suspense。 咱們也能夠經過最新的源碼驗證這一點。 useTransition 的工做'看似'很是簡單:

function updateTransition(
  config: SuspenseConfig | void | null,
): [(() => void) => void, boolean] {
  const [isPending, setPending] = updateState(false); // 至關於useState
  const startTransition = updateCallback(             // 至關於useCallback
    callback => {
      setPending(true); // 設置 pending 爲 true
      // 以低優先級調度執行
      Scheduler.unstable_next(() => {
        // ⚛️ 設置suspenseConfig
        const previousConfig = ReactCurrentBatchConfig.suspense;
        ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
        try {
          // 還原 pending
          setPending(false);

          // 執行你的回調
          callback();

        } finally {
          // ⚛️ 還原suspenseConfig
          ReactCurrentBatchConfig.suspense = previousConfig;
        }
      });
    },
    [config, isPending],
  );
  return [startTransition, isPending];
}
複製代碼

看似很普通,要點在哪?Sebmarkbage 在上述的 PR 中也說起了一些信息。

  • startTransition 一開始執行就將 pending 設置爲true。接着使用 unstable_next 執行回調, unstable_next 能夠下降更新的優先級。也就是說 unstable_next 回調中觸發的’變動‘優先級會比較低,它會讓位爲高優先級的更新,或者當前事務繁忙時,調度到下一空閒期再應用,但也可能立刻就被應用。

  • 要點是 ReactCurrentBatchConfig.suspense 的配置, 這裏面會配置 Suspense 的超時時間。它代表這個區間觸發的變動都被關聯該 suspenseConfig, 這些變動會根據 suspenseConfig 來計算本身的 expiredTime(能夠視做‘優先級’)。咱們暫且將這些關聯了 suspenseConfig 的變動稱爲 Pending 變動.

  • Pending 變動 觸發的從新渲染(Render)也會關聯該 suspenseConfig。若是在渲染期間觸發了 Suspense,那麼Pending 變動 就會被延遲提交(commit),它們會緩存在內存中, 等到 Suspense 超時或者就緒, 抑或被其餘更新覆蓋, 才強制提交到用戶界面。

  • Pending 變動 只是被延遲提交了,可是不會影響最終數據和視圖的一致性。React 會在內存中從新渲染,只是不提交到用戶界面而已。


React 內部的實現太過複雜,我發現去挖它或者用文字表達出來成本都很高。所以換一種方式,經過實驗(黑盒)方式來了解它的行爲:

這些實驗代碼在這個 CodeSandbox


1️⃣ 利用 startTransition 來運行低優先級任務

這個實驗主要用於驗證 unstable_next, 它會讓下降更新的優先級。經過下面的實驗咱們會觀察到: 經過startTransition 包裹的變動在任務繁忙的狀況會稍微延後更新,可是最終狀態是一致的。

實驗代碼:

export default function App() {
  const [count, setCount] = useState(0);
  const [tick, setTick] = useState(0);
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const handleClick = () => {
    // ⚛️ 同步更新
    setCount(count + 1);

    startTransition(() => {
      // ⚛️ 低優先級更新 tick
      setTick(t => t + 1);
    });
  };

  return (
    <div className="App"> <h1>Hello useTransition</h1> <div> <button onClick={handleClick}>ADD + 1</button> {pending && <span>pending</span>} </div> <div>Count: {count}</div> {/* ⚛️ 這是一個複雜的組件,渲染須要一點時間,模擬繁忙的狀況 */} <ComplexComponent value={tick} /> </div> ); } 複製代碼

實驗結果以下:


在連續點擊的狀況下,ComplexComponent 的更新會明顯滯後,這是由於 tick 變動會被延後和合並,可是最後它們的結果是一致的.



2️⃣ startTransition 更新觸發 Suspense

export default function App() {
  const [count, setCount] = useState(0);
  const [tick, setTick] = useState(0);
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const handleClick = () => {
    startTransition(() => {
      setCount(c => c + 1);
      setTick(c => c + 1);
    });
  };

  return (
    <div className="App">
      <h1>Hello useTransition {tick}</h1>
      <div>
        <button onClick={handleClick}>ADD + 1</button>
        {pending && <span className="pending">pending</span>}
      </div>
      <Tick />
      <SuspenseBoundary id={count} />
    </div>
  );
}

const SuspenseBoundary = ({ id }) => {
  return (
    <Suspense fallback="Loading...">
      {/* 這裏會拋出一個Promise異常,3s 後 resolved */}
      <ComponentThatThrowPromise id={id} />
    </Suspense>
  );
};

// Tick 組件每秒遞增一次
const Tick = ({ duration = 1000 }) => {
  const [tick, setTick] = useState(0);

  useEffect(() => {
    const t = setInterval(() => {
      setTick(tick => tick + 1);
    }, duration);
    return () => clearInterval(t);
  }, [duration]);

  return <div className="tick">tick: {tick}</div>;
};
複製代碼

當咱們點擊按鈕時會遞增 count 和 tick, count 會傳遞給 SuspenseBoundary,從而觸發 Suspense。

經過上面的結果能夠知道,在 startTransition 中進行了變動(攜帶suspenseConfig), 對應的從新渲染觸發了 Suspense,因此進入了Pending狀態,它們渲染結果不會被當即‘提交’,頁面仍是保持在原來的狀態。

另外你會發現 App 組件的 tick 跟 SuspenseBoundary 同樣也會被‘中止’(看Hello Transition 後面的tick),由於 tick 變動也關聯了suspenseConfig。

而 Tick 組件則每一秒遞增一次,不會被阻塞。

這就說明了一旦觸發了Suspense,只要關聯了 suspenseConfig 的變動就會被‘暫停’提交。



3️⃣ 將 tick 更新提到 startTransition 做用域外

在 2️⃣ 的基礎上,將 setTick 提到 startTransition 做用域外:

export default function App() {
  const [count, setCount] = useState(0);
  const [tick, setTick] = useState(0);
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  console.log("App rendering with", count, tick, pending);

  const handleClick = () => {
    setTick(c => c + 1);
    startTransition(() => {
      setCount(c => c + 1);
    });
  };

  const handleAddTick = () => setTick(c => c + 1);

  useEffect(() => {
    console.log("App committed with", count, tick, pending);
  });

  return (
    <div className="App"> <h1>Hello useTransition {tick}</h1> <div> <button onClick={handleClick}>ADD + 1</button> <button onClick={handleAddTick}>Tick + 1</button> {pending && <span className="pending">pending</span>} </div> <Tick /> <SuspenseBoundary id={count} /> </div> ); } 複製代碼


如今 tick 會被當即更新,而 SuspenseBoundary 還會掛在 pending 狀態。

咱們打開控制檯看一下,輸出狀況:

App rendering with 1 2 true   # pending 被設置爲true, count 這是時候是 1, 而 tick 是 2
App rendering with 1 2 true
read  1
App committed with 1 2 true    # 進入Pending 狀態以前的一次提交,咱們在這裏開始展現 pending 指示符
 # 下面 Tick 更新了三次(3s)
# 咱們注意到,每一次 React 都會從新渲染一下 App 組件,即 'ping' 一下處於 Pending 狀態的組件, 檢查一下是否‘就緒’(沒有觸發Suspense)
# 若是還觸發 Suspense, 說明還要繼續等待,這些從新渲染的結果不會被提交

App rendering with 2 2 false # ping, 這裏count變成了2,且 pending 變成了 false
App rendering with 2 2 false # 可是 React 在內存中渲染它們,咱們看不到
read  2

Tick rendering with 76        # Tick 從新渲染
Tick rendering with 76
Tick committed with 76        # 提交 Tick 更新,刷新到界面上
App rendering with 2 2 false  # ping 仍是沒有就緒,繼續 pending
App rendering with 2 2 false
read  2

Tick rendering with 77
Tick rendering with 77
Tick committed with 77
App rendering with 2 2 false # ping
App rendering with 2 2 false
read  2

Tick rendering with 78
Tick rendering with 78
Tick committed with 78
App rendering with 2 2 false # ping
App rendering with 2 2 false
read  2
 # Ok, Promise 已經就緒了,這時候再一次從新渲染 App
# 此次沒有觸發 Suspense,React 會立刻提交用戶界面
App rendering with 2 2 false
App rendering with 2 2 false
read  2
App committed with 2 2 false
複製代碼

經過上面的日誌,咱們能夠清晰地理解 Pending 組件的更新行爲



4️⃣ 嵌套Suspense

在3️⃣的基礎上,將 SuspenseBoundary 改寫爲 DoubleSuspenseBoundary, 這裏會嵌套一個 Suspense 加載一個更耗時的資源:

const DoubleSuspenseBoundary = ({ id }) => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      {/* 須要加載 2s  */}
      <ComponentThatThrowPromise id={id} timeout={2000} />
      <Suspense fallback={<div>Loading second...</div>}>
        {/* 須要加載 4s  */}
        <ComponentThatThrowPromise id={id + "second"} timeout={4000} />
      </Suspense>
    </Suspense>
  )
}
複製代碼

測試一下效果:


首先注意觀察首次掛載,Suspense 首次掛載時不會觸發延遲提交,所以咱們首先會看到 Loading...、接着第一個 ComponentThatThrowPromise 加載完畢,顯示ComponentThatThrowPromise id: 0Loading second..., 最後徹底加載完畢。

接着咱們點擊按鈕,這時候 DoubleSuspenseBoundary 會保持不動,等待 5s 後(也就是第二個ComponentThatThrowPromise加載完畢), 才提交。


理想的效果是跟首次掛載的時候同樣:在第一個 ComponentThatThrowPromise 就緒時就切換過來,不用等待第二個加載完畢。

感受有點不對?我這這裏想了好久, 官方文檔上 Concurrent UI Patterns (Experimental) - Wrap Lazy Features in <Suspense> 說了,第二個ComponentThatThrowPromise 已經嵌套在 Suspense 中了,理論上應該不會阻塞提交。

回到開頭的第一句話:'Suspense 首次掛載時不會觸發延遲提交'。咱們再試一下, 給 DoubleSuspenseBoundary 設置一個key,強制讓它銷燬從新建立:

export default function App() {
  // .....
  return (
    <div className="App"> <h1>Hello useTransition {tick}</h1> <div> <button onClick={handleClick}>ADD + 1</button> {pending && <span className="pending">pending</span>} </div> <Tick /> {/* ⚛️ 這裏添加key,強制從新銷燬建立 */} <DoubleSuspenseBoundary id={count} key={count} /> </div> ) } 複製代碼

試一下效果:


咱們發現,每次點擊都是Loading..., Pending 狀態沒有了! 由於每次 count 遞增, DoubleSuspenseBoundary 就會從新建立,不會觸發延遲提交。

基於這個原理,咱們能夠再改造一下 DoubleSuspenseBoundary, 這一次,咱們只給嵌套的 Suspense 加上key,讓它們從新建立不阻塞 Pending 狀態.

const DoubleSuspenseBoundary = ({ id }) => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <ComponentThatThrowPromise id={id} timeout={2000} />
      {/* ⚛️ 咱們不但願這個 Suspense 阻塞 pending 狀態, 給它加個key, 讓它強制從新建立 */}
      <Suspense key={id} fallback={<div>Loading second...</div>}>
        <ComponentThatThrowPromise id={id + "second"} timeout={4000} />
      </Suspense>
    </Suspense>
  );
};
複製代碼

最後的效果

It's work! 🍻



5️⃣ 能夠和 Mobx 和 Redux 配合使用嗎?

我也不知道,測試一下:

mport React, { useTransition, useEffect } from "react";
import { createStore } from "redux";
import { Provider, useSelector, useDispatch } from "react-redux";
import SuspenseBoundary from "./SuspenseBoundary";
import Tick from "./Tick";

const initialState = { count: 0, tick: 0 };
const ADD_TICK = "ADD_TICK";
const ADD_COUNT = "ADD_COUNT";

const store = createStore((state = initialState, action) => {
  const copy = { ...state };
  if (action.type === ADD_TICK) {
    copy.tick++;
  } else {
    copy.count++;
  }
  return copy
});

export const Page = () => {
  const { count, tick } = useSelector(({ tick, count }) => ({ tick, count }));
  const dispatch = useDispatch();
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const addTick = () => dispatch({ type: ADD_TICK });
  const addCount = () => dispatch({ type: ADD_COUNT });

  const handleClick = () => {
    addTick();
    startTransition(() => {
      console.log("Start transition with count: ", count);
      addCount();
      console.log("End transition");
    });
  };

  console.log(`App rendering with count(${count}) pendig(${pending})`);

  useEffect(() => {
    console.log("committed with", count, tick, pending);
  });

  return (
    <div className="App"> <h1>Hello useTransition {tick}</h1> <div> <button onClick={handleClick}>ADD + 1</button> {pending && <span className="pending">pending</span>} </div> <Tick /> <SuspenseBoundary id={count} /> </div> ); }; export default () => { return ( <Provider store={store}> <Page /> </Provider> ); }; 複製代碼

先看一下運行效果:



What’s the problem? 整個界面都 Pending 了, 整個界面不僅僅指 App 這顆子樹,並且 Tick 也不走了。打開控制檯看到了一個警告:

Warning: Page triggered a user-blocking update that suspended.

The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes.
Refer to the documentation for useTransition to learn how to implement this pattern.
複製代碼

先來看一下目前Rudux 和 Mobx 的Hooks API 是怎麼更新的,本質上它們都採用訂閱機制,在事件觸發後進行強制更新, 基本結構以下:

function useSomeOutsideStore() {
  // 獲取外部 store
  const store = getOutsideStore()
  const [, forceUpdate] = useReducer(s => s + 1, 0)

  // ⚛️ 訂閱外部數據源
  useEffect(() => {
    const disposer = store.subscribe(() => {
      // ⚛️ 強制更新
      forceUpdate()
    ))

    return disposer
  }, [store])

  // ...
}
複製代碼

也就是說,咱們在 startTransition 中更新 Redux 狀態時,會同步接收到事件,而後調用 forceUpdateforceUpdate 纔是真正在 suspenseConfig 上下文中變動的狀態

咱們再看一下控制檯日誌:

Start transition with count 0
End transition
App rendering with count(1) pendig(true)  # 這裏出問題了 🔴, 你能夠和實驗 3️⃣ 中的日誌對比一下
App rendering with count(1) pendig(true)  # 實驗 3️⃣ 中這裏的 count 是 0,而這裏的count是1,說明沒有 defer!
read  1

Warning: App triggered a user-blocking update that suspended.
The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes.
Refer to the documentation for useTransition to learn how to implement this pattern.
複製代碼

經過日誌能夠基本上可以定位出問題,count 沒有被延遲更新,因此致使'同步'觸發了 Suspense,這也是 React 警告的緣由。 因爲 useTransition 目前還處於實驗階段,若是不是 startTransition 上下文中的狀態更新致使的Suspense,行爲仍是未肯定的

可是最終的行爲有點玄學,它會致使整個應用被‘Pending’,全部狀態更新都不會被提交。這塊我也很疑惑,沒有精力深究下去,只能等待後續官方的更新,讀者也能夠去琢磨琢磨。

所以,暫時不推薦將會觸發 Suspense 的狀態放置在 Redux 或者 Mobx 中。



最後再重申一下, useTransition 要進入 Pending 狀態要符合如下幾個條件:

  • 最好使用 React 自己的狀態機制進行更新, 如 Hooks 或 setState, 當前不要使用 Mobx 和 Redux
  • 這些更新會觸發 Suspense。
  • 更新必須在startTransition做用域下, 這些更新會關聯 suspenseConfig
  • 這些更新觸發的從新渲染中, 必須觸發至少一個 Suspense
  • 這個 Suspense 不是首次掛載


那 useDeferedValue 呢?

若是你理解了上面的內容, 那麼 useDeferedValue 就好辦了,它不過是 useTransition 的簡單封裝:

function useDeferredValue<T>( value: T, config: TimeoutConfig | void | null, ): T {
  const [prevValue, setValue] = useState(value);
  const [startTransition] = useTransition(config)

  // ⚛️ useDeferredValue 只不過是監聽 value 的變化,
  // 而後在 startTransition 中更新它。從而實現延遲更新的效果
  useEffect(
    () => {
      startTransition(() => {
        setValue(value);
      })
    },
    [value, config],
  );

  return prevValue;
}
複製代碼

useDeferredValue 只不過是使用 useEffect 監聽 value 的變化, 而後在 startTransition 中更新它。從而實現延遲更新的效果。上文實驗 1️⃣ 已經介紹過運行效果,React 會下降 startTransition 中更新的優先級, 這意味着在事務繁忙時它們會延後執行。



總結

咱們一開始介紹了 useTransition 的應用場景, 讓頁面實現 Pending -> Skeleton -> Complete 的更新路徑, 用戶在切換頁面時能夠停留在當前頁面,讓頁面保持響應。 相比展現一個無用的空白頁面或者加載狀態,這種用戶體驗更加友好。

固然上述的假設條件時數據加載很慢,若是數據加載很快,利用 useTransition 機制,咱們實現不讓用戶看到加載狀態,這樣能避免頁面頁面抖動和閃爍, 看起來像沒有加載的過程。

接着咱們簡單介紹了 useTransition 的運行原理和條件。 若是 startTransition 中的狀態更新觸發了 Suspense,那麼對應的組件就會進入 Pending 狀態。在 Pending 狀態期間,startTransition中設置變動都會被延遲提交。 Pending 狀態會持續到 Suspense 就緒或者超時。

useTransition 必須和 Suspense 配合使用才能施展魔法。還有一個用戶場景是咱們能夠將低優先級的更新放置到 startTransition 中。好比某個更新的成本很高,就能夠選擇放到 startTransition 中, 這些更新會讓位高優先級的任務,另外會 React 延遲或合併一個比較複雜的更新,讓頁面保持響應。



Ok,關於 Concurrent 模式的介紹就先告一段落了, 這是中文的第一手資料。寫這些文章耗掉了我大部分的業餘時間,若是你喜歡個人文章,請多給我點贊和反饋。



參考資料


相關文章
相關標籤/搜索