React hooks中swr的原理和源碼解析

受權轉載自:JonyYu 前端

https://github.com/forthealllight/blog/issues/61react

swr是一個hook組件,能夠做爲請求庫和狀態管理庫,本文主要介紹一下在項目中如何實戰使用swr,而且會解析一下swr的原理。從原理出發讀一讀swr的源碼ios

  • 什麼是swr
  • swr的的源碼

1、什麼是swr

useSWR 是 react hooks 中一個比較有意思的組件,既能夠做爲請求庫,也能夠做爲狀態管理的緩存用,SWR 的名字來源於「stale-while-revalidate」, 是在HTTP RFC 5861標準中提出的一種緩存更新策略 :git

首先從緩存中取數據,而後去真實請求相應的數據,最後將緩存值和最新值作對比,若是緩存值與最新值相同,則不用更新,不然用最新值來更新緩存,同時更新UI展現效果。github

useSWR 能夠做爲請求庫來用:web

//fetch
import useSWR from 'swr'
import fetch from 'unfetch'
const fetcher = url => fetch(url).then(r => r.json())
function App ({
  const { data, error } = useSWR('/api/data', fetcher)
  // ...
}

//axios
const fetcher = url => axios.get(url).then(res => res.data)
function App ({
  const { data, error } = useSWR('/api/data', fetcher)
  // ...
}

//graphql
import { request } from 'graphql-request'
const fetcher = query => request('https://api.graph.cool/simple/v1/movies', query)
function App ({
  const { data, error } = useSWR(
    `{
      Movie(title: "Inception") {
        releaseDate
        actors {
          name
        }
      }
    }`
,
    fetcher
  )
  // ...
}

此外,由於相同的 key 老是返回相同的實例,在 useSWR 中只保存了一個 cache 實例,所以 useSWR 也能夠看成全局的狀態管理機。好比能夠全局保存用戶名稱 :面試

import useSWR from 'swr';
function useUser(id: string{
  const { data, error } = useSWR(`/api/user`, () => {
    return {
      name'yuxiaoliang',
      id,
    };
  });
  return {
    user: data,
    isLoading: !error && !data,
    isError: error,
  };
}
export default useUser;

具體的 swr 的用法不是本文的重點,具體能夠看文檔,本文用一個例子來引出對於 swr 原理的理解:算法

const sleep = async (times: number) => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, times);
    });
};
const { data: data500 } = useSWR('/api/user'async () => {
    await sleep(500);
    return { a'500 is ok' };
});
const { data: data100 } = useSWR('/api/user'async () => {
    await sleep(100);
    return { a'100 is ok' };
});

上述的代碼中輸出的是 data100 和 data500 分別是什麼?編程

答案是:json

data100和data500都輸出了{a:'500 is ok '}

緣由也很簡單,在swr默認的時間內(默認是 2000 毫秒),對於同一個 useSWRkey ,這裏的 key‘/api/user’ 會進行重複值清除, 只始終 2000 毫秒內第一個 keyfetcher 函數來進行緩存更新。

帶着這個例子,咱們來深刻讀讀 swr 的源碼

2、swr的源碼

咱們從 useSWR 的 API 入手,來讀一讀 swr 的源碼。首先在 swr 中本質是一種內存中的緩存更新策略,因此在 cache.ts 文件中,保存了緩存的 map

(1)cache.ts 緩存

class Cache implements CacheInterface {
 

  constructor(initialData: any = {}) {
    this.__cache = new Map(Object.entries(initialData))
    this.__listeners = []
  }

  get(key: keyInterface): any {
    const [_key] = this.serializeKey(key)
    return this.__cache.get(_key)
  }

  set(key: keyInterface, value: any): any {
    const [_key] = this.serializeKey(key)
    this.__cache.set(_key, value)
    this.notify()
  }

  keys() {
    
  }

  has(key: keyInterface) {
  
  }

  clear() {
  
  }

  delete(key: keyInterface) {
  
  }
  serializeKey(key: keyInterface): [string, any, string] {
    let args = null
    if (typeof key === 'function') {
      try {
        key = key()
      } catch (err) {
        // dependencies not ready
        key = ''
      }
    }

    if (Array.isArray(key)) {
      // args array
      args = key
      key = hash(key)
    } else {
      // convert null to ''
      key = String(key || '')
    }

    const errorKey = key ? 'err@' + key : ''

    return [key, args, errorKey]
  }

  subscribe(listener: cacheListener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    let isSubscribed = true
    this.__listeners.push(listener)
    return () => {
       //unsubscribe
    }
  }

  // Notify Cache subscribers about a change in the cache
  private notify() {
   
  }

上述是 cache 類的定義,本質其實很簡單,維護了一個 map 對象,以 key 爲索引,其中key 能夠是字符串,函數或者數組,將 key 序列化的方法爲:serializeKey

 serializeKey(key: keyInterface): [string, any, string] {
    let args = null
    if (typeof key === 'function') {
      try {
        key = key()
      } catch (err) {
        // dependencies not ready
        key = ''
      }
    }

    if (Array.isArray(key)) {
      // args array
      args = key
      key = hash(key)
    } else {
      // convert null to ''
      key = String(key || '')
    }

    const errorKey = key ? 'err@' + key : ''

    return [key, args, errorKey]
  }

從上述方法的定義中咱們能夠看出:

  • 若是傳入的 key 是字符串,那麼這個字符串就是序列化後的 key
  • 若是傳入的 key 是函數,那麼執行這個函數,返回的結果就是序列化後的 key
  • 若是傳入的 key 是數組,那麼經過 hash 方法(相似 hash 算法,數組的值序列化後惟一)序列化後的值就是 key

此外,在 cache 類中,將這個保存了 keyvalue 信息的緩存對象 map ,保存在實例對象 this.__cache 中,這個 this.__cache 對象就是一個 map ,有set get等方法。

(2)事件處理

在swr中,能夠配置各類事件,當事件被觸發時,會觸發相應的從新請求或者說更新函數。swr對於這些事件,好比斷網重連,切換 tab 從新聚焦某個 tab 等等,默認是會自動去更新緩存的。

在swr中對事件處理的代碼爲

const revalidate = revalidators => {
    if (!isDocumentVisible() || !isOnline()) return

    for (const key in revalidators) {
      if (revalidators[key][0]) revalidators[key][0]()
    }
  }

  // focus revalidate
  window.addEventListener(
    'visibilitychange',
    () => revalidate(FOCUS_REVALIDATORS),
    false
  )
  window.addEventListener('focus', () => revalidate(FOCUS_REVALIDATORS), false)
  // reconnect revalidate
  window.addEventListener(
    'online',
    () => revalidate(RECONNECT_REVALIDATORS),
    false
)

上述 FOCUS_REVALIDATORSRECONNECT_REVALIDATORS 事件中保存了相應的更新緩存函數,當頁面觸發事件visibilitychange(顯示隱藏)、focus(頁面聚焦)以及online(斷網重連)的時候會觸發事件,自動更新緩存

(3)useSWR 緩存更新的主體函數

useSWR 是swr的主體函數,決定了如何緩存以及如何更新,咱們先來看 useSWR 的入參和形參。

入參:

  • key : 一個惟一值,能夠是字符串、函數或者數組,用來在緩存中惟一標識 key
  • fetcher : (可選) 返回數據的函數
  • options : (可選)對於 useSWR 的一些配置項,好比事件是否自動觸發緩存更新等等。

出參:

  • data : 與入參 key 相對應的,緩存中相應 keyvalue
  • error : 在請求過程當中產生的錯誤等
  • isValidating : 是否正在請求或者正在更新緩存中,能夠作爲 isLoading 等標識用。
  • mutate(data?, shouldRevalidate?) : 更新函數,手動去更新相應 keyvalue

從入參到出參,咱們本質在作的事情,就是去控制 cache 實例,這個 map 的更新的關鍵是:

何時須要直接從緩存中取值,何時須要從新請求,更新緩存中的值

const stateRef = useRef({
    data: initialData,
    error: initialError,
    isValidatingfalse
})
const CONCURRENT_PROMISES = {}  //以key爲鍵,value爲新的經過fetch等函數返回的值
const CONCURRENT_PROMISES_TS = {} //以key爲鍵,value爲開始經過執行函數獲取新值的時間戳

下面咱們來看,緩存更新的核心函數:revalidate

  // start a revalidation
  const revalidate = useCallback(
    async (
      revalidateOpts= {}
    ) => {
      if (!key || !fn) return false
      revalidateOpts = Object.assign({ dedupefalse }, revalidateOpts)
      let loading = true
      let shouldDeduping =
        typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe

      // start fetching
      try {
        dispatch({
          isValidatingtrue
        })

        let newData
        let startAt

        if (shouldDeduping) {
        
          startAt = CONCURRENT_PROMISES_TS[key]
          newData = await CONCURRENT_PROMISES[key]
          
        } else {
         
          if (fnArgs !== null) {
            CONCURRENT_PROMISES[key] = fn(...fnArgs)
          } else {
            CONCURRENT_PROMISES[key] = fn(key)
          }

          CONCURRENT_PROMISES_TS[key] = startAt = Date.now()

          newData = await CONCURRENT_PROMISES[key]

          setTimeout(() => {
            delete CONCURRENT_PROMISES[key]
            delete CONCURRENT_PROMISES_TS[key]
          }, config.dedupingInterval)

        }

        const shouldIgnoreRequest =
        
          CONCURRENT_PROMISES_TS[key] > startAt ||
          
          (MUTATION_TS[key] &&
         
            (startAt <= MUTATION_TS[key] ||
            
              startAt <= MUTATION_END_TS[key] ||
           
              MUTATION_END_TS[key] === 0))

        if (shouldIgnoreRequest) {
          dispatch({ isValidatingfalse })
          return false
        }

        cache.set(key, newData)
        cache.set(keyErr, undefined)

        // new state for the reducer
        const newState: actionType<Data, Error> = {
          isValidatingfalse
        }

        if (typeof stateRef.current.error !== 'undefined') {
          // we don't have an error
          newState.error = undefined
        }
        if (!config.compare(stateRef.current.data, newData)) {
          // deep compare to avoid extra re-render
          // data changed
          newState.data = newData
        }

        // merge the new state
        dispatch(newState)

        if (!shouldDeduping) {
          // also update other hooks
          broadcastState(key, newData, undefined)
        }
      } catch (err) {
        // catch err
      }

      loading = false
      return true
    },
    [key]
  )

上述代碼已經經過簡化, dispatch 就是更新 useSWR 返回值的函數:

const stateDependencies = useRef({
    datafalse,
    errorfalse,
    isValidatingfalse
})
const stateRef = useRef({
    data: initialData,
    error: initialError,
    isValidatingfalse
})
let dispatch = useCallback(payload => {
let shouldUpdateState = false
for (let k in payload) {
  stateRef.current[k] = payload[k]
  if (stateDependencies.current[k]) {
    shouldUpdateState = true
  }
}
if (shouldUpdateState || config.suspense) {
  if (unmountedRef.current) return
  rerender({})
 }
}, [])

在上述的 dispath 函數中,咱們根據須要去更新 stateRefstateRef 的返回值,就是最終 useSWR 的返回值,這裏的 rerender 是一個react hooks中的強制更新的一個hook:

const rerender = useState(null)[1]

每次執行 rerender({}) 的時候,就會觸發所在 hook 函數內組件的總體更新。其次咱們還要再一次明確:

const CONCURRENT_PROMISES = {}  //以key爲鍵,value爲新的經過fetch等函數返回的值
const CONCURRENT_PROMISES_TS = {} //以key爲鍵,value爲開始經過執行函數獲取新值的時間戳

接着來看 revalidate 更新函數的核心部分:

    let shouldDeduping =
        typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe
    let newData
    let startAt

    if (shouldDeduping) {
    
      startAt = CONCURRENT_PROMISES_TS[key]
      newData = await CONCURRENT_PROMISES[key]
      
    } else {
     
      if (fnArgs !== null) {
        CONCURRENT_PROMISES[key] = fn(...fnArgs)
      } else {
        CONCURRENT_PROMISES[key] = fn(key)
      }

      CONCURRENT_PROMISES_TS[key] = startAt = Date.now()

      newData = await CONCURRENT_PROMISES[key]

      setTimeout(() => {
        delete CONCURRENT_PROMISES[key]
        delete CONCURRENT_PROMISES_TS[key]
      }, config.dedupingInterval)

    }

上述代碼中, shouldDeduping 是用來判斷是否須要去重的依據,從上述代碼能夠看出 config.dedupingInterval 的默認值是 2000 毫秒,也就是在 2000 毫秒內,對於同一個 key 會去重,也就是說,若是 2000 毫秒內,對於同一個 key ,同時發起了多個更新函數,那麼會以第一次更新的結果爲準。以 key 爲鍵,記錄每一個 key 發起的時候的時間戳的數組是 CONCURRENT_PROMISES_TS ,而 CONCURRENT_PROMISES ,由此能夠看出,更準確 的說法是:

必定時間內,去重後的key和value的值的集合,key是useSWR中的惟一key,也就是cache實例map的key,value就是最新的緩存中更新過的值。

(4)useSWR 中如何更新

根據上述的代碼咱們知道了更新函數是怎麼樣的,在內存中保存了 CONCURRENT_PROMISES_TS 這個對象,其 keycache 中的 keyvalue 爲最新的值,那麼如何在 CONCURRENT_PROMISES_TS 對象 key 所對應的值發生變化的時候,去更新 useSWR 實例的返回值,從而達到咱們最終的緩存更新效果呢。

咱們接着來看代碼:

//保存對象
const CACHE_REVALIDATORS = {}

//具體更新函數
const onUpdate: updaterInterface<Data, Error> = (
  shouldRevalidate = true,
  updatedData,
  updatedError,
  dedupe = true
) => {
  // update hook state
  const newState: actionType<Data, Error> = {}
  let needUpdate = false

  if (
    typeof updatedData !== 'undefined' &&
    !config.compare(stateRef.current.data, updatedData)
  ) {
    newState.data = updatedData
    needUpdate = true
  }
  
  if (stateRef.current.error !== updatedError) {
    newState.error = updatedError
    needUpdate = true
  }
  //更新當前的stateRef
  if (needUpdate) {
    dispatch(newState)
  }

  if (shouldRevalidate) {
    return revalidate()
  }
  return false
}

//增長監聽key
const addRevalidator = (revalidators, callback) => {
    if (!callback) return
    if (!revalidators[key]) {
      revalidators[key] = [callback]
    } else {
      revalidators[key].push(callback)
    }
}
addRevalidator(CACHE_REVALIDATORS, onUpdate)

//更新緩存的方法
const broadcastState: broadcastStateInterface = (key, data, error) =>    {
      const updaters = CACHE_REVALIDATORS[key]
      if (key && updaters) {
        for (let i = 0; i < updaters.length; ++i) {
          updaters[i](false, data, error)
        }
      }
  }

broadcastState 方法會在每一次更新 cachekey 的時候觸發,而 CACHE_REVALIDATORS 保存了全部與 key 相關的更新函數,這裏須要注意的是:

爲何CACHE_REVALIDATORS[key]的值是一個數組?

由於 useSWRkey ,同一個 key 能夠有多個更新函數,所以 CACHE_REVALIDATORS[key] 是一個數組。

舉例來講,在同一個組件中使用兩個同名 key ,可是他們的更新函數不一樣,是被容許的:

 const { data: data500 } = useSWR('/api/user'async () => {
    await sleep(500);
    return { message'500 is ok' };
 });
 const { data: data100 } = useSWR('/api/user'async () => {
    await sleep(100);
    return { message'100 is ok' };
 });

(5)mutate 主動觸發更新函數

瞭解了useSWR 中的更新,那麼剩下的這個 mutate 就及其簡單:

const mutate: mutateInterface = async ()=>{
  let data, error

  if (_data && typeof _data === 'function') {
    // `_data` is a function, call it passing current cache value
    try {
      data = await _data(cache.get(key))
    } catch (err) {
      error = err
    }
  } else if (_data && typeof _data.then === 'function') {
    // `_data` is a promise
    try {
      data = await _data
    } catch (err) {
      error = err
    }
  } else {
    data = _data
  }
  
  ....
  
  const updaters = CACHE_REVALIDATORS[key]
  if (updaters) {
    const promises = []
    for (let i = 0; i < updaters.length; ++i) {
      promises.push(updaters[i](!!shouldRevalidate, data, error, i > 0))
    }
    // return new updated value
    return Promise.all(promises).then(() => {
      if (error) throw error
      return cache.get(key)
    })
  }
}

簡單的說就是拿到值,而後調用 const updaters = CACHE_REVALIDATORS[key] 數組中的每個更新函數,更新相應的 useSWR 的值便可。這裏 data 的值能夠是直接從緩存中取,或者是手動傳入(相似於樂觀更新的方式)。

最後

歡迎關注「前端瓶子君」,回覆「交流」加入前端交流羣!
歡迎關注「前端瓶子君」,回覆「算法」自動加入,從0到1構建完整的數據結構與算法體系!
在這裏(算法羣),你能夠天天學習一道大廠算法編程題(阿里、騰訊、百度、字節等等)或 leetcode,瓶子君都會在次日解答喲!
另外,每週還有手寫源碼題,瓶子君也會解答喲!
》》面試官也在看的算法資料《《
「在看和轉發」 就是最大的支持

本文分享自微信公衆號 - 前端瓶子君(pinzi_com)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索