Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React hooks中swr的原理和源码解析 #61

Open
forthealllight opened this issue Jul 30, 2020 · 0 comments
Open

React hooks中swr的原理和源码解析 #61

forthealllight opened this issue Jul 30, 2020 · 0 comments

Comments

@forthealllight
Copy link
Owner

forthealllight commented Jul 30, 2020

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


    swr是一个hook组件,可以作为请求库和状态管理库,本文主要介绍一下在项目中如何实战使用swr,并且会解析一下swr的原理。从原理出发读一读swr的源码

  • 什么是swr
  • swr的的源码

一、什么是swr

    useSWR是react hooks中一个比较有意思的组件,既可以作为请求库,也可以作为状态管理的缓存用,SWR的名字来源于“stale-while-revalidate”, 是在HTTP RFC 5861标准中提出的一种缓存更新策略 :

首先从缓存中取数据,然后去真实请求相应的数据,最后将缓存值和最新值做对比,如果缓存值与最新值相同,则不用更新,否则用最新值来更新缓存,同时更新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也可以当作全局的状态管理机。比如可以全局保存用户名称 :

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默认的时间内(默认是2000毫秒),对于同一个useSWR的key,这里的key是‘/api/user’会进行重复值清除, 只始终2000毫秒内第一个key的fetcher函数来进行缓存更新。

    带着这个例子,我们来深入读读swr的源码

二、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类中,将这个保存了key和value信息的缓存对象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_REVALIDATORS,RECONNECT_REVALIDATORS事件中保存了相应的更新缓存函数,当页面触发事件visibilitychange(显示隐藏)、focus(页面聚焦)以及online(断网重连)的时候会触发事件,自动更新缓存

(3)useSWR 缓存更新的主体函数

    useSWR是swr的主体函数,决定了如何缓存以及如何更新,我们先来看useSWR的入参和形参。

入参:

  • key: 一个唯一值,可以是字符串、函数或者数组,用来在缓存中唯一标识key
  • fetcher: (可选) 返回数据的函数
  • options: (可选)对于useSWR的一些配置项,比如事件是否自动触发缓存更新等等。

出参:

  • data: 与入参key相对应的,缓存中相应key的value值
  • error: 在请求过程中产生的错误等
  • isValidating: 是否正在请求或者正在更新缓存中,可以做为isLoading等标识用。
  • mutate(data?, shouldRevalidate?): 更新函数,手动去更新相应key的value值

从入参到出参,我们本质在做的事情,就是去控制cache实例,这个map的更新的关键是:

什么时候需要直接从缓存中取值,什么时候需要重新请求,更新缓存中的值

const stateRef = useRef({
    data: initialData,
    error: initialError,
    isValidating: false
})
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({ 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 <= MUTATION_TS[key] ||
            
              startAt <= MUTATION_END_TS[key] ||
           
              MUTATION_END_TS[key] === 0))

        if (shouldIgnoreRequest) {
          dispatch({ isValidating: false })
          return false
        }

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

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

        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({
    data: false,
    error: false,
    isValidating: false
})
const stateRef = useRef({
    data: initialData,
    error: initialError,
    isValidating: false
})
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函数中,我们根据需要去更新stateRef,stateRef的返回值,就是最终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这个对象,其key为cache中的key,value为最新的值,那么如何在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方法会在每一次更新cache的key的时候触发,而CACHE_REVALIDATORS保存了所有与key相关的更新函数,这里需要注意的是:

为什么CACHE_REVALIDATORS[key]的值是一个数组?

    因为useSWR的key,同一个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的值可以是直接从缓存中取,或者是手动传入(类似于乐观更新的方式)。

@forthealllight forthealllight changed the title 如何优雅的在react hooks中使用swr React hooks中swr的使用和源码解析 Jul 31, 2020
@forthealllight forthealllight changed the title React hooks中swr的使用和源码解析 React hooks中swr的原理和源码解析 Aug 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant