App下載

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

猿友 2020-09-21 10:48:03 瀏覽數(shù) (8513)
反饋

文章來源于公眾號:前端瓶子君

swr是一個hook組件,可以作為請求庫和狀態(tài)管理庫,本文主要介紹一下在項目中如何實戰(zhàn)使用swr,并且會解析一下swr的原理。從原理出發(fā)讀一讀swr的源碼

  • 什么是swr
    - swr的的源碼

一、什么是swr

useSWRreact hooks 中一個比較有意思的組件,既可以作為請求庫,也可以作為狀態(tài)管理的緩存用,SWR 的名字來源于“stale-while-revalidate”, 是在HTTP RFC 5861標準中提出的一種緩存更新策略 :

首先從緩存中取數(shù)據(jù),然后去真實請求相應(yīng)的數(shù)據(jù),最后將緩存值和最新值做對比,如果緩存值與最新值相同,則不用更新,否則用最新值來更新緩存,同時更新UI展示效果。

useSWR 可以作為請求庫來用:

//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 也可以當(dāng)作全局的狀態(tài)管理機。比如可以全局保存用戶名稱 :

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 分別是什么?

答案是:

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

原因也很簡單,在swr默認的時間內(nèi)(默認是 2000 毫秒),對于同一個 useSWRkey ,這里的 key‘/api/user’ 會進行重復(fù)值清除, 只始終 2000 毫秒內(nèi)第一個 keyfetcher 函數(shù)來進行緩存更新。

帶著這個例子,我們來深入讀讀 swr 的源碼

二、swr的源碼

我們從 useSWR 的 API 入手,來讀一讀 swr 的源碼。首先在 swr 中本質(zhì)是一種內(nèi)存中的緩存更新策略,所以在 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 類的定義,本質(zhì)其實很簡單,維護了一個 map 對象,以 key 為索引,其中key 可以是字符串,函數(shù)或者數(shù)組,將 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 是函數(shù),那么執(zhí)行這個函數(shù),返回的結(jié)果就是序列化后的 key
  • 如果傳入的 key 是數(shù)組,那么通過 hash 方法(類似 hash 算法,數(shù)組的值序列化后唯一)序列化后的值就是 key 。

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

(2)事件處理

在swr中,可以配置各種事件,當(dāng)事件被觸發(fā)時,會觸發(fā)相應(yīng)的重新請求或者說更新函數(shù)。swr對于這些事件,比如斷網(wǎng)重連,切換 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 事件中保存了相應(yīng)的更新緩存函數(shù),當(dāng)頁面觸發(fā)事件visibilitychange(顯示隱藏)、focus(頁面聚焦)以及online(斷網(wǎng)重連)的時候會觸發(fā)事件,自動更新緩存 。

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

useSWR 是swr的主體函數(shù),決定了如何緩存以及如何更新,我們先來看 useSWR 的入?yún)⒑托螀ⅰ?/p>

入?yún)?

  • key : 一個唯一值,可以是字符串、函數(shù)或者數(shù)組,用來在緩存中唯一標識 key
  • fetcher : (可選) 返回數(shù)據(jù)的函數(shù)
  • options : (可選)對于 useSWR 的一些配置項,比如事件是否自動觸發(fā)緩存更新等等。

出參:

  • data : 與入?yún)?key 相對應(yīng)的,緩存中相應(yīng) keyvalue
  • error : 在請求過程中產(chǎn)生的錯誤等
  • isValidating : 是否正在請求或者正在更新緩存中,可以做為 isLoading 等標識用。
  • mutate(data?, shouldRevalidate?) : 更新函數(shù),手動去更新相應(yīng) keyvalue

從入?yún)⒌匠鰠?,我們本質(zhì)在做的事情,就是去控制 cache 實例,這個 map 的更新的關(guān)鍵是:

什么時候需要直接從緩存中取值,什么時候需要重新請求,更新緩存中的值

const stateRef = useRef({
    data: initialData,
    error: initialError,
    isValidating: false
})
const CONCURRENT_PROMISES = {}  //以key為鍵,value為新的通過fetch等函數(shù)返回的值
const CONCURRENT_PROMISES_TS = {} //以key為鍵,value為開始通過執(zhí)行函數(shù)獲取新值的時間戳

下面我們來看,緩存更新的核心函數(shù):revalidate


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


      // start fetching
      try {
        dispatch({
          isValidating: true
        })


        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

0 人點贊