取数是前端业务的重要部分,也经历过几回演化:前端
$.post
在内的各类取数封装。data
isLoading
error
封装。swr 在 2019.10.29 号提交,仅仅 12 天就攒了 4000+ star,平均一天收获 300+ star!本周精读就来剖析这个库的功能与源码,了解这个 React Hooks 的取数库的 Why How 与 What。react
首先介绍 swr 的功能。ios
为了和官方文档有所区别,笔者以探索式思路介绍这个它,但例子都取自官方文档。git
首先回答一个根本问题:为何用 Hooks 替代 fetch 或数据流取数?github
由于 Hooks 能够触达 UI 生命周期,取数本质上是 UI 展现或交互的一个环节。 用 Hooks 取数的形式以下:typescript
import useSWR from "swr";
function Profile() {
const { data, error } = useSWR("/api/user", fetcher);
if (error) return <div>failed to load</div>;
if (!data) return <div>loading...</div>;
return <div>hello {data.name}!</div>;
}
复制代码
首先看到的是,以同步写法描述了异步逻辑,这是由于渲染被执行了两次。json
useSWR
接收三个参数,第一个参数是取数 key
,这个 key
会做为第二个参数 fetcher
的第一个参数传入,普通场景下为 URL,第三个参数是配置项。axios
Hooks 的威力还不只如此,上面短短几行代码还自带以下特性:后端
固然,自动刷新或从新取数也不必定是咱们想要的,swr 容许自定义配置。api
上面提到,useSWR
还有第三个参数做为配置项。
独立配置
经过第三个参数为每一个 useSWR
独立配置:
useSWR("/api/user", fetcher, { revalidateOnFocus: false });
复制代码
配置项能够参考 文档。
能够配置的有:suspense 模式、focus 从新取数、从新取数间隔/是否开启、失败是否从新取数、timeout、取数成功/失败/重试时的回调函数等等。
第二个参数若是是 object 类型,则效果为配置项,第二个 fetcher 只是为了方便才提供的,在 object 配置项里也能够配置 fetcher。
全局配置
SWRConfig
能够批量修改配置:
import useSWR, { SWRConfig } from "swr";
function Dashboard() {
const { data: events } = useSWR("/api/events");
// ...
}
function App() {
return (
<SWRConfig value={{ refreshInterval: 3000 }}>
<Dashboard />
</SWRConfig>
);
}
复制代码
独立配置优先级高于全局配置,在精读部分会介绍实现方式。
最重量级的配置项是 fetcher
,它决定了取数方式。
自定义取数逻辑其实分几种抽象粒度,好比自定义取数 url,或自定义整个取数函数,而 swr 采起了相对中间粒度的自定义 fetcher
:
import fetch from "unfetch";
const fetcher = url => fetch(url).then(r => r.json());
function App() {
const { data } = useSWR("/api/data", fetcher);
// ...
}
复制代码
因此 fetcher
自己就是一个拓展点,咱们不只能自定义取数函数,自定义业务处理逻辑,甚至能够自定义取数协议:
import { request } from "graphql-request";
const API = "https://api.graph.cool/simple/v1/movies";
const fetcher = query => request(API, query);
function App() {
const { data, error } = useSWR(
`{
Movie(title: "Inception") {
releaseDate
actors {
name
}
}
}`,
fetcher
);
// ...
}
复制代码
这里回应了第一个参数称为取数 Key 的缘由,在 graphql 下它则是一段语法描述。
到这里,咱们能够自定义取数函数,但却没法控制什么时候取数,由于 Hooks 写法使取数时机与渲染时机结合在一块儿。swr 的条件取数机制能够解决这个问题。
所谓条件取数,即 useSWR
第一个参数为 null 时则会终止取数,咱们能够用三元运算符或函数做为第一个参数,使这个条件动态化:
// conditionally fetch
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);
// ...or return a falsy value
const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);
复制代码
上例中,当 shouldFetch
为 false 时则不会取数。
第一个取数参数推荐为回调函数,这样 swr 会 catch 住内部异常,好比:
// ... or throw an error when user.id is not defined
const { data, error } = useSWR(() => "/api/data?uid=" + user.id, fetcher);
复制代码
若是 user
对象不存在,user.id
的调用会失败,此时错误会被 catch 住并抛到 error
对象。
实际上,user.id
仍是一种依赖取数场景,当 user.id
发生变化时须要从新取数。
若是一个取数依赖另外一个取数的结果,那么当第一个数据结束时才会触发新的取数,这在 swr 中不须要特别关心,只需按照依赖顺序书写 useSWR
便可:
function MyProjects() {
const { data: user } = useSWR("/api/user");
const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);
if (!projects) return "loading...";
return "You have " + projects.length + " projects";
}
复制代码
swr 会尽量并行没有依赖的请求,并按依赖顺序一次发送有依赖关系的取数。
能够想象,若是手动管理取数,当依赖关系复杂时,为了确保取数的最大可并行,每每须要精心调整取数递归嵌套结构,而在 swr 的环境下只需顺序书写便可,这是很大的效率提高。优化方式在下面源码解读章节详细说明。
依赖取数是自动从新触发取数的一种场景,其实 swr 还支持手动触发从新取数。
trigger
能够经过 Key 手动触发取数:
import useSWR, { trigger } from "swr";
function App() {
return (
<div>
<Profile />
<button
onClick={() => {
// set the cookie as expired
document.cookie =
"token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
// tell all SWRs with this key to revalidate
trigger("/api/user");
}}
>
Logout
</button>
</div>
);
}
复制代码
大部分场景没必要如此,由于请求的从新触发由数据和依赖决定,但遇到取数的必要性不禁取数参数决定,而是时机时,就须要用手动取数能力了。
特别在表单场景时,数据的改动是可预期的,此时数据驱动方案只能等待后端返回结果,其实能够优化为本地先修改数据,等后端结果返回后再刷新一次:
import useSWR, { mutate } from "swr";
function Profile() {
const { data } = useSWR("/api/user", fetcher);
return (
<div>
<h1>My name is {data.name}.</h1>
<button
onClick={async () => {
const newName = data.name.toUpperCase();
// send a request to the API to update the data
await requestUpdateUsername(newName);
// update the local data immediately and revalidate (refetch)
mutate("/api/user", { ...data, name: newName });
}}
>
Uppercase my name!
</button>
</div>
);
}
复制代码
经过 mutate
能够在本地临时修改某个 Key 下返回结果,特别在网络环境差的状况下加快响应速度。乐观取数,表示对取数结果是乐观的、可预期的,因此才能在结果返回以前就预测并修改告终果。
在 React Suspense 模式下,全部子模块均可以被懒加载,包括代码和请求均可以被等待,只要开启 suspense
属性便可:
import { Suspense } from "react";
import useSWR from "swr";
function Profile() {
const { data } = useSWR("/api/user", fetcher, { suspense: true });
return <div>hello, {data.name}</div>;
}
function App() {
return (
<Suspense fallback={<div>loading...</div>}>
<Profile />
</Suspense>
);
}
复制代码
onErrorRetry
能够统一处理错误,包括在错误发生后从新取数等:
useSWR(key, fetcher, {
onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
if (retryCount >= 10) return;
if (error.status === 404) return;
// retry after 5 seconds
setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000);
}
});
复制代码
// conditionally fetch
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);
// ...or return a falsy value
const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);
// ... or throw an error when user.id is not defined
const { data } = useSWR(() => "/api/data?uid=" + user.id, fetcher);
复制代码
在 Hooks 场景下,包装一层自定义 Context
便可实现全局配置。
首先 SWRConfig
本质是一个定制 Context Provider
:
const SWRConfig = SWRConfigContext.Provider;
复制代码
在 useSWR
中将当前配置与全局配置 Merge 便可,经过 useContext
拿到全局配置:
config = Object.assign({}, defaultConfig, useContext(SWRConfigContext), config);
复制代码
从源码能够看到更多细节用心,useSWR
真的比手动调用 fetch
好不少。
兼容性
useSWR
主体代码在 useEffect
中,可是为了将请求时机提早,放在了 UI 渲染前(useLayoutEffect
),并兼容了服务端场景:
const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect;
复制代码
非阻塞
请求时机在浏览器空闲时,所以请求函数被 requestIdleCallback
包裹:
window["requestIdleCallback"](softRevalidate);
复制代码
softRevalidate
是开启了去重的 revalidate
:
const softRevalidate = () => revalidate({ dedupe: true });
复制代码
即默认 2s 内参数相同的重复取数会被取消。
性能优化
因为 swr 的 data
、isValidating
等数据状态是利用 useState
分开管理的:
let [data, setData] = useState(
(shouldReadCache ? cacheGet(key) : undefined) || config.initialData
);
// ...
let [isValidating, setIsValidating] = useState(false);
复制代码
而取数状态变化时每每 data
与 isValidating
要一块儿更新,为了仅触发一次更新,使用了 unstable_batchedUpdates
将更新合并为一次:
unstable_batchedUpdates(() => {
setIsValidating(false);
// ...
setData(newData);
});
复制代码
其实还有别的解法,好比使用 useReducer
管理数据也能达到相同性能效果。
当页面切换时,能够暂时以上一次数据替换取数结果,即初始化数据从缓存中拿:
const shouldReadCache = config.suspense || !useHydration();
// stale: get from cache
let [data, setData] = useState(
(shouldReadCache ? cacheGet(key) : undefined) || config.initialData
);
复制代码
上面一段代码在 useSWR
的初始化期间,useHydration
表示是否为初次加载:
let isHydration = true;
export default function useHydration(): boolean {
useEffect(() => {
setTimeout(() => {
isHydration = false;
}, 1);
}, []);
return isHydration;
}
复制代码
Suspense 分为两块功能:异步加载代码与异步加载数据,如今提到的是异步加载数据相关的能力。
Suspense 要求代码 suspended,即抛出一个能够被捕获的 Promise 异常,在这个 Promise 结束后再渲染组件。
核心代码就这一段,抛出取数的 Promise:
throw CONCURRENT_PROMISES[key];
复制代码
等取数完毕后再返回 useSWR
API 定义的结构:
return {
error: latestError,
data: latestData,
revalidate,
isValidating
};
复制代码
若是没有上面 throw
的一步,在取数完毕前组件就会被渲染出来,因此 throw
了请求的 Promise 使得这个请求函数支持了 Suspense。
翻了一下代码,没有找到对循环依赖特别处理的逻辑,后来看了官方文档才恍然大悟,原来是经过 try/catch
+ onErrorRetry
机制实现依赖取数的。
看下面这段代码:
const { data: user } = useSWR("/api/user");
const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);
复制代码
怎么作到智能按依赖顺序请求呢?咱们看 useSWR
取数函数的主体逻辑:
try {
// 设置 isValidation 为 true
// 取数、onSuccess 回调
// 设置 isValidation 为 false
// 设置缓存
// unstable_batchedUpdates
} catch (err) {
// 撤销取数、缓存等对象
// 调用 onErrorRetry
}
复制代码
可见取数逻辑被 try
住了,那么 user.id
在 useSWR("/api/user")
没有 Ready 的状况必定会抛出异常,则自动进入 onErrorRetry
逻辑,看看下次取数时 user.id
有没有 Ready。
那么何时才轮到下次取数呢?这个时机是:
const count = Math.min(opts.retryCount || 0, 8);
const timeout =
~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval;
复制代码
重试时间基本按 2 的指数速度增加。
因此 swr 会优先按照并行方式取数,存在依赖的取数会重试,直到上游 Ready。这种简单的模式稍稍损失了一些性能(没有在上游 Ready 后及时重试下游),但不失为一种巧妙的解法,并且最大化并行也使得大部分场景性能反而比手写的好。
笔者给仔细阅读本文的同窗留下两道思考题:
若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)