精讀《Hooks 取數 - swr 源碼》

1 引言

取數是前端業務的重要部分,也經歷過幾回演化:前端

  • fetch 的兼容性已經足夠好,足以替換包括 $.post 在內的各類取數封裝。
  • 原生用得久了,發現拓展性更好、支持 ssr 的同構取數方案也挺好,好比 isomorphic-fetchaxios
  • 對於數據驅動場景仍是不夠,數據流逐漸將取數封裝起來,同時針對數據驅動狀態變化管理進行了 data isLoading error 封裝。
  • Hooks 的出現讓組件更 Reactive,咱們發現取數仍是優雅回到了組件裏,swr 就是一個教科書般的例子。

swr 在 2019.10.29 號提交,僅僅 12 天就攢了 4000+ star,平均一天收穫 300+ star!本週精讀就來剖析這個庫的功能與源碼,瞭解這個 React Hooks 的取數庫的 Why How 與 What。react

2 概述

首先介紹 swr 的功能。ios

爲了和官方文檔有所區別,筆者以探索式思路介紹這個它,但例子都取自官方文檔。git

2.1 爲何用 Hooks 取數

首先回答一個根本問題:爲何用 Hooks 替代 fetch 或數據流取數?github

由於 Hooks 能夠觸達 UI 生命週期,取數本質上是 UI 展現或交互的一個環節。 用 Hooks 取數的形式以下:typescript

import useSWR from "swr";

function Profile() {
  const { data, error } = useSWR("/api/user", fetcher);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}
複製代碼

首先看到的是,以同步寫法描述了異步邏輯,這是由於渲染被執行了兩次。json

useSWR 接收三個參數,第一個參數是取數 key,這個 key 會做爲第二個參數 fetcher 的第一個參數傳入,普通場景下爲 URL,第三個參數是配置項。axios

Hooks 的威力還不只如此,上面短短几行代碼還自帶以下特性:後端

  1. 可自動刷新。
  2. 組件被銷燬再渲染時優先啓用本地緩存。
  3. 在列表頁中瀏覽器回退能夠自動記憶滾動條位置。
  4. tabs 切換時,被 focus 的 tab 會從新取數。

固然,自動刷新或從新取數也不必定是咱們想要的,swr 容許自定義配置。api

2.2 配置

上面提到,useSWR 還有第三個參數做爲配置項。

獨立配置

經過第三個參數爲每一個 useSWR 獨立配置:

useSWR("/api/user", fetcher, { revalidateOnFocus: false });
複製代碼

配置項能夠參考 文檔

能夠配置的有:suspense 模式、focus 從新取數、從新取數間隔/是否開啓、失敗是否從新取數、timeout、取數成功/失敗/重試時的回調函數等等。

第二個參數若是是 object 類型,則效果爲配置項,第二個 fetcher 只是爲了方便才提供的,在 object 配置項裏也能夠配置 fetcher。

全局配置

SWRConfig 能夠批量修改配置:

import useSWR, { SWRConfig } from "swr";

function Dashboard() {
  const { data: events } = useSWR("/api/events");
  // ...
}

function App() {
  return (
    <SWRConfig value={{ refreshInterval: 3000 }}>
      <Dashboard />
    </SWRConfig>
  );
}
複製代碼

獨立配置優先級高於全局配置,在精讀部分會介紹實現方式。

最重量級的配置項是 fetcher,它決定了取數方式。

2.3 自定義取數方式

自定義取數邏輯其實分幾種抽象粒度,好比自定義取數 url,或自定義整個取數函數,而 swr 採起了相對中間粒度的自定義 fetcher

import fetch from "unfetch";

const fetcher = url => fetch(url).then(r => r.json());

function App() {
  const { data } = useSWR("/api/data", fetcher);
  // ...
}
複製代碼

因此 fetcher 自己就是一個拓展點,咱們不只能自定義取數函數,自定義業務處理邏輯,甚至能夠自定義取數協議:

import { request } from "graphql-request";

const API = "https://api.graph.cool/simple/v1/movies";
const fetcher = query => request(API, query);

function App() {
  const { data, error } = useSWR(
    `{
      Movie(title: "Inception") {
        releaseDate
        actors {
          name
        }
      }
    }`,
    fetcher
  );
  // ...
}
複製代碼

這裏迴應了第一個參數稱爲取數 Key 的緣由,在 graphql 下它則是一段語法描述。

到這裏,咱們能夠自定義取數函數,但卻沒法控制什麼時候取數,由於 Hooks 寫法使取數時機與渲染時機結合在一塊兒。swr 的條件取數機制能夠解決這個問題。

2.4 條件取數

所謂條件取數,即 useSWR 第一個參數爲 null 時則會終止取數,咱們能夠用三元運算符或函數做爲第一個參數,使這個條件動態化:

// conditionally fetch
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);

// ...or return a falsy value
const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);
複製代碼

上例中,當 shouldFetch 爲 false 時則不會取數。

第一個取數參數推薦爲回調函數,這樣 swr 會 catch 住內部異常,好比:

// ... or throw an error when user.id is not defined
const { data, error } = useSWR(() => "/api/data?uid=" + user.id, fetcher);
複製代碼

若是 user 對象不存在,user.id 的調用會失敗,此時錯誤會被 catch 住並拋到 error 對象。

實際上,user.id 仍是一種依賴取數場景,當 user.id 發生變化時須要從新取數。

2.5 依賴取數

若是一個取數依賴另外一個取數的結果,那麼當第一個數據結束時纔會觸發新的取數,這在 swr 中不須要特別關心,只需按照依賴順序書寫 useSWR 便可:

function MyProjects() {
  const { data: user } = useSWR("/api/user");
  const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);

  if (!projects) return "loading...";
  return "You have " + projects.length + " projects";
}
複製代碼

swr 會盡量並行沒有依賴的請求,並按依賴順序一次發送有依賴關係的取數。

能夠想象,若是手動管理取數,當依賴關係複雜時,爲了確保取數的最大可並行,每每須要精心調整取數遞歸嵌套結構,而在 swr 的環境下只需順序書寫便可,這是很大的效率提高。優化方式在下面源碼解讀章節詳細說明。

依賴取數是自動從新觸發取數的一種場景,其實 swr 還支持手動觸發從新取數。

2.6 手動觸發取數

trigger 能夠經過 Key 手動觸發取數:

import useSWR, { trigger } from "swr";

function App() {
  return (
    <div>
      <Profile />
      <button
        onClick={() => {
          // set the cookie as expired
          document.cookie =
            "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";

          // tell all SWRs with this key to revalidate
          trigger("/api/user");
        }}
      >
        Logout
      </button>
    </div>
  );
}
複製代碼

大部分場景沒必要如此,由於請求的從新觸發由數據和依賴決定,但遇到取數的必要性不禁取數參數決定,而是時機時,就須要用手動取數能力了。

2.7 樂觀取數

特別在表單場景時,數據的改動是可預期的,此時數據驅動方案只能等待後端返回結果,其實能夠優化爲本地先修改數據,等後端結果返回後再刷新一次:

import useSWR, { mutate } from "swr";

function Profile() {
  const { data } = useSWR("/api/user", fetcher);

  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button
        onClick={async () => {
          const newName = data.name.toUpperCase();
          // send a request to the API to update the data
          await requestUpdateUsername(newName);
          // update the local data immediately and revalidate (refetch)
          mutate("/api/user", { ...data, name: newName });
        }}
      >
        Uppercase my name!
      </button>
    </div>
  );
}
複製代碼

經過 mutate 能夠在本地臨時修改某個 Key 下返回結果,特別在網絡環境差的狀況下加快響應速度。樂觀取數,表示對取數結果是樂觀的、可預期的,因此才能在結果返回以前就預測並修改告終果。

2.8 Suspense 模式

在 React Suspense 模式下,全部子模塊均可以被懶加載,包括代碼和請求均可以被等待,只要開啓 suspense 屬性便可:

import { Suspense } from "react";
import useSWR from "swr";

function Profile() {
  const { data } = useSWR("/api/user", fetcher, { suspense: true });
  return <div>hello, {data.name}</div>;
}

function App() {
  return (
    <Suspense fallback={<div>loading...</div>}>
      <Profile />
    </Suspense>
  );
}
複製代碼

2.9 錯誤處理

onErrorRetry 能夠統一處理錯誤,包括在錯誤發生後從新取數等:

useSWR(key, fetcher, {
  onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
    if (retryCount >= 10) return;
    if (error.status === 404) return;

    // retry after 5 seconds
    setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000);
  }
});
複製代碼

  1. 本地突變
  2. suspense mode
  3. 錯誤處理
// conditionally fetch
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);

// ...or return a falsy value
const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);

// ... or throw an error when user.id is not defined
const { data } = useSWR(() => "/api/data?uid=" + user.id, fetcher);
複製代碼

3 精讀

3.1 全局配置

在 Hooks 場景下,包裝一層自定義 Context 便可實現全局配置。

首先 SWRConfig 本質是一個定製 Context Provider:

const SWRConfig = SWRConfigContext.Provider;
複製代碼

useSWR 中將當前配置與全局配置 Merge 便可,經過 useContext 拿到全局配置:

config = Object.assign({}, defaultConfig, useContext(SWRConfigContext), config);
複製代碼

3.2 useSWR 的一些細節

從源碼能夠看到更多細節用心,useSWR 真的比手動調用 fetch 好不少。

兼容性

useSWR 主體代碼在 useEffect 中,可是爲了將請求時機提早,放在了 UI 渲染前(useLayoutEffect),併兼容了服務端場景:

const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect;
複製代碼

非阻塞

請求時機在瀏覽器空閒時,所以請求函數被 requestIdleCallback 包裹:

window["requestIdleCallback"](softRevalidate);
複製代碼

softRevalidate 是開啓了去重的 revalidate:

const softRevalidate = () => revalidate({ dedupe: true });
複製代碼

即默認 2s 內參數相同的重複取數會被取消。

性能優化

因爲 swrdataisValidating 等數據狀態是利用 useState 分開管理的:

let [data, setData] = useState(
  (shouldReadCache ? cacheGet(key) : undefined) || config.initialData
);
// ...
let [isValidating, setIsValidating] = useState(false);
複製代碼

而取數狀態變化時每每 dataisValidating 要一塊兒更新,爲了僅觸發一次更新,使用了 unstable_batchedUpdates 將更新合併爲一次:

unstable_batchedUpdates(() => {
  setIsValidating(false);
  // ...
  setData(newData);
});
複製代碼

其實還有別的解法,好比使用 useReducer 管理數據也能達到相同性能效果。

3.3 初始緩存

當頁面切換時,能夠暫時以上一次數據替換取數結果,即初始化數據從緩存中拿:

const shouldReadCache = config.suspense || !useHydration();

// stale: get from cache
let [data, setData] = useState(
  (shouldReadCache ? cacheGet(key) : undefined) || config.initialData
);
複製代碼

上面一段代碼在 useSWR 的初始化期間,useHydration 表示是否爲初次加載:

let isHydration = true;

export default function useHydration(): boolean {
  useEffect(() => {
    setTimeout(() => {
      isHydration = false;
    }, 1);
  }, []);

  return isHydration;
}
複製代碼

3.4 支持 suspense

Suspense 分爲兩塊功能:異步加載代碼與異步加載數據,如今提到的是異步加載數據相關的能力。

Suspense 要求代碼 suspended,即拋出一個能夠被捕獲的 Promise 異常,在這個 Promise 結束後再渲染組件。

核心代碼就這一段,拋出取數的 Promise:

throw CONCURRENT_PROMISES[key];
複製代碼

等取數完畢後再返回 useSWR API 定義的結構:

return {
  error: latestError,
  data: latestData,
  revalidate,
  isValidating
};
複製代碼

若是沒有上面 throw 的一步,在取數完畢前組件就會被渲染出來,因此 throw 了請求的 Promise 使得這個請求函數支持了 Suspense。

3.5 依賴的請求

翻了一下代碼,沒有找到對循環依賴特別處理的邏輯,後來看了官方文檔才恍然大悟,原來是經過 try/catch + onErrorRetry 機制實現依賴取數的。

看下面這段代碼:

const { data: user } = useSWR("/api/user");
const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);
複製代碼

怎麼作到智能按依賴順序請求呢?咱們看 useSWR 取數函數的主體邏輯:

try {
  // 設置 isValidation 爲 true
  // 取數、onSuccess 回調
  // 設置 isValidation 爲 false
  // 設置緩存
  // unstable_batchedUpdates
} catch (err) {
  // 撤銷取數、緩存等對象
  // 調用 onErrorRetry
}
複製代碼

可見取數邏輯被 try 住了,那麼 user.iduseSWR("/api/user") 沒有 Ready 的狀況必定會拋出異常,則自動進入 onErrorRetry 邏輯,看看下次取數時 user.id 有沒有 Ready。

那麼何時才輪到下次取數呢?這個時機是:

const count = Math.min(opts.retryCount || 0, 8);
const timeout =
  ~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval;
複製代碼

重試時間基本按 2 的指數速度增加。

因此 swr 會優先按照並行方式取數,存在依賴的取數會重試,直到上游 Ready。這種簡單的模式稍稍損失了一些性能(沒有在上游 Ready 後及時重試下游),但不失爲一種巧妙的解法,並且最大化並行也使得大部分場景性能反而比手寫的好。

4 總結

筆者給仔細閱讀本文的同窗留下兩道思考題:

  • 關於 Hooks 取數仍是在數據流中取數,你怎麼看呢?
  • swr 解決依賴取數的方法還有更好的改進辦法嗎?

討論地址是:精讀《Hooks 取數 - swr 源碼》 · Issue #216 · dt-fe/weekly

若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公衆號

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章
相關標籤/搜索