{"id":18507728,"url":"https://github.com/chenbin92/swr-source-code","last_synced_at":"2025-09-01T12:07:40.872Z","repository":{"id":37974859,"uuid":"224178561","full_name":"chenbin92/swr-source-code","owner":"chenbin92","description":null,"archived":false,"fork":false,"pushed_at":"2023-01-05T01:43:53.000Z","size":1680,"stargazers_count":20,"open_issues_count":17,"forks_count":3,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-09T20:48:22.158Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chenbin92.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-11-26T11:41:30.000Z","updated_at":"2023-12-09T17:00:35.000Z","dependencies_parsed_at":"2023-02-03T04:32:34.863Z","dependency_job_id":null,"html_url":"https://github.com/chenbin92/swr-source-code","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/chenbin92/swr-source-code","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenbin92%2Fswr-source-code","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenbin92%2Fswr-source-code/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenbin92%2Fswr-source-code/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenbin92%2Fswr-source-code/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chenbin92","download_url":"https://codeload.github.com/chenbin92/swr-source-code/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenbin92%2Fswr-source-code/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273122120,"owners_count":25049539,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-01T02:00:09.058Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-06T15:12:16.104Z","updated_at":"2025-09-01T12:07:40.833Z","avatar_url":"https://github.com/chenbin92.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003e 说明：不同的分支对应不同功能的实现，可参照文章逐步实现完整的示例。\n\n# 深入浅出 SWR 原理\n\n本文主要是基于 SWR 源码对其原理进行分析，但并不会直接从源码开始，而是从实际需求场景一步一步推导进而实现 SWR 的功能，如果不了解 SWR 是什么，可以先看上一篇[《SWR：最具潜力的 React Hooks 请求库》](https://zhuanlan.zhihu.com/p/89570321)或者直接看 SWR 的官方介绍文档。\n\n![image.png](https://img.alicdn.com/tfs/TB1UsoYnubviK0jSZFNXXaApXXa-1492-686.png)\n\n\u003ca name=\"1f950e90\"\u003e\u003c/a\u003e\n## 目录\n\n- 需求场景\n- 简易模型\n- 功能迭代\n- [x] 自定义请求\n- [x] 全局配置项\n- [x] 依赖请求\n- [x] 焦点验证\n- [x] 其他功能\n- 原理分析\n- 总结\n\n\u003ca name=\"shOmB\"\u003e\u003c/a\u003e\n## 需求场景\n\n随着 React Hooks 的浪潮，各种基于 Hooks 的方案越来越多，其中主要包含 **状态管理**、**数据请求**、**通用功能的封装 **等等。而 **数据请求** 是日常业务开发中最常见的需求，那么在  Hooks 模式下，我们应该如何请求数据，先来看下面的一个简单示例。\n\n**产品需求**：首页通过接口获取 github trending 项目列表，然后点击列表项可查看单个项目的信息。\n\n![demo.gif](https://img.alicdn.com/tfs/TB1Jyvwobj1gK0jSZFOXXc7GpXa-1425-803.gif)\u003cbr /\u003e**程序实现**：接到需求后一顿操作，无非就是在数据请求时需要显示 loading 效果，数据获取完成时展示列表数据，以及考虑请求错误后的容错处理，稳健如飞的撸出了如下代码：\n\n```jsx\n// 首页列表实现\n\nconst Home = () =\u003e {\n  // 设置初始数据\n  const [data, setData] = useState([]) \n  // 设置初始状态\n  const [isLoading, setIsLoading] = useState(false)\n  // 设置初始错误值\n  const [isError, setIsError] = useState(false)\n\n  useEffect(() =\u003e {\n    // 定义 fetchFn \n    const fetchData = async () =\u003e {\n      setIsLoading(true)\n      try {\n        const result = await fetch('api/data')\n        setData(result)\n      } catch(error) {\n        setIsError(true)\n      }\n      setIsLoading(false)\n    }\n    // 调用接口\n    fetchData()\n  }, [])\n  \n  return (\n     \u003cdiv className='hero'\u003e\n        \u003ch1 className='title'\u003eTrending Projects\u003c/h1\u003e\n        {isError \u0026\u0026 \u003cdiv\u003eSomething went wrong ...\u003c/div\u003e}\n        \u003cdiv\u003e\n            {\n            \tisLoading ? 'loading...' :\n            \tdata.map(project =\u003e\n    \t\t\t\t\t\t\u003cp key={project}\u003e\u003cLink href='/[user]/[repo]' as={`/${project}`}\u003e\u003ca\u003e{project}\u003c/a\u003e\u003c/Link\u003e\u003c/p\u003e\n        \t\t\t)\n        \t\t}\n    \t\t\u003c/div\u003e\n    \u003c/div\u003e\n  )\n}\n```\n\n获取项目详情实现与上面基本一样，基础代码如下：\n\n```jsx\n// 项目详情实现\nconst project = () =\u003e {\n  const [data, setData] = useState([]) \n  const [isLoading, setIsLoading] = useState(false)\n  const [isError, setIsError] = useState(false)\n\n  useEffect(() =\u003e {\n    const fetchData = async () =\u003e {\n      setIsLoading(true)\n      try {\n        // 获取的 API 带了 id 参数\n        const result = await fetch(`api/data?id=${id}`)\n        setData(result)\n      } catch(error) {\n        setIsError(true)\n      }\n      setIsLoading(false)\n    }\n\n    fetchData()\n  }, [id])\n  \n  return ()\n}\n```\n\n如上面的例子所示，代码看上去很简洁，一个纯函数包含了数据请求时的请求状态、容错处理、数据更新，视图渲染，以及使用了 React 的 `useEffect` 和 `useState`  两个 Hooks API，很好的满足了场景需求。\n\n这看上去很好，但你可能存在一些疑惑，从示例代码可以看到获取项目列表和项目详情的 **数据请求部分的代码 **基本上是一样的，同样的代码重复写两遍，这显然是不能接受的，基于此通常的做法是对其进行一层抽象封装，实现逻辑的复用，具体如下。\n\n\u003ca name=\"4dBya\"\u003e\u003c/a\u003e\n## 简易模型\n\n基于重复的数据请求代码，对比发现只是 API 和初始数据值的不同，其他如设置 `data` ， `isLoading` ， `isError` 的逻辑都是一样，可以先将其进行一层抽象封装以便进行复用，简易模型如下：\n\n```jsx\nimport { useState, useEffect } from 'react'\nimport fetch from 'isomorphic-unfetch'\n\n/**\n * 对 fetch 进行封并返回 isLoading、isError、data 三个值\n * @param {*} url 请求的 API 地址\n * @param {*} initialData 初始化数据\n */\nfunction useFetch(url, initialData) {\n  const [data, setData] = useState(initialData)\n  const [isLoading, setIsLoading] = useState(false);\n  const [isError, setIsError] = useState(false)\n\n  useEffect(() =\u003e {\n    const fetchData = async () =\u003e {\n      setIsError(false)\n      setIsLoading(true)\n\n      try {\n        const result = await fetch(url)\n        const newData = await result.json()\n        setData(newData)\n      } catch(error) {\n        setIsError(false)\n      }\n\n      setIsLoading(false)\n    }\n\n    fetchData()\n  }, [])\n\n  return [data, isLoading, isError]\n}\n\nexport default useFetch;\n\n```\n\u003ca name=\"RgpD4\"\u003e\u003c/a\u003e\n### \n然后修改我们的业务代码如下，这时视图层只需要一行代码即可完成数据的请求，并返回了 `data` 、 `isLoading` 、 `isError` 三个值，渲染处理逻辑完全一致。\n\n```jsx\n// 首页列表实现\nconst Home = () =\u003e {\n  const [data, isLoading, isError] = useFetch('api/data', []);\n  \n  return (\n    // render jsx\n  )\n}\n\n// 项目详情实现\nconst project = () =\u003e {\n\tconst [data, isLoading, isError] = useFetch(`api/data?id=${id}`, []);\n  \n  return (\n    // render jsx\n  )\n}\n```\n\n至此我们的 useFetch API 形式如下，接收 `url`  和 `initialData`  作为参数，返回 `data` 、 `isLoading` 、 `isError`  三个值。\n\n![image.png](https://img.alicdn.com/tfs/TB1fJruokL0gK0jSZFxXXXWHVXa-1138-186.png)\n\n\u003ca name=\"Perop\"\u003e\u003c/a\u003e\n## 功能迭代\n\n上面的代码看起来应该是不错了，通过 useFetch 的封装，在具体的视图中只需要调用 useFetch 传入对应的 API 地址和初始数据，即可正常工作，然而实际的业务场景并不都是如此，接下来将逐步对它进行功能迭代，满足常见的业务开发需求。\n\n\u003ca name=\"mevks\"\u003e\u003c/a\u003e\n### 自定义请求\n\n上面实现的 useFetch 是将 fetch 的实现逻辑进行了内置，且默认使用了 `isomorphic-unfetch` 这个库，在实际业务中，你可能习惯了使用 axios，也可能需要对 fetch 的逻辑进行定制，那么现有的 useFetch 显然就不能满足要求，这时我们可以考虑将 fetch 逻辑通过参数的形式进行传入，外层可以自定义获取数据的行为，如果不传递则默认为 `undefined` 。\n\n```javascript\nimport { useState, useEffect } from 'react'\n\n// 支持传入 fetcher 用于自定义请求\nfunction useFetch(url, fetcher) {\n  const [data, setData] = useState(undefined)\n  const [isLoading, setIsLoading] = useState(false);\n  const [isError, setIsError] = useState(false)\n\n  useEffect(() =\u003e {\n    const fetchData = async () =\u003e {\n      setIsError(false)\n      setIsLoading(true)\n\n      try {\n        // 这里直接调用外部传进来的 fetcher，并使用 url 作为参数\n        const newData = await fetcher(url);\n        setData(newData)\n      } catch(error) {\n        setIsError(false)\n      }\n\n      setIsLoading(false)\n    }\n\n    fetchData()\n  }, [url])\n\n  return [data, isLoading, isError]\n}\n\nexport default useFetch;\n```\n\n这时在组件层获取数据的方式，可自定义请求函数如下：\n\n```javascript\nimport fetch from 'isomorphic-unfetch'\n\nconst customFetch = async (...args) =\u003e {\n  const res = await fetch(...args)\n  return await res.json()\n}\n\nconst Home = () =\u003e {\n  // useFetch 的第二个参数可以使用自定义的 customFetch\n  const [data, isLoading, isError] = useFetch('api/data', customFetch);\n  return ()\n}\n```\n\n可以看到，useFetch 现在可以接收一个函数用于获取数据，且该函数的唯一参数为 useFetch 的第一个参数 url，这意味着可以使用你喜欢的任何请求库来获取数据。\n\n![image.png](https://img.alicdn.com/tfs/TB158PvokT2gK0jSZPcXXcKkpXa-1088-222.png)\n\n\n\u003ca name=\"SsZrI\"\u003e\u003c/a\u003e\n### 全局配置项\n\n我们已经可以通过自定义 fetcher 获取数据，但每个调用处都需要重复的去传递 fetcher，因此可以考虑将其统一配置，在调用时可以直接使用该默认配置，也可以自定义配置来覆盖，为此需要一个全局配置的方式。\n\n在 React 中全局配置数据共享最简单的就是通过 Context 方式，这里我们选择使用 useContext 来实现 useFetch 的全局配置功能。\n\n```javascript\nimport { createContext } from 'react'\n\nconst useFetchConfigContext = createContext({})\nuseFetchConfigContext.displayName = 'useFetchConfigContext'\n\nexport default useFetchConfigContext;\n```\n\nuseFetch 改造如下\n\n```javascript\nimport { useState, useEffect } from 'react'\n\nfunction useFetch(url, fetcher, options = {}) {\n  const [data, setData] = useState(undefined)\n  const [isLoading, setIsLoading] = useState(false);\n  const [isError, setIsError] = useState(false)\n\n  // 通过 useContext 获取 useFetchConfigContext 的全局配置\n  const config = Object.assign(\n    {},\n    useContext(useFetchConfigContext),\n    options\n  )\n\n  let fn = fetcher\n  if(typeof fn === 'undefined') {\n    // 使用全局配置的 fetcher\n    fn = config.fetcher\n  }\n\n  useEffect(() =\u003e {\n    // ...\n  }, [url])\n\n  return [data, isLoading, isError]\n}\n\n// 导出 useFetchConfig \nconst useFetchConfig = useFetchConfigContext.Provider;\nexport { useFetchConfig };\n\nexport default useFetch;\n```\n\n现在组件层获取数据的方式如下，在全局统一配置 fetcher，然后在调用 useFetch 的组件中只需要传入对应的 url 即可：\n\n**全局配置**\n\n```jsx\nimport React from 'react'\nimport App from 'next/app'\nimport { useFetchConfig as UseFetchConfig } from '../libs/useFetch'\nimport fetch from '../libs/fetch';\n\nclass MyApp extends App {\n  render() {\n    const { Component, pageProps } = this.props\n    return (\n      /* 通过 useFetchConfig 配置全局的 useFetch 参数 */\n      \u003cUseFetchConfig value={{ fetcher: fetch }}\u003e\n        \u003cComponent {...pageProps} /\u003e\n      \u003c/UseFetchConfig\u003e\n    )\n  }\n}\n```\n\n**组件调用**\n\n```jsx\nconst Home = () =\u003e {\n  const [data, isLoading, isError] = useFetch('api/data');\n  return (\n    // jsx code\n  )\n}\n```\n\n至此，我们提供了一个全局配置来代替每个调用 useFetch 时的重复逻辑，现在我们的 useFetch 功能如下：\n\n![image.png](https://img.alicdn.com/tfs/TB1hajuoeH2gK0jSZFEXXcqMpXa-1256-402.png)\n\n\u003ca name=\"nXpdP\"\u003e\u003c/a\u003e\n### 依赖请求\n\n除了自定义请求和全局配置，实际业务中另外一类常见需求就是请求之间的依赖，**如 B 依赖 A 的返回结果作为请求参数**，通常的写法如下：\n\n```javascript\nconst { data: a } = await fetch('/api/a')\nconst { data: b } = await fetch(`/api/b?id=${a.id}`)\n```\n\n\n那么在 `useFetch`  的模式下该如何处理这类需求，当 `/api/a`  接口未正常返回结果时 `a`  的值为 `undefined` ，在 `/api/b`  接口中调用 `a.id` 就会直接抛出异常，导致页面渲染失败。\n\n**那这是否意味我们可以假设当调用接口时 `url`  这个参数抛出异常，也就意味着它的依赖还没有准备就绪，暂停这个数据的请求；等到依赖项准备就绪时，然后对就绪的数据发起新的一轮请求，以此来解决依赖请求的问题。**\n\n而依赖项准备就绪的时机也就是在任一请求完成时，如上面的 `/api/a`  请求完成时 `useFetch`  会通过 `setState`  触发重新渲染，同时 `/api/b?id=${a.id}` 得到更新，只需要将该 `url`  作为 `useEffect`  的依赖项即可自动监听并触发新一轮的请求。其示意图如下：\u003cbr /\u003e![image.png](https://img.alicdn.com/tfs/TB1aGLtobr1gK0jSZR0XXbP8XXa-704-290.png)\n\n通过上面的分析， `useFetch`  处理依赖请求的逻辑主要分为以下三步：\n\n1. 约定参数 `url`  可以是一个函数并且该函数返回一个字符串作为请求的唯一标识符；\n1. 当调用该函数抛出异常时就意味着它的依赖还没有就绪，将暂停这个请求；\n1. 在依赖的请求完成时，通过 `setState`  触发重新渲染，此时 `url`  会被更新，同时通过 `useEffect`  监听 `url`  是否有改变触发新一轮的请求。\n\n```javascript\nconst Home = () =\u003e {\n  // A 和 B 两个并行请求，且 B 依赖 A 请求\n  const { data: a } = useFetch('/api/a')\n  const { data: b } = useFetch(() =\u003e `/api/b?id=${a.id}`)\n\n  return ()\n}\n\nconst useFetch = (url, fetcher, options) =\u003e {\n  \n  const getKeyArgs = _key =\u003e {\n    let key\n    if (typeof _key === 'function') {\n      // 核心所在:\n      // 当 url 抛出异常时意味着它的依赖还没有就绪则暂停请求\n      // 也就是将 key 设置为空字符串\n      try {\n        key = _key()\n      } catch (err) {\n        key = ''\n      }\n    } else {\n      // convert null to ''\n      key = String(_key || '')\n    }\n    return key\n  }\n\n  useEffect(() =\u003e {\n    const [data, setData] = useState(undefined)\n    const key = getKeyArgs(url)\n    const fetchData = async () =\u003e {\n      try {\n        const newData = await fn(key);\n        setData(newData)\n      } catch(error) {\n        // \n      }\n    },\n\n    fetchData()\n    \n  // 核心所在\n  // 当 A 请求完成时通过 setData 触发 UI 重新渲染\n  // 继而当 url 更新时触发 B 的新一轮请求\n  }, [key])\n\n  return {}\n}\n```\n\n\n如 SWR 官方文档所描述，允许获取依赖于其他请求的数据，且可以**确保最大程度的并行**（avoiding waterfalls），其原理主要是通过约定 `key` 为一个函数进行 `try {}` 处理 ，并巧妙的结合 React 的 `UI = f(data)` 模型来触发请求，以此确保最大程度的并行。\n\u003e SWR also allows you to fetch data that depends on other data. It ensures the maximum possible parallelism (avoiding waterfalls), as well as serial fetching when a piece of dynamic data is required for the next data fetch to happen.\n\n\n当然在依赖请求过程中，我们可能需要对 `useFetch`  有更多的控制权，比如设置请求的超时时间，以及请求超时需要触发回调，请求成功/失败的回调等。我们可以通过添加第三个参数 `options`  进行传入，完整实现如下：\n\n```javascript\nfunction useFetch(url, fetcher, options = {}) {\n  // 从 useContext 获取全局配置和默认配置进行合并，继而和 useFetch 的 options 进行合并\n  // 优先级分别是 useFetch \u003e useFetchConfigContext \u003e defaultConfig\n  const config = Object.assign(\n    {},\n    defaultConfig,\n    useContext(useFetchConfigContext),\n    options\n  )\n\n  const key = getKeyArgs(url)\n\n  // 通过 options 设置初始值\n  const [data, setData] = useState(options.initialData)\n  const [isLoading, setIsLoading] = useState(false);\n  const [isError, setIsError] = useState(false)\n\n  const fetchData = useCallback(\n    async () =\u003e {\n      if(!key) return false\n      setIsLoading(true)\n\n      let loading = true\n\n      let newData\n\n      try {\n        // 请求超时触发 onLoadingSlow 回调函数\n        if (config.loadingTimeout) {\n          setTimeout(() =\u003e {\n            if (loading) config.onLoadingSlow(key, config)\n          }, config.loadingTimeout)\n        }\n\n        newData = await fn(key);\n\n        // 触发请求成功时的回调函数\n        config.onSuccess \u0026\u0026 config.onSuccess(newData, key, config)\n\n        // 批量更新\n        unstable_batchedUpdates(() =\u003e {\n          setData(newData)\n          setIsLoading(false)\n        })\n      } catch(error) {\n        unstable_batchedUpdates(() =\u003e {\n          setIsError(true)\n          setIsLoading(false)\n        })\n\n        // 触发请求失败时的回调函数\n        config.onError \u0026\u0026 config.onError(error, key, config)\n      }\n\n      loading = false\n      return true\n    },\n    // eslint-disable-next-line\n    [key]\n  )\n\n  useEffect(() =\u003e {\n    fetchData()\n  }, [fetchData])\n\n  return {data, isLoading, isError}\n}\n```\n\n\n相关技术点：\n\n**unstable_batchedUpdates**\n\n在 React 中某些场景下如果多次调用 setState 则会导致多次的 render，但有些 setState 的渲染是没有必须的，如上述实现代码的 `setData(data)` 和 `setIsLoading(false)` ，因此 react 提供了 `unstable_batchedUpdates`  API 用来批量处理。\n\n```javascript\n// 示例来源：Does React keep the order for state updates\nimport { unstable_batchedUpdates } from 'react-dom';\n\npromise.then(() =\u003e {\n  // Forces batching\n  ReactDOM.unstable_batchedUpdates(() =\u003e {\n    this.setState({a: true}); // Doesn't re-render yet\n    this.setState({b: true}); // Doesn't re-render yet\n    this.props.setParentState(); // Doesn't re-render yet\n  });\n  // When we exit unstable_batchedUpdates, re-renders once\n});\n```\n\n**useCallback**\u003cbr /\u003e**\u003cbr /\u003e对事件句柄进行缓存，如 useState 的第二个返回值是更新函数 setState，但是每次都是返回新的，使用 useCallback 可以让它使用上次的函数。useCallback 接收内联回调函数以及依赖项数组作为参数，该回调函数仅在某个依赖项改变时才会更新。\n\n```javascript\nconst memoizedCallback = useCallback(\n  () =\u003e {\n    doSomething(a, b);\n  },\n  \n  // a, b 更新时才会调用回调函数\n  [a, b],\n);\n```\n\n到这里 `useFetch`  的 API 形式如下，已经支持传入请求的 `url` 、自定义的 `fetcher` 、以及一些可选的配置项。 \n\n![image.png](https://img.alicdn.com/tfs/TB14avroXT7gK0jSZFpXXaTkpXa-1238-366.png)\n\n\u003ca name=\"Gk33E\"\u003e\u003c/a\u003e\n### 聚焦验证\n\nReact 团队在前不久的 React Conf 上发布了关于 `Concurrent`  模式的实验性文档，如果说 React Hooks 目的是提高开发体验，那么 `Concurrent`  模式则专注于提升用户体验。同样对于一个基于 React Hook 的请求库而言，除了提供强大的功能之外，提升用户体验也是需要考虑的能力之一。\n\n`stale-while-revalidate`  是 HTTP 的缓存策略值之一，其核心就是**允许客户端先使用缓存中不新鲜的数据**，**然后在后台异步重新验证更新缓存**，等下次使用的时候数据就是新鲜的了，旨在通过缓存提高用户体验。\n\n![image.png](https://img.alicdn.com/tfs/TB157zsokP2gK0jSZPxXXacQpXa-1492-747.png)\u003cbr /\u003e如上图所示，在 `useFetch` 中也可以借鉴这种缓存机制，如在请求之前先从缓存返回数据（stale），然后在异步发送请求，最后当数据返回时更新缓存并触发 UI 的重新渲染，从而提高用户体验。这里以鼠标聚焦页面时先从缓存获取数据然后异步请求更新为例，来看看具体的实现。\n\n基于上面的分析，我们首先需要将所有请求的数据结果在内存中进行缓存，来模拟 stale-while-revalidate 的缓存效果，可以利用 ES6 的 `new Map()` 来实现，以 `{[key]: [value]}`  的形式记录请求的数据结果，设计如下：\n\n```javascript\nconst __cache = new Map()\n\nfunction cacheGet(key) {\n  return __cache.get(key) || undefined\n}\n\nfunction cacheSet(key, value) {\n  return __cache.set(key, value)\n}\n\nfunction cacheClear() {\n  __cache.clear()\n}\n```\n\n同时，需要记录异步请求的集合，用来根据不同的 `key`  触发对应的验证函数， 以  `{[key]: [revalidate]}` 的形式进行记录请求集合，设计如下：\n\n```javascript\n// 记录并发的请求函数集合\nconst CONCURRENT_PROMISES = {}\n\n// 记录聚焦的验证函数集合\nconst FOCUS_REVALIDATORS = {}\n\n// 记录缓存中的验证函数集合\nconst CACHE_REVALIDATORS = {}\n```\n\n完整的代码实现如下：\n\u003e 基于 `stale-while-revalidate`  的思想, 这里将 `useFetch`  命名为 `useSWR` ，同时将原有的 `isLoading`  命名为 `isValidating` ，将数据请求函数 `fetchData` 命名为 `revalidate` .\n\n\n```javascript\n// 基于 stale-while-revalidate 的思想, 这里将 useFetch 命名为 useSWR\nfunction useSWR(url, fetcher, options = {}) {\n  // 约定 `key` 是发送请求的唯一标识符\n  // key 可以被改变，当改变时触发请求\n  const [key] = getKeyArgs(url)\n  \n  // `keyErr`是错误对象的缓存键\n  const keyErr = getErrorKey(key)\n  \n  const config = Object.assign(\n    {},\n    defaultConfig,\n    useContext(useFetchConfigContext),\n    options\n  )\n\n  let fn = fetcher\n  if(typeof fn === 'undefined') {\n    // 使用全局的 fetcher\n    fn = config.fetcher\n  }\n\n  // 通过 useState 设置 data | error  的初始值，优先从缓存获取数据（stale data）\n  const [data, setData] = useState(cacheGet(key) || undefined)\n  const [error, setError] = useState(cacheGet(keyErr) || undefined)\n\n  // 基于 stale-while-revalidate 的思想，这里将 isLoading 命名为 isValidating\n  const [isValidating, setIsValidating] = useState(false);\n\n  // useRef 可以用来存储任何会改变的值，解决了在函数组件上不能通过实例去存储数据的问题。另外还可以 useRef 来访问到改变之前的数据\n  const unmountedRef = useRef(false)\n  const keyRef = useRef(key)\n  const errorRef = useRef(error)\n  const dataRef = useRef(data)\n\n  // 基于 stale-while-revalidate 的思想，这里将 fetchData 命名为 revalidate\n  // 当依赖项 key 变化时 useCallback 会重新执行\n  const revalidate = useCallback(\n    async () =\u003e {\n      if(!key) return false\n      if (unmountedRef.current) return false\n\n      let loading = true\n\n      try {\n        setIsValidating(true)\n\n        // 请求超时触发 onLoadingSlow 回调函数\n        if (config.loadingTimeout) {\n          setTimeout(() =\u003e {\n            if (loading) config.onLoadingSlow(key, config)\n          }, config.loadingTimeout)\n        }\n\n        // 将请求记录到 CONCURRENT_PROMISES 对象\n        CONCURRENT_PROMISES[key] = fn(key)\n\n        // 执行请求\n        const newData = await CONCURRENT_PROMISES[key]\n\n        // 请求成功时的回调\n        config.onSuccess(newData, key, config)\n\n        // 将请求结果存储到缓存 cache 中\n        cacheSet(key, newData)\n        cacheSet(keyErr, undefined)\n        keyRef.current = key\n\n        // 批量更新\n        unstable_batchedUpdates(() =\u003e {\n          setIsValidating(false)\n\n          if (typeof errorRef.current !== 'undefined') {\n            setError(undefined)\n            errorRef.current = undefined\n          }\n\n          // 数据改变调用 setData 更新触发 UI 渲染\n          setData(newData)\n          dataRef.current = newData\n        })\n      } catch(err) {\n        delete CONCURRENT_PROMISES[key]\n\n        cacheSet(keyErr, err)\n        keyRef.current = key\n\n        // 请求出错设置错误值\n        if (errorRef.current !== err) {\n          errorRef.current = err\n\n          unstable_batchedUpdates(() =\u003e {\n            setIsValidating(false)\n            setError(err)\n          })\n        }\n\n        // 请求失败时的回调\n       config.onError(err, key, config)\n      }\n\n      loading = false\n      return true\n    },\n    [key]\n  )\n\n  useEffect(() =\u003e {\n    // 在 key 更新之后，我们需要将其标记为 mounted\n    unmountedRef.current = false\n\n    // 当组件挂载（hydrated）后，获取缓存数据进行更新，并触发重新验证\n    const currentHookData = dataRef.current\n    const latestKeyedData = cacheGet(key)\n\n    // 如果 key 已更改或缓存已更新，则更新状态\n    if (\n      keyRef.current !== key ||\n      !deepEqual(currentHookData, latestKeyedData)\n    ) {\n      setData(latestKeyedData)\n      dataRef.current = latestKeyedData\n      keyRef.current = key\n    }\n\n    // revalidate with deduping\n    const softRevalidate = () =\u003e revalidate()\n\n    // 触发验证\n    if (\n      typeof latestKeyedData !== 'undefined' \u0026\u0026\n      window['requestIdleCallback']\n    ) {\n      // 如果有缓存则延迟重新验证，优先使用缓存数据进行渲染\n      window['requestIdleCallback'](softRevalidate)\n    } else {\n      // 没有缓存则执行验证\n      softRevalidate()\n    }\n\n    // 每当窗口聚焦时，重新验证\n    let onFocus\n    if (config.revalidateOnFocus) {\n      // 节流：避免快速切换标签页重复调用\n      onFocus = throttle(softRevalidate, config.focusThrottleInterval)\n      if (!FOCUS_REVALIDATORS[key]) {\n        FOCUS_REVALIDATORS[key] = [onFocus]\n      } else {\n        FOCUS_REVALIDATORS[key].push(onFocus)\n      }\n    }\n\n    // 注册全局缓存的更新监听函数\n    const onUpdate = (\n      shouldRevalidate = true,\n      updatedData,\n      updatedError\n    ) =\u003e {\n      // 批量更新\n      unstable_batchedUpdates(() =\u003e {\n        if (\n          typeof updatedData !== 'undefined' \u0026\u0026\n          !deepEqual(dataRef.current, updatedData)\n        ) {\n          setData(updatedData)\n          dataRef.current = updatedData\n        }\n\n        if (errorRef.current !== updatedError) {\n          setError(updatedError)\n          errorRef.current = updatedError\n        }\n        keyRef.current = key\n      })\n\n      if (shouldRevalidate) {\n        return softRevalidate()\n      }\n      return false\n    }\n\n    // 将更新函数添加到监听队列\n    if (!CACHE_REVALIDATORS[key]) {\n      CACHE_REVALIDATORS[key] = [onUpdate]\n    } else {\n      CACHE_REVALIDATORS[key].push(onUpdate)\n    }\n\n    return () =\u003e {\n      // 清除副作用的相关逻辑\n      unmountedRef.current = true\n\n      if (onFocus \u0026\u0026 FOCUS_REVALIDATORS[key]) {\n        const index = FOCUS_REVALIDATORS[key].indexOf(onFocus)\n        if (index \u003e= 0) FOCUS_REVALIDATORS[key].splice(index, 1)\n      }\n      if (CACHE_REVALIDATORS[key]) {\n        const index = CACHE_REVALIDATORS[key].indexOf(onUpdate)\n        if (index \u003e= 0) CACHE_REVALIDATORS[key].splice(index, 1)\n      }\n    }\n  }, [key, revalidate])\n\n  return {data, isValidating, error}\n}\n```\n\n相关技术点：\n\n**requestIdleCallback**\u003cbr /\u003e**\u003cbr /\u003e在 React 16 实现了新的调度策略(Fiber)，新的调度策略提到的异步、可中断，其实就是基于浏览器的 requestIdleCallback 和 requestAnimationFrame 两个API，在 useSWR 中也是使用了 requestIdleCallback 这个 API，其作用就是在前浏览器处于空闲状态的时候执相对较低的任务，也即传给 requestIdleCallback 的回调函数，可以看到在 useSWR 中验证函数作为 requestIdleCallback 的回调函数，如果有缓存则延迟重新验证，优先使用缓存数据进行渲染。\n\n```javascript\n// 触发验证\nif (\n  typeof latestKeyedData !== 'undefined' \u0026\u0026\n  window['requestIdleCallback']\n) {\n  // 如果有缓存则延迟重新验证，优先使用缓存数据进行渲染\n  window['requestIdleCallback'](softRevalidate)\n} else {\n  // 没有缓存则执行验证\n  softRevalidate()\n}\n```\n\n**聚焦验证**\u003cbr /\u003e**\u003cbr /\u003e有了上面的分析，聚焦验证就很好实现了，主要是通过判断窗口的可见性来触发验证请求：\n\n判断窗口是否可见:\n\n```javascript\nexport default function isDocumentVisible() {\n  if (\n    typeof document !== 'undefined' \u0026\u0026\n    typeof document.visibilityState !== 'undefined'\n  ) {\n    return document.visibilityState !== 'hidden'\n  }\n  // always assume it's visible\n  return true\n}\n```\n\n绑定监听窗口状态触发验证：\n\n```javascript\n// Focus revalidate\nlet eventsBinded = false\nif (typeof window !== 'undefined' \u0026\u0026 window.addEventListener \u0026\u0026 !eventsBinded) {\n  const revalidate = () =\u003e {\n    if (!isDocumentVisible() || !isOnline()) return\n\n    // eslint-disable-next-line\n    for (let key in FOCUS_REVALIDATORS) {\n      if (FOCUS_REVALIDATORS[key][0]) FOCUS_REVALIDATORS[key][0]()\n    }\n  }\n  window.addEventListener('visibilitychange', revalidate, false)\n  window.addEventListener('focus', revalidate, false)\n  // only bind the events once\n  eventsBinded = true\n}\n```\n\n\u003ca name=\"oe1e7\"\u003e\u003c/a\u003e\n### 其他功能\n\n在 useSWR 官网中，可以看到还有其他诸如支持 Interval polling、Suspense Mode、Local Mutation 等的能力，但了解其原理之后我们知道其本质都是通过不同的条件和时机来触发验证进行实现的，这里不再一一分析。另外有趣的是由于 useSWR 是 nextjs 的相关团队出品，其也支持了 SSR 能力，以及做了针对服务端的一些特殊处理，如在 useSWR 中判断是服务端的时候使用的 `useLayoutEffect` ， `Suppense` 模式尽在客户端使用等。\n\n```javascript\n// React currently throws a warning when using useLayoutEffect on the server.\n  // To get around it, we can conditionally useEffect on the server (no-op) and\n  // useLayoutEffect in the browser.\n  const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect\n\n  // mounted (client side rendering)\n  useIsomorphicLayoutEffect(() =\u003e {\n  \n    // suspense\n  if (config.suspense) {\n    if (IS_SERVER)\n      throw new Error('Suspense on server side is not yet supported!')\n  })\n```\n\n\n\u003ca name=\"KLdzc\"\u003e\u003c/a\u003e\n## 核心原理\n\n下面是基于源码画的一张 SWR 的流程图，用来汇总上述的源码分析过程，链接其核心主要分三个阶段：\n\n1. 在调用 `useSWR()` 时获取缓进行数据初始化阶段；\n1. 在 `useIsomorphicLayoutEffect` 时如果有缓存则优先使用缓存数据，然后异步调用 `revalidate` 进行数据验证并更新缓存数据阶段；\n1. 在 `useIsomorphicLayoutEffect` 时调用 `unstable_batchedUpdates`  渲染视图阶段。\n\n![image.png](https://img.alicdn.com/tfs/TB1eYYtobr1gK0jSZR0XXbP8XXa-1261-1046.png)\n\u003ca name=\"7Oj2Z\"\u003e\u003c/a\u003e\n## 总结\n\n通过上面的分析，相比社区的现有请求类库，useSWR 除了提供常见的功能之外，其核心在于借鉴了 `stale-while-revalidate` 缓存的思想，并与 React Hooks 进行结合，优先从缓存中获取数据保证的 UI 的快速渲染， 然后在后台异步重新验证更新缓存，一旦缓存得到更新，利用 setState 的机制又会重新触发 UI 的渲染，这意味着组件将得到一个不断地自动更新的数据流，来确保数据的新鲜性。\n\n另外，更强大的是由于 useSWR 缓存的是所有异步请求的数据，本质上相当于拥有了 Global Store 的能力，间接的提供了一种 **状态管理 **的方案；而事实上，useSWR 除了异步请求数据之外，也可以通过同步的方式往缓存中写入数据，满足组件之间的状态同步需求。目前官方还未将这一能力在其文档释放出来，但 @偏右 已经提交了一个示例 [local-state-sharing](https://github.com/zeit/swr/blob/master/examples/local-state-sharing) 演示其可行性。这意味着在某些场景下，我们也许并不需要诸如 Redux / mobx / unstated-next / icestore/dva/ React Context 等等状态管理库。\n\n在未来，也许我们可以这样玩，将 **数据请求 **和 **状态管理 **合二为一，大致的脑图如下：\u003cbr /\u003e![image.png](https://img.alicdn.com/tfs/TB1RsfroXT7gK0jSZFpXXaTkpXa-2060-1490.png)\n\n\u003ca name=\"va6qC\"\u003e\u003c/a\u003e\n## 参考\n\n- [How to fetch data with React Hooks?](https://www.robinwieruch.de/react-hooks-fetch-data)\n- [Two HTTP Caching Extensions](https://www.mnot.net/blog/2007/12/12/stale)\n- [离线指南 - stale-while-revalidate](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/?hl=zh-CN#stale-while-revalidate)\n- [SWR 与前端数据依赖请求](https://zhuanlan.zhihu.com/p/90660704)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchenbin92%2Fswr-source-code","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchenbin92%2Fswr-source-code","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchenbin92%2Fswr-source-code/lists"}