超香的react data fetching library - SWR,與嘗試解釋它背後的實作原理
SWR 介紹
- 是美國
Vercel
公司出的一個 library, react Hook base,Vercel 公司在 2020 年 4 月從以前的名字ZEIT
改名,它們就是做 react 社群裡很有名的 Server-side-rendering 框架 -next.js
,還有Now
這個用來幫助 web 開發者快速部署 web app 的服務的新創公司。 SWR
就是一個 custom Hook,export 出來的 API 也很簡單,就useSWR
,mutate
,trigger
這三個。它想要簡化 react app 裡,打 API,取資料,更新各處有用到相同的資料的 UI 這樣的事- 它有一套 cache 機制,會將前一次 fetch 過 API 的結果儲存起來,在每次再打 API 之前,就能先 return 就 cache 裡的資料,等到 API response 後,再更新畫面,在適當的時機並且用得好的話還能夠減少向後端 API 的請求次數。
- 它使用用一個
key
的概念來作為辨識,就算在不同的 component 中useSWR
,如果給的key
相同,就會拿到同樣的資料。 key
在最簡單的使用情境裡會用rest API路徑
來做辨別,再稍微複雜一點時就是把打 API 時的請求參數
(request parameter)也作為最後產生 key 的一個依據,所以代表如果請求參數不同,key 就會不同,這是使用時可以自己控制的。i.e. 使用時可以把請求參數做為 key 的依據,也可以不考慮。- 有一套手動讓畫面上用到相同的 key(bound mutate 的情況) 或是特定的 key(global mutate 的情況)的時候,使其他 component 用新的資料 re-render 的機制,非常強勁,本文將會嘗試解說它是如何做到這樣。
往下看之前,必須先知道的知識
- 讀者必須對 react hooks 已經蠻熟悉,常常在使用。
- 知道 react hooks 裡的 stale closure 的狀況,如果沒聽過這個詞的話,可以參考一樣是本站的這篇文章,裡面很詳細地介紹了 react hook 的 stale closure 的特徵跟為什麼會有 stale closure
TL;DR
useSWR 的
快取機制
是把它放在一個在該模組中的 ES6-Map 裡(所以還是算是 private,並沒有被 export 出來),每次必須要 return 值出去之前,先用key
去該 Map 裡找是否有上一次的資料,有的話就先 return 上一次的值。 這一點跟一般的 custom Hook 不同,以前人們寫的 customHook 通常會用useState
,useReducer
...去作為資料的承載者,然後利用對應的 setState 來讓用了這個 custom Hook 的 component re-render(重新執行),以讓該 component 拿到新的結果,
但是用一個共同的 Map 是有道理的,因為如果不這樣做的話,在不同的 component 裡useSWR
,然後 key 相同的話就沒辦法拿到同樣的資料了。useSWR 有一個叫
mutate
的 API,它可以讓使用者手動指定用了哪個 key 的那些 useSWR 呼叫的 component,使他們接到新的資料,並 re-rerender,以用新的資料來更新畫面,有點像是 redux 那樣,有新的資料時讓各處有用到那個新的資料的地方,一起更新。
這也是一個殺手級的功能,SWR
用了一個很聰明的useState
跟useCallback
的巧妙組合技,很輕鬆地就實作出了這樣好像是通知(notify)
的功能。而且要能夠做到這樣,很重要的一個條件是 react Hooks 有 stale closure 這樣的特性。這一部分是我這篇文章裡會花最多篇幅去解釋的,所以如果讀者有興趣的話,不妨繼續閱讀下去,一定會讓您有些收穫。
Dive to Code.....
SWR 的源碼目前還不算太複雜,主要的東西也幾乎都寫在同一個檔案裡,在這裡可以看到 src/use-swr.ts,
雖然還沒很複雜但是這檔案也有快 800 行,因為它的註解寫得還蠻詳細的,不過要全部看過還是有點累,所以我已經寫了一個較簡單的版本,在 codesandbox 上,只有 80 行不到,當然它就沒有涵蓋到原版的全部功能,只是稍微簡單地實作了快取
與通知
的功能,但是這已經可以讓我們學到很多了!
如果先懂得我下面想講的概念,再去看 SWR 的 source code 絕對是如虎添翼啊啊啊啊
但是注意本文寫作時間是在 2020/05/22,SWR 0.2.2 版,無法保證未來的實作機制是不是會有所改變
在上面的實作中,我們按下其中一個按鈕,onClick 裡是 Math.random 出一個亂數,然後使用useSWR(key, fetcher)
之後得到的mutate
,透過mutate
將新的值傳入,就會讓其他二個地方一起被更新,因為它們都使用了相同的 key: /123
,
但是在 code 中我們看不到傳統的上到下的 state 傳遞,也沒有redux
, 也沒有Context
這是怎麼做到的呢?
import React, { useEffect, useState, useCallback } from 'react'
const cache = new Map() // 用來放快取資料的Map
const REVALIDATORS = new Map() // 用來放資料更新時 要通知的component的Map, revalidators 指的就是那些對應的key有新的資料時需要被 re-render的component們
let REVALIDATOR_ID = 0
const getRevalidator_id = () => REVALIDATOR_ID
const changeRevalidator_id = () => {
REVALIDATOR_ID += 1
}
const iteratorAndNotifySubscribers = key => {
const subscribers = REVALIDATORS.get(key)
const keys = Object.keys(subscribers)
keys.forEach(k => subscribers[k] && subscribers[k]()) // 這個subscribers[k] 拿出來的就是 setState,執行了每個同樣的key的 useSWR之後的component,我們call它的setState 就能讓那些component re-render,而它們re-render再次執行useSWR時就會從cache裡用key取到新的值
}
const notifySubscribers = async (key, fetcherOrData) => {
if (typeof fetcherOrData === 'function') {
const fetcher = fetcherOrData
const data = await fetcher()
cache.set(key, data)
iteratorAndNotifySubscribers(key)
} else {
const data = fetcherOrData
cache.set(key, data)
iteratorAndNotifySubscribers(key)
}
}
const useSWR = (key, fetcher) => {
const _key = key
const makeReRender = useState(null)[1] // 每次新執行一次 useSWR(...) 就會利用 useState得到一個 setState,因為useState是return一個array,[1] 就能取到setter, 我們沒有要真的用到[0]的state,所以不需要拿到state
const dispatch = useCallback(() => {
// 把這個state setter放進useCallback裡,它就會被useCallback造成的 stale closure `封住`,setter就不會再被變動,這是之後我們要做notify時要使用的
// ({}) 是非常關鍵的一手,它會讓我們 dispatch() 時 就一定會讓這個 useSWR的component re-render
makeReRender({}) // key point
}, [key])
const boundMutate = useCallback(
// boundMutate, 使用時不用給key, 因為key已經被 useCallback的stale closure封住在裡面,不會再改變。而data是在呼叫 boundMutate(data) 時傳入,所以會是變動的值
data => {
notifySubscribers(key, data)
},
[]
)
useEffect(() => {
const _revalidator_id = getRevalidator_id() + 1
const subscribers = REVALIDATORS.get(_key)
if (!subscribers) {
const initSubScribersHolder = Object.create(null)
initSubScribersHolder[_revalidator_id] = dispatch // 把setState放進對應的 key 和revalidator_id的位置,因為會有多個component用到同樣的key,所以必須是 {key: {id: dispatch}} 這樣的兩層HahsTable (第二層是用object)
REVALIDATORS.set(_key, initSubScribersHolder)
} else {
subscribers[_revalidator_id] = dispatch
}
changeRevalidator_id()
return () => {
// coponent要從畫面上消失時,要將它從通知清單裡清除掉
const subscribers = REVALIDATORS.get(_key) // 注意這個_key也是被stale closure抓進去,這就讓我們在清除的時候不必再去想要怎麼得到key,在宣告clear function的當下 它就已經被抓進去了
subscribers[_revalidator_id] = null // 這個_revalidator_id也是,宣告的當下被抓進 clear function裡,而且不可能再被改變
}
}, [])
useEffect(() => {
notifySubscribers(key, fetcher)
}, [key])
return {
data: cache.get(key),
mutate: boundMutate,
}
}
export { useSWR }
notify 機制的實作關鍵
每次某個 component 裡執行第一次 useSWR(...)時 就會呼叫 useState 得到一個 setState,這邊取名叫 makeReRender
const makeReRender = useState(null)[1]
把它 makeReRender 用 useCallback 包起來,利用 stale closure 的特性,讓這個 function 是個 constant,
makeReRender({})
傳進去的{}
是非常關鍵的,沒有它不行,沒有它的話,就沒辦法 make that component that invoke useSWR re-render 了。
原碼 Line197
const dispatch = useCallback(() => {
makeReRender({}) // key point
}, [key])
利用呼叫useSWR
時傳入的 key,把每次第一次呼叫 useSWR 時得到的setState
存在一個 Map 裡,因為可能有很多個 component 用到同樣的 key,這 Map 必須是兩層的,
useEffect(() => {
const _revalidator_id = getRevalidator_id() + 1
const subscribers = REVALIDATORS.get(_key)
if (!subscribers) {
const initSubScribersHolder = Object.create(null)
initSubScribersHolder[_revalidator_id] = dispatch // 把setState放進對應的 key 和revalidator_id的位置,因為會有多個component用到同樣的key,所以必須是 {key: {id: dispatch}} 這樣的兩層HahsTable (第二層是用object)
REVALIDATORS.set(_key, initSubScribersHolder)
} else {
subscribers[_revalidator_id] = dispatch
}
changeRevalidator_id()
return () => {
// coponent要從畫面上消失時,要將它從通知清單裡清除掉
const subscribers = REVALIDATORS.get(_key) // 注意這個_key也是被stale closure抓進去,這就讓我們在清除的時候不必再去想要怎麼得到key,在宣告clear function的當下 它就已經被抓進去了
subscribers[_revalidator_id] = null // 這個_revalidator_id也是,宣告的當下被抓進 clear function裡,而且不可能再被改變
}
}, [])
當外部呼叫mutate(data)
的時候,利用 key,把對應的setState
取出來,然後呼叫
(下面 code 裡最後一個(),所以subscribers[k]
就是某個setState
),
data 並沒有傳入到 setState 裡,因為在呼叫這裡之前就已經把 cache 裡的 data 更新,我們只是要利用useState
之後會讓 component 再次執行的機制,
使各個 component 再次執行useSWR
,流程中就會從 cache 裡把新的值取出來,然後 return 到 component 上
這流程類似源碼的 Line59
const iteratorAndNotifySubscribers = key => {
const subscribers = REVALIDATORS.get(key)
const keys = Object.keys(subscribers)
keys.forEach(k => subscribers[k] && subscribers[k]()) // 這個subscribers[k] 拿出來的就是 setState,執行了每個同樣的key的 useSWR之後的component,我們call它的setState 就能讓那些component re-render,而它們re-render再次執行useSWR時就會從cache裡用key取到新的值
}
to sum up
- 把各個 setState function 存在一個 Map 裡
- 在資料有變動時(自己手動發動 mutate 或 trigger 來告訴 SWR 資料有變動),更新快取裡的資料,抓出對應的
setState
,發動,使其觸動 component 的 re-render(源碼在這邊還有做很多更細微的操作,如打 API 的時候在 fetch 完之前,先更新isValidating
,讓各個 component 知道現在正在打 API 中 Line227) - 各個有用到同樣的 key 的 component 們就會因為再次執行
useSWR
而拿到新的資料,更新 return 的 UI 描述。