React Suspense 嚐鮮

陳月半啦發表於2019-03-06

前言

如同字面意思,Suspense 讓元件遇到非同步操作時進入“懸停”狀態,等非同步操作有結果時再回歸正常狀態。

非同步操作簡單歸為兩類:

  1. 非同步載入程式碼
  2. 非同步載入資料

非同步載入程式碼

非同步載入程式碼就是所謂的 code splitting,實現起來就像是這樣:

import React, {lazy, Suspense} from 'react';

const OtherComponent = lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <OtherComponent />
    </Suspense>
  );
}
複製程式碼

值得一提的是目前版本 (截止至 react@16.8) 還不支援服務端渲染,但還是會在以後的版本上支援的。

非同步載入資料

Suspense 非同步載入資料截止到目前都是不穩定的版本,根據 React 16.x Roadmap,大概到2019年中期釋出穩定版本,但是 React 官方提供了一個獨立的包 react-cache,使用它結合 react@16.6.0 可以讓我們提前感受一下 Suspense 非同步載入資料。

import { unstable_createResource } from 'react-cache';

const getSomething = (something) => new Promise((resolve) => {
  setTimeout(() => {
    resolve(something);
  }, 1000);
})

const resource = unstable_createResource((id) => getSomething(id))

function Demo() {
  const data = resource.read(666)
  return (
    <div>{data}</div>
  );
}
複製程式碼

細談 Suspense

在上面的例項中,Suspense 元件傳入了 fallback 屬性,這個屬性用於顯示載入中的頁面,就是俗稱的 loading 咯。

那麼我們在想一個問題,如果一個非同步請求資料的過程非常快,這樣會使得載入中畫面一閃而過,導致閃屏。

Suspense 針對這種情況給出解決方案 maxDuration 屬性:

<Suspense fallback={<Spinner />} maxDuration={500}>
  // ...
</Suspense>
複製程式碼

當非同步獲取資料的時間大於 maxDuration 時間時展示 fallback,否則直接展示資料。

需要注意的是 maxDuration 屬性只有在 Concurrent Mode 下才能生效,在以往的 Sync 模式下 maxDuration 始終為0, 具體使用簡單給出一個例項:

// ReactDOM.render(<App />, document.getElementById('root'));
ReactDOM
  .unstable_createRoot(document.getElementById('root'))
  .render(
    <React.unstable_ConcurrentMode>
      <App />
    </React.unstable_ConcurrentMode>
  );
複製程式碼

原理

Suspense 的實現原理頗有爭議。

當我們在 render 內寫非同步請求資料時會丟擲一個異常,當然它應該是一個 promise,這個異常會被 Suspense 內一個新的生命週期 ComponentDidCatch 捕獲到,在這個生命週期內 Suspense 將子元件渲染為 loading ,等到非同步請求結束,loading結束,此時又回到了正常的元件。

翻了一下 unstable_createResource 的原始碼,果然 Pending 狀態下會 throw 一個 suspender 物件,這個物件就是一個 promise

function unstable_createResource(fetch, maybeHashInput) {
  // ...
  var resource = {
    read: function (input) {
      // ...
      var key = hashInput(input);
      var result = accessResult(resource, fetch, input, key);
      switch (result.status) {
        case Pending:
          {
            var suspender = result.value;
            throw suspender;
          }
        case Resolved:
          {
            var _value = result.value;
            return _value;
          }
        case Rejected:
          {
            var error = result.value;
            throw error;
          }
        default:
          // Should be unreachable
          return undefined;
      }
    },
    // ...
  };
  return resource;
}
複製程式碼

總結

不知道你能不能接受如此 hack 的實現方式,固然缺點是有的,但是它帶來的便利性真的讓我異常期待。

參考:

相關文章