取數是前端業務的重要部分,也經歷過幾回演化:前端
$.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 後及時重試下游),但不失爲一種巧妙的解法,並且最大化並行也使得大部分場景性能反而比手寫的好。
筆者給仔細閱讀本文的同窗留下兩道思考題:
討論地址是: 精讀《Hooks 取數 - swr 源碼》 · Issue #216 · dt-fe/weekly
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
<img width=200 src="https://img.alicdn.com/tfs/TB...;>
版權聲明:自由轉載-非商用-非衍生-保持署名( 創意共享 3.0 許可證)