取數是前端業務的重要部分,也經歷過幾回演化:前端
$.post
在內的各類取數封裝。data
isLoading
error
封裝。swr 在 2019.10.29 號提交,僅僅 12 天就攢了 4000+ star,平均一天收穫 300+ star!本週精讀就來剖析這個庫的功能與源碼,瞭解這個 React Hooks 的取數庫的 Why How 與 What。react
首先介紹 swr 的功能。ios
爲了和官方文檔有所區別,筆者以探索式思路介紹這個它,但例子都取自官方文檔。git
首先回答一個根本問題:爲何用 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 的威力還不只如此,上面短短几行代碼還自帶以下特性:後端
固然,自動刷新或從新取數也不必定是咱們想要的,swr 容許自定義配置。api
上面提到,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
,它決定了取數方式。
自定義取數邏輯其實分幾種抽象粒度,好比自定義取數 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 的條件取數機制能夠解決這個問題。
所謂條件取數,即 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
發生變化時須要從新取數。
若是一個取數依賴另外一個取數的結果,那麼當第一個數據結束時纔會觸發新的取數,這在 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 還支持手動觸發從新取數。
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>
);
}
複製代碼
大部分場景沒必要如此,由於請求的從新觸發由數據和依賴決定,但遇到取數的必要性不禁取數參數決定,而是時機時,就須要用手動取數能力了。
特別在表單場景時,數據的改動是可預期的,此時數據驅動方案只能等待後端返回結果,其實能夠優化爲本地先修改數據,等後端結果返回後再刷新一次:
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 下返回結果,特別在網絡環境差的狀況下加快響應速度。樂觀取數,表示對取數結果是樂觀的、可預期的,因此才能在結果返回以前就預測並修改告終果。
在 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>
);
}
複製代碼
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);
}
});
複製代碼
// 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);
複製代碼
在 Hooks 場景下,包裝一層自定義 Context
便可實現全局配置。
首先 SWRConfig
本質是一個定製 Context Provider
:
const SWRConfig = SWRConfigContext.Provider;
複製代碼
在 useSWR
中將當前配置與全局配置 Merge 便可,經過 useContext
拿到全局配置:
config = Object.assign({}, defaultConfig, useContext(SWRConfigContext), config);
複製代碼
從源碼能夠看到更多細節用心,useSWR
真的比手動調用 fetch
好不少。
兼容性
useSWR
主體代碼在 useEffect
中,可是爲了將請求時機提早,放在了 UI 渲染前(useLayoutEffect
),併兼容了服務端場景:
const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect;
複製代碼
非阻塞
請求時機在瀏覽器空閒時,所以請求函數被 requestIdleCallback
包裹:
window["requestIdleCallback"](softRevalidate);
複製代碼
softRevalidate
是開啓了去重的 revalidate
:
const softRevalidate = () => revalidate({ dedupe: true });
複製代碼
即默認 2s 內參數相同的重複取數會被取消。
性能優化
因爲 swr 的 data
、isValidating
等數據狀態是利用 useState
分開管理的:
let [data, setData] = useState(
(shouldReadCache ? cacheGet(key) : undefined) || config.initialData
);
// ...
let [isValidating, setIsValidating] = useState(false);
複製代碼
而取數狀態變化時每每 data
與 isValidating
要一塊兒更新,爲了僅觸發一次更新,使用了 unstable_batchedUpdates
將更新合併爲一次:
unstable_batchedUpdates(() => {
setIsValidating(false);
// ...
setData(newData);
});
複製代碼
其實還有別的解法,好比使用 useReducer
管理數據也能達到相同性能效果。
當頁面切換時,能夠暫時以上一次數據替換取數結果,即初始化數據從緩存中拿:
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;
}
複製代碼
Suspense 分爲兩塊功能:異步加載代碼與異步加載數據,如今提到的是異步加載數據相關的能力。
Suspense 要求代碼 suspended,即拋出一個能夠被捕獲的 Promise 異常,在這個 Promise 結束後再渲染組件。
核心代碼就這一段,拋出取數的 Promise:
throw CONCURRENT_PROMISES[key];
複製代碼
等取數完畢後再返回 useSWR
API 定義的結構:
return {
error: latestError,
data: latestData,
revalidate,
isValidating
};
複製代碼
若是沒有上面 throw
的一步,在取數完畢前組件就會被渲染出來,因此 throw
了請求的 Promise 使得這個請求函數支持了 Suspense。
翻了一下代碼,沒有找到對循環依賴特別處理的邏輯,後來看了官方文檔才恍然大悟,原來是經過 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.id
在 useSWR("/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 後及時重試下游),但不失爲一種巧妙的解法,並且最大化並行也使得大部分場景性能反而比手寫的好。
筆者給仔細閱讀本文的同窗留下兩道思考題:
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)