開源專案讀起來 | 聽說可以幫你保管資料再決定要不要更新的 SWR

神Q超人
Starbugs Weekly 星巴哥技術專欄
16 min readJan 4, 2021
Photo by Paul Volkmer on Unsplash

Hi!大家好,我是神Q超人!因為這篇文剛好是在 1 月 1 號寫的,所以先祝個大家新年快樂!然後就來說說這個開源專案讀起來這系列的文章要幹嘛吧(很貪心的想要學一堆東西 😂)!

會有這個系列的念頭是因為在業界很常聽到

閱讀比寫作還要重要

如果平常如果不去看看那些使用率高的開源專案是怎麼寫出來的,之後換作要自己寫的話怎麼辦啦!而且透過閱讀那些開源專案,還偷學到許多那些厲害的工程師們設計程式的技巧!

但對初學者來說,開源專案並沒有那麼好讀,開源專案裡的程式碼和一般在 MDN 上看到那些範例程式根本無法比較,不過在前幾個禮拜我有看到一篇文章,是關於 我是如何阅读源码的,裡面作者說了一句話:

带着目的去看源码,我们只有带着问题出发的时候,才会具有更高的效率。

聽到後讓我一整個震驚,然後從椅子上摔下去,對啊!根本就沒有讀懂整個專案的必要,換個角度來說,

閱讀開源專案的目的是,找到我想知道的某個功能是如何實現的!

那廢話說了那麼多,就直接開始本系列第一篇文章吧!

SWR 介紹

關於 SWR 大家也許會比較陌生,因為它是 React Hooks 生態圈的函式庫,所以可能只有某部分的 React 開發者會稍微聽過它。先來看看官方文件上如何說的:

SWR is a strategy to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.

總之 SWR 的功能主要是為了讓前端更有效率的取得資料,SWR 會在你要從 API 取得資料的時候先確認之前有沒有取過了,如果有取過就先送前一次的給你,等到 API 把資料送到前端的時候,再確認資料一不一樣,如果不一樣的話就再把新的資料存起來,然後送給你更新畫面!基本的程式碼如下:

想看更多此函式庫的介紹可以參考:React SWR | 取得遠端資料的殺手級函式庫

useSWR 就是我們這一次要解析的主要功能了,上方的程式碼中它接收了兩個參數,一個是作為 key 的 apiUser,另一個就是用來打 API 的 fetcher,除此之外還有另外一個方法是 mutate,呼叫它的時候可以傳入 key,useSwr 會再打一次對應的 API 獲取資料。執行結果:

在第一次 render 的時候會從 API 獲取資料,得到資料後 SWR 會存起來,然後 component 會因為得到資料 render 一次(所以 console 中出現兩個 render),而之後每次點擊 Fetch User Again 按鈕都會再打一次 API,但因為 SER 判斷每次回來的資料都相同,就不更新當前存的資料,頁面也不會重新 render!

相信大家看了 Demo 和聽完需求後,腦子裡面應該想了各式各樣能夠實現同樣功能的程式碼,那接下來就來看看其他人是怎麼做的,順便驗證自己的想法正不正確吧!

開始閱讀 SWR

在閱讀開源專案的時候我習慣把整個專案 clone 到本機裡面用 VS Code 打開來看,這樣就不用在 GitHub 上跳頁面切換檔案,而且想要執行的時候也可以直接執行。讚啦!

專案的出口處

這裡指的出口處就是整個專案究竟是在哪裡將我們要使用的功能 export,而專案的 export 也會是我們在使用時所 import 的東西。

每個專案的入口處都不太一樣,但就我個人的話都會先去 src 目錄看有沒有 index 這個檔案,而果不其然在該位置下就有個 index.ts,而打開來後就看見一堆東西被 export

https://github.com/vercel/swr/blob/master/src/index.ts

從這個檔案我們可以知道,我們在使用時 importuseSWR 是來自於第二行來源 src/use-swr。

解析 useSWR

打開 src/use-swr 後會看到一堆程式碼,不過不要緊,我們沒有要看懂所有東西,只需要知道 useSWR 是如何在打 API 的時候,從結果來決定要不要更新的,所以目標是先找到 useSWR 到底是定義哪一行。

因為在 src/index.ts 中,是從 src/use-swr 取出 default 的東西取名成 useSWR,所以在 src/use-swr 直接用 export default 就能找到到底 export 了什麼:

找到最後一行找到被 exportuseSWR(https://github.com/vercel/swr/blob/master/src/use-swr.ts

接著再用一樣的方法,找到該檔案中定義 useSWR 的位置,就在第 236 行:

找到 useSWR 啦!(https://github.com/vercel/swr/blob/master/src/use-swr.ts

useSWR 裡面首要判斷的就是傳進來的參數數量,並把它們放到正確的位置,在上面的範例有說到啊,使用 useSWR 可以傳入兩個參數,第一個是 key,所以在第 243 行就把傳進來的第一個參數放到 _key 裡面,第二個是用來打 API 的 fetcher,那在 250 行也可以看到 fetcher 被裝到 fn 了。

除了 key 和 fetcher 外,其實還可以傳入第三個用來做一些配置的 config 參數,但那不重要,我們只要先看懂我們想看的就好,也就是 _keyfn 會如何被對待。

serializeKey

取到 _key 後第一件事情就是用 cache.serializeKey 解析 _key 的內容,而 cache 的位置會在 src/cache.ts 裡面:

https://github.com/vercel/swr/blob/master/src/cache.ts#L45

值得注意的地方是 cache.serializeKeykey 的處理,在使用 useSWR 的時候 key 可以傳入一個字串、陣列或是方法,大家也可以看到上方程式碼的第 49 行先判斷了 key 是不是方法,如果是的話就執行方法,並用結果當作 key 值。

接著在 56 行繼續判斷如果 key 是陣列的話,就把裡面的內容都取出來,當作執行 fetcher 時的參數。最後才是一般的字串。

resolveData

解析完傳入的參數後下一步可以直接看到 resolveDataresolveData 就是去看當前的 cache 中有沒有該 key 值對應的資料(關於 cache 待會會提到,可以先把它當作一個 object),如果有的話就使用,沒有的話就用 config.initialData,但是我們也沒有設定 initialData,因此第一次在這裡取到的就會是 undefined:

https://github.com/vercel/swr/blob/master/src/use-swr.ts#L279

useRef 與 useState

然後一樣是上方的程式碼片段,在第 296 行的部分是我覺得核心的地方之一,因為 SWR 為了可以控制 component 重新 render 的時間,所以 SWR 用 useRef 來管理資料,而不是 useStateuseRef 改變並不會造成 component 重新 render)。那你說 SWR 是怎麼去觸發 component 的 render 呢?

答案還是 useState,再往下看一點可以發現 SWR 定義了一個叫做 rerender 的方法,而這個方法就是用 useState 建立的:

https://github.com/vercel/swr/blob/master/src/use-swr.ts#L305

也就是說,使用了 useRef 來管理資料的 SWR 為了要能夠在 useRef 內的資料更新的時候可以觸發 component 重新 render,就直接把 useState 當作工具人,需要更新的時候就執行 rerender({}),但從 Hooks 回傳的資料則是在 stateRef 裡面。

那再接下來的程式碼都是在定義一些會用到的方法,這個部分我會都先跳過,之後有執行到相關的方法才會再回來閱讀。

useIsomorphicLayoutEffect

下一段要執行的程式碼是第 550 行的 useIsomorphicLayoutEffect,而這個 useIsomorphicLayoutEffect 被定義在第 40 行:

https://github.com/vercel/swr/blob/master/src/use-swr.ts#L40

我覺得 SWR 的註解寫的超好,如果是在 server 端上執行 SWR 的話(SSR),useIsomorphicLayoutEffect 就是 useEffect,如果是在 client 的話就用 useLayoutEffect,而這兩個 Hooks 的差別在於執行的時間,useEffect 會在 DOM 都準備好以後才執行,而useLayoutEffect 會和 DOM 在同一個時間同步執行。React 的文件 也有提到為什麼不能在 SSR 中使用 useLayoutEffect

useIsomorphicLayoutEffect 裡面 SWR 先去判斷 key 是不是空的,如果是的話回傳 undefined,直接結束。

判斷完後先從 stateRefresolveDatacache 裡面)取出管理的資料,並分別定義為 currentHookDatalatestKeyedData,之後比較兩者的值,如果一樣的話就沒事,不一樣的話就觸發 dispatch(待會再回來看 dispatch),因為第一次使用的時候不論是 currentHookDatalatestKeyedData 都是 undefined,所以會繼續往下跑:

https://github.com/vercel/swr/blob/master/src/use-swr.ts#L562

接下來就是重點了,大家可以直接看到下方的程式碼片段:

https://github.com/vercel/swr/blob/master/src/use-swr.ts#L574

如果 latestKeyedData 等於 undefined 的話,就會執行 softRevalidate,而 softRevalidate 的內容則是回傳 revalidate 的結果,所以接下來就移到 SWR 在處理獲取和緩存資料的精華 revalidate 吧!

revalidate

只要看到 revalidate 閱讀難度就大大降低了,在 revalidate 內的前幾行也是在判斷一些東西,如果有問題的話就直接回傳 false。

值得注意的是 revalidate 畢竟是處理請求的 Hooks,所以為了避免超時也做了一些處理,大家只要先注意到在開頭有個 loading 被定義為 true,之後會再提到在哪用到它。

之後定義了 newDatestartAt 兩個變數就進入獲取新資料的階段:

https://github.com/vercel/swr/blob/00c5847ce6e95902920933aa1d49c7143f8ce4ba/src/use-swr.ts#L405

在上方 405 行的判斷是是為了避免重複去做請求,所以如果當前還有未完成的請求就直接把資料取出來,不用再請求一次。如果沒有請求的話就進入 else 的內容,首先用一個 setTimeout 來在幾秒後的未來驗證 loading 還是不是 true,如果是的話就請求太久啦!

第 419 行開始就是先判斷有沒有參數(看 key 傳進來是不是陣列),然後再看情況執行 fn 也就是我們打 API 的 fetcher,然後把執行後的 Promise 裝到 CONCURRENT_PROMISES 裡面,一直到 247 行的部分才取出來給 newData,也順便記錄了一下開始的時間到 CONCURRENT_PROMISES_TS,所以剛剛在前面的 if 判斷式才可以在重複打請求的時候直接從上方兩個變數中用 key 拿資料。

打完 API 就是開始把新資料放到 cache 裡面,然後觸發 dispatch 囉:

https://github.com/vercel/swr/blob/00c5847ce6e95902920933aa1d49c7143f8ce4ba/src/use-swr.ts#L473

感覺過很久又終於提到了 cachedispatch,該時候來說說他們做了什麼了。

cache

cache 的程式碼除了剛剛說到的 serializeKey 之外,就是用 JavaScript 的 Map 管理所有 key 值對應的資料(在下方程式碼中的第 5 行),而上述提到的 cache.getcache.set 就是去做取出和設置的事情(分別在 13 和 18 行),其他大家有興趣可以再自己多看看 😂:

https://github.com/vercel/swr/blob/00c5847ce6e95902920933aa1d49c7143f8ce4ba/src/cache.ts#L4

dispatch

dispatch 只有短短的 19 行,在做的事情也相當單純,那就是去比對當前存在 stateRef 的資料,如果新資料和原本的資料相同就直接 continue,不同就更新,並執行 rerender 讓 component 用新的 stateRef 更新畫面:

https://github.com/vercel/swr/blob/00c5847ce6e95902920933aa1d49c7143f8ce4ba/src/use-swr.ts#L306

這裡有個值得注意的地方,那就是在上方 314 行的判斷 stateDependencies.current[key],一開始定義 stateDependencies 的地方在 stateRef 的上方,stateDependenciesstateRef 有著相同的結構,分別是 dataerrorisValidating,而在當初定義的時候全部都是 false,也就是說,如果 stateDependencies 一直是 false,就不會觸發到 rerender 對吧?

stateDependencies 究竟是用在哪裡呢?讓我們先繼續往下看到 useSWR 所回傳的資料吧!

memoizedState

這個 memoizedState 就是最後從 useSWR 回傳的資料啦:

從上方的程式碼中可以發現,SWR 用了 getter 讓使用者在取得 data 的時候可以跑一些取得資料外的邏輯,然後多跑的邏輯就是 stateDependencies.current.data = true 這行!這就代表著,只有在外部真的去使用了 data 這個資料時,stateDependencies.current.data 才會變成 true。

這個部分就和剛剛在 dispatch 內對 stateDependencies.current[key] 的判斷串連起來了!SWR 只會在你真正使用到 dataerror 或是 isValidating 的時候才會去做 rerender,以防止你根本就沒用到的東西,改變了還需要去做 rerender

SWR 為了效能所做的貢獻也不只如此,大家一路讀下來應該都有發現,不論是 dispatchrevalidate 或最後回傳的 memoizedState 也都用了 useCallbackuseMemo 去記住運算的結果或方法,並只在必要的時候重新產生,這些部分都減少了許多不必要的運算。

第二次取資料

看到現在的各位相信都應該了解原理了!如果在第一次請求結束後,用同樣的 key 再向 useSWR 要資料會發生什麼事呢?那就是在建立 stateRef 的時候就會先透過 resolveData 取得上一次 cache 的資料,所以回傳的 memoizedState 就會先回傳上一次的資料,而不是 undefined。

接著一樣執行到 useIsomorphicLayoutEffect 的時候就會遇到命運的交叉口了:

https://github.com/vercel/swr/blob/00c5847ce6e95902920933aa1d49c7143f8ce4ba/src/use-swr.ts#L581

如果當前已經有舊資料的話,就換成執行 rAFrAf 一樣會去判斷是不是在 server 的環境,然後再去判斷如果是 client 的話,當前的瀏覽器有沒有 requestAnimationFrame,沒有的話就用 setTimeout 建立一個未來發生的事件,也就是等當前所有的事情都就緒後,才執行 revalidate 取得最新的資料:

https://github.com/vercel/swr/blob/00c5847ce6e95902920933aa1d49c7143f8ce4ba/src/use-swr.ts#L33

執行 revalidate 之後取得資料的動作就像剛剛說的那樣子啦!

結論

這大概是我繼 redux-thunk(才 14 行而已 😂)後第一次那麼認真的閱讀開源專案的程式碼,就整理一下個人在 useSWR 裡有看到哪些覺得很酷的地方:

  1. useRef 來管理資料,並把 useState 當作讓 component 重新 render 的工具人這真的很酷!
  2. 用 Boolean 值的 loading 搭配 setTimeout 處理請求時間過長,第一次那麼近距離的看到處理請求時間過長的邏輯,雖然簡單但很實用!
  3. stateDependencies 搭配 getter 去判斷是不是有用到的資料,只在有用到的資料改變的時候才觸發 rerender,這小巧思好讚!

大家看完本篇文章後,有哪些寫法也讓你覺得眼睛一亮的呢?歡迎在下面留言和我分享哦!🙌

最後如果文章中有任何問題、還想看哪些開源專案的解析,或是好奇哪些功能是如何實現的,再麻煩大家留言告訴我了!非常感謝! 🙏

--

--