这篇博客是笔者在公司内部的一场技术分享,内容来自 PPT 的整理。
前言
快速学习新事物的方法论
我想通过讲解 SWR,向大家分享认识方法论,这是我认为本篇文章最重要的内容
SWR 介绍
首先来看看 SWR 是什么?
- SWR 是一个方法(函数)
- SWR 是一个自定义的 React Hook
- SWR 是一个管理 HTTP 请求的 React Hook
SWR 功能
SWR 提供非常多的功能。包括不限于请求状态封装、去除重复请求、缓存 HTTP 响应、依赖请求、轮询等等
请求状态封装
不用 SWR 实现一个简单的 HTTP GET 请求
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUsers() {
try {
setIsLoading(true);
const response = await fetch(url).json((res) => res.json());
setData(response);
} catch (e) {
setError(e);
} finally {
setIsLoading(false);
}
}
void fetchUsers();
}, []);
需要在组件内维护三个状态,分别是 data
、isLoading
和 error
,并且还要维护状态的变化
使用 SWR 实现上面的逻辑
const { data, isLoading, error } = useSWR(url);
使用 SWR 只需要一行代码就完全实现上面的业务逻辑,非常简单
重复请求去除和响应数据缓存
未使用 SWR,每一个 Avatar
组件在渲染的时候,都会发起 HTTP 请求,同一个接口会调用三次
function Avatar() {
const [data, setData] = useState({});
useEffect(() => {
fetchUsers()
.then((data) => setData(data))
.catch(err);
}, []);
return <img src={data.avatar_url} />;
}
function App() {
// 发三次 HTTP 请求
return (
<>
<Avatar />
<Avatar />
<Avatar />
</>
);
}
使用 SWR
function Avatar() {
const { data, error } = useSWR("/api/user", fetcher);
return <img src={data.avatar_url} />;
}
function App() {
// 只会发一次 HTTP 请求
return (
<>
<Avatar />
<Avatar />
<Avatar />
</>
);
}
使用 SWR 后,当 Avatar
组件渲染的时候,接口调用会被 SWR 拦截,三次调用接口会处理成一次调用,HTTP 成功响应后的数据缓存在内存中,使用数据的地方直接从内存中读取
支持依赖请求
不使用 SWR 处理接口依赖
function MyProjects() {
const [projects, setProjects] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
async function fetchProjects() {
try {
setIsLoading(true);
const user = await fetch("/api/user");
const projects = await fetch("/api/projects?uid=" + user.id);
setProjects(projects);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
}
void fetchProjects();
}, []);
if (isLoading) return "loading...";
return "You have " + projects.length + " projects";
}
使用 SWR 处理接口依赖
function MyProjects() {
const { data: user } = useSWR("/api/user");
const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);
// 传递函数时,SWR 会用返回值作为 key
// 如果函数抛出错误或返回 falsy 值,SWR 会知道某些依赖还没准备好
// 这种情况下,当 user 未加载时,user.id 抛出错误
if (!projects) return "loading...";
return "You have " + projects.length + " projects";
}
轮询
只需要设置 refreshInterval
配置项,就可以实现轮询
比如需要每秒请求该接口,可以这样配置
useSWR(url, { refreshInterval: 1000 });
React Suspense
<Suspense>
是 React16.8 推出的新组件,主要用来解决网络 IO 问题
在前端业务中,有特别多网络 IO 的地方,比如读写数据库、HTTP 请求等等。而 <Suspense>
是 React 给出网络 IO 的优雅解决方案。SWR 支持 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>
);
}
不使用 Suspense
模式实现上述逻辑
function Profile() {
const { data, isLoading } = useSWR("/api/user");
if (isLoading) return "loading...";
return <div>hello, {data.name}</div>;
}
function App() {
return <Profile />;
}
通过对比发现,Suspense
模式自动对 isLoading 状态进行 UI 处理,不需要开发人员手动处理
在 Suspense
模式下,data
总是请求响应(因此你无需检查它是否是 undefined
)。但如果发生错误,则需要使用错误边界(ErrorBoundary
)来捕获它:
<ErrorBoundary fallback={<h2>Could not fetch posts.</h2>}>
<Suspense fallback={<h1>Loading posts...</h1>}>
<Profile />
</Suspense>
</ErrorBoundary>
总的来说,Suspense 模式通过组件在异步操作完成之前不渲染,异步操作完成后将结果返回给组件,避免渲染过程中的闪烁和错误,从而实现异步渲染和更好的用户体验。
分页和滚动位置恢复
表格分页
function App() {
const [pageIndex, setPageIndex] = useState(0);
return (
<div>
<Page index={pageIndex} />
<div style={{ display: "none" }}>
<Page index={pageIndex + 1} />
</div>
<button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
<button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
</div>
);
}
由于 SWR 的缓存,我们可以预加载下一页的页面。我们将下一页的页面渲染到隐藏的 div 元素中,这样 SWR 会触发下一页页面的数据获取。当用户导航到下一页时,数据就已经存在了
Optimistic UI
Optimistic UI 直译过来叫乐观 UI。大白话就是先用本地的数据更新 UI,同时发起 HTTP 请求更新数据,如果 HTTP 请求发生错误,UI 会回滚数据。在这整个过程中,UI 的变化不需要等待
使用 optimisticData
选项,可以手动更新本地数据,同时等待远程数据更改的完成。搭配 rollbackOnError
使用还可以控制何时回滚数据
function Profile() {
const { mutate } = useSWRConfig();
const { data } = useSWR("/api/user", fetcher);
return (
<>
<h1>My name is {data.name}.</h1>
<button
onClick={async () => {
const options = {
optimisticData: newUser,
rollbackOnError(error) {
return error.name !== "AbortError"; // 如果超时中止请求的错误,不执行回滚
},
};
mutate("/api/user", updateFn(newUser), options); //立即更新本地数据, 发送一个请求以更新数据, 触发重新验证(重新请求)确保本地数据正确
}}
>
Uppercase my name!
</button>
</>
);
}
预请求
SWR 提供 preload
API 以编程方式预取资源并将结果存储在缓存中。preload
接受 key
和 fetcher
作为参数。在组件未渲染之前,调用 preload
方法发起 HTTP 请求,将结果缓存在内存中
import { useState } from "react";
import useSWR, { preload } from "swr";
const fetcher = (url) => fetch(url).then((res) => res.json());
// 渲染 User 组件之前发起 HTTP 请求
preload("/api/user", fetcher);
function User() {
const { data } = useSWR("/api/user", fetcher);
}
export default function App() {
const [show, setShow] = useState(false);
return (
<div>
<button onClick={() => setShow(true)}>Show User</button>
{show ? <User /> : null}
</div>
);
}
由于在组件未渲染之前数据已经缓存在内存中,当组件渲染时用户不需要等待 HTTP 请求响应,可以显著提升用户体验
设计原理
“SWR” 这个名字来自 stale-while-revalidate:一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略
这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),当返回数据的时候用最新的数据替换运行的数据。数据的请求和替换的过程都是异步的,对于用户来说无需等待新请求返回时就能看到数据
stale-while-revalidate
是 HTTP 的响应头 cache-control
的一个属性值,它允许立马返回一个已经过时 (stale) 的响应。与此同时浏览器又在背后默默重新发起一次请求,响应的结果被存储起来下次使用。因此它很好的隐藏了服务器或者网络的响应延时
为什么这里说允许返回一个 stale 的响应呢?如何判断响应是 stale 的呢,这是因为 stale-while-revalidate
是和 max-age
一起使用的,如果时间超过了 max-age
,则是作为 stale
Cache-Control: max-age=600, stale-while-revalidate=30
- 表示请求的结果在 600s 内都是新鲜(stale 的反义词)的,如果在 600s 内发起了相同请求,则直接返回磁盘缓存
- 如果在 600s~630s 内发起了相同的请求,则响应虽然已经过时(stale)了,但是浏览器会直接把之前的缓存结果返回,与此同时浏览器又在背后自己发一个请求,响应结果留作下次使用
- 如果超过 630s 后,发起了相同请求,则这就是一个普通请求,就和第一次请求一样,从服务器获取响应结果,并且浏览器并把响应结果缓存起来
源码分析
HTTP 请求的封装
const stateDependencies = useRef<StateDependencies>({}).current;
const data = isUndefined(cachedData) ? fallback : cachedData;
const error = cached.error;
const isValidating = isUndefined(cached.isValidating)
? defaultValidatingState
: cached.isValidating;
const isLoading = isUndefined(cached.isLoading)
? defaultValidatingState
: cached.isLoading;
const cached = useSyncExternalStore(
useCallback(
(callback: () => void) =>
subscribeCache(
key,
(current: State<Data, any>, prev: State<Data, any>) => {
if (!isEqual(prev, current)) callback();
}
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[cache, key]
)
);
重复请求去除和响应数据缓存
if (shouldStartNewRequest) {
//...
FETCH[key] = [currentFetcher(fnArg as DefinitelyTruthy<Key>), getTimestamp()];
}
[newData, startAt] = FETCH[key];
newData = await newData;
if (shouldStartNewRequest) {
setTimeout(cleanupState, config.dedupingInterval);
}
const cleanupState = () => {
// Check if it's still the same request before deleting it.
const requestInfo = FETCH[key];
if (requestInfo && requestInfo[1] === startAt) {
delete FETCH[key];
}
};
通过 shouldStartNewRequest
状态去判断是否需要发起新的请求,去除重复的请求
轮询
function next() {
//...
if (interval && timer !== -1) {
timer = setTimeout(execute, interval);
}
}
function execute() {
if() {
// ...
} else {
next()
}
}
SWR 利用 setTimeout
和递归实现轮询
预请求数据
export const preload = <
Data = any,
SWRKey extends Key = Key,
Fetcher extends BareFetcher = PreloadFetcher<Data, SWRKey>
>(
key_: SWRKey,
fetcher: Fetcher
): ReturnType<Fetcher> => {
const [key, fnArg] = serialize(key_);
// 将响应的结果缓存在全局变量中
const [, , , PRELOAD] = SWRGlobalState.get(cache) as GlobalState;
// Prevent preload to be called multiple times before used.
if (PRELOAD[key]) return PRELOAD[key];
const req = fetcher(fnArg) as ReturnType<Fetcher>;
PRELOAD[key] = req;
return req;
};
组件未渲染之前,提前调用 preload
方法向服务器发送请求,把响应结果缓存在全局变量中,等组件渲染时直接从缓存中读取
Suspense 使用原理
在组件里,throw
Promise 触发 Suspense
的 fallback
逻辑。当组件处理异步逻辑的时候显示 fallback
的 UI,等待异步逻辑操作完成,再显示其他结果。简单的例子如下:
// Example.jsx
const Example = () => {
throw Promise.resolve(9);
return <></>;
};
// App.jsx
function App() {
return (
<ErrorBoundary fallback={<>errors</>}>
<Suspense fallback={<>loading</>}>
<Example />
</Suspense>
</ErrorBoundary>
);
}
SWR 启用 Suspense 逻辑
const use = (promise) => {
if (promise.status === "pending") {
throw promise; // <--- 当前状态是 pending 时,throw promise,执行 Suspense 的 fallback 逻辑
} else if (promise.status === "fulfilled") {
return promise.value as T;
} else if (promise.status === "rejected") {
throw promise.reason;
} else {
promise.status = "pending";
promise.then(/**/);
throw promise;
}
};
边界问题
SWR 只是对请求相关的封装,当涉及跨组件之间状态处理时需要使用第三方状态管理工具
不管是 React 还是 Vue,都强调状态驱动。笔者本人将 React 组件内的状态分为两种,一种是 UI 状态,由 useState
/props
/useReducer
等等维护,另一种是数据状态,来自服务端。而 SWR 只管理数据状态,这就是 SWR 的边界问题,也可以说是 SWR 的适用场景。状态管理工具负责处理组件与组件之间的状态共享。比如比较火的状态管理工具 zustand
- Flux 架构是不是解决状态管理的万金油方案?
是的
- 为什么发布订阅模式可以跨组件传递状态?
因为发布订阅模式具有中心化的 Store, 存储了 Listeners
- 状态管理库从类组件到函数组件本质上发生变化没?
本质上没有发生变化
上面三个问题是对状态管理的深度思考
总结
快速学习新事物的方法论
这是认识新事物的方法论,用于快速了解、掌握和使用新事物。这套方法论使用范围很广,既可以运用在工作中,也可以运用在生活中
参考链接
- SWR - 用于数据请求的 React Hooks 库
- 花了好几个小时,终于懂了什么叫 SWR