React缓存,如何使用useAsyncMemo()缓存异步函数结果?
发布于 作者:苏南大叔 来源:程序如此灵动~
上一篇文章里面,苏南大叔说可以使用useMemo()缓存普通函数的昂贵计算逻辑结果。在文章的最后,特别强调了useMemo()作用于普通函数,而不能作用于异步函数。异步函数的关键词有:async、await、promise、resolve、reject、.then()等等。本文将使用react的另外一个非官方钩子useAsyncMemo(),实现异步函数结果的缓存。

苏南大叔的“程序如此灵动”博客,记录苏南大叔的代码编程经验总结。本文测试环境:nodejs@20.18.0,create-react-app@5.0.1,react-router-dom@6.27.0,react@18.3.1,use-async-memo@1.2.5。useAsyncMemo()是个钩子,所以只能用在函数式组件里面。
接口代码
异步函数大部分情况下是成功resolve(),但是也存在reject()或者new Error()。在苏南大叔以前的文章里面,还分为setTimeout模拟,以及fetch真实请求两种情况。
对于本文实验用的接口来说,存在一个是否catch异常的问题(reject其实也算个异常):
- 在前面的文章里面,苏南大叔描述,不能
catch,因为这个要由useRequest钩子处理。 - 本篇文章里面,情况有变。这个新的
useAsyncMemo()钩子,不能处理异常。所以,需要自行catch。
本地代码
ApiService.js:
const ApiService = {
fetchDataRemote: function (p1) {
return fetch("http://localhost:3222/get?" + p1)
.then((response) => {
return response.json();
})
.catch(() => {
return { message: "fail" }; //这个算正常返回值
});
},
fetchDataFake: function (p1) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ time: Date.now().toString().slice(-6) });
//reject()需要被catch,干脆别发出吧...
}, 3000);
});
},
};
export default ApiService;远程代码
express.js:
const express = require("express");
const app = express();
var cors = require("cors");
app.use(cors());
app.all("*", (req, res) => {
let ret = {
time: Date.now().toString().slice(-6),
};
res.send(JSON.stringify(ret));
});
const server = app.listen(process.env.PORT || 3222, () => {
const port = server.address().port;
console.log("http://localhost:%d", port);
});不使用useAsyncMemo
假设没有这个use-async-memo的钩子的话,可能代码是这样写的:
现况之useeffeect
import React, { useEffect, useState } from "react";
import ApiService from "./ApiService";
function doRequest(p1) {
// return ApiService.fetchDataRemote(p1);
return ApiService.fetchDataFake(p1);
}
export default function App() {
const [s, setS] = useState(0); // 用作组件更新
const [e, setE] = useState(""); // 用作演示结果
useEffect(() => {
(async () => {
console.log("run e0");
setE(await doRequest());
})();
}, []);
return (
<div>
<div>昂贵值 e: {JSON.stringify(e)}</div>
<button onClick={() => setS(s + 1)}>
组件自身渲染,观察昂贵值是否变化{s}
</button>
</div>
);
}
这里为了避免async延时更新带来死循环。这里使用useEffect()来保证其只执行一次。如果有依赖项的话,也可以添加到useEffect()的第二个参数里面。
现况之死循环
不使用useEffect()的话,可能会陷入死循环。虽然是个类似setInterval的效果,并不是while(true)那样的死循环。
import React, { useEffect, useState } from "react";
import ApiService from "./ApiService";
function doRequest(p1) {
// return ApiService.fetchDataRemote(p1);
return ApiService.fetchDataFake(p1);
}
export default function App() {
const [s, setS] = useState(0); // 用作组件更新
const [e, setE] = useState(""); // 用作演示结果
// useEffect(() => {
(async () => {
console.log("run e0");
setE(await doRequest(s));
})();
// }, [s]);
return (
<div>
<div>昂贵值 e: {JSON.stringify(e)}</div>
<button onClick={() => setS(s + 1)}>
组件自身渲染,观察昂贵值是否变化{s}
</button>
</div>
);
}
useAsyncMemo()
useAsyncMemo()是个非官方的钩子,它的github地址是:
作者看上去是个阿里的神人。useAsyncMemo()对标的就是react官方的useMemo(),所以两者的功用也很相似,就是对函数的结果进行缓存。useAsyncMemo()缓存的是异步函数,useMemo()缓存的是非异步函数。
官方给出的原型是这样的:
function useAsyncMemo<T>(factory: () => Promise<T>, deps: DependencyList, initial?: T): T值得注意的是:这里出现了一个initial?参数。这个参数是useMemo()里面所没有的。经过代码尝试,发现这个参数是用来提供一个结果占位符的。换句话说,就是这个“昂贵的异步函数”还没有返回值的时候,提前先返回一个值。等异步函数正式返回数据之后,就代替这个占位的值。
使用之前,记得先安装use-async-memo:
npm i use-async-memo --saveuseAsyncMemo()之异步
这个useAsyncMemo()的依赖项参数,必须填写,不能省略,否则就是死循环。即使没有依赖项,那么,也需要写成[]。如果需要传递参数的话,依然需要做到:
- 参数取自全局(父级)
- 参数加入依赖项(参数变,函数就变)
import React, { useState } from "react";
import { useAsyncMemo } from "use-async-memo";
import ApiService from "./ApiService";
function doRequest(p1) {
// return ApiService.fetchDataRemote(p1);
return ApiService.fetchDataFake(p1);
}
export default function App() {
const [s, setS] = useState(0); // 用作组件更新
const [n, setN] = useState(1); // 用作useAsyncMemo传参
// const e0 = useAsyncMemo(() => {
// console.log("run e0");
// return doRequest();
// }); // 没有定义依赖,恒定变化,疯狂死循环变化
const e0 = useAsyncMemo(() => {
console.log("run e0");
return doRequest();
}, []); // 无论如何都不变,自身渲染不变,又没有依赖项
const e1 = useAsyncMemo(() => {
console.log("run e1");
return doRequest(n);
}, [n]); //传参n,很合理,注意没出现async和await
const e2 = useAsyncMemo(async () => {
console.log("run e2");
return await doRequest(n);
}, [n]); //和上面的写法很不同,上面没有await和async
return (
<div>
<div>
e0: {JSON.stringify(e0)}[无依赖,恒定不变]
<br />
e1: {JSON.stringify(e1)}[依赖n,n变就变]
<br />
e2: {JSON.stringify(e2)}[依赖n,n变就变]
<br />
</div>
<button onClick={() => setS(s + 1)}>
组件自身渲染, 昂贵值不变。s:{s}
</button>
<br />
<button onClick={() => setN(n + 1)}>n变化, n同时是依赖项(n做参数) n:{n}</button>
</div>
);
}
useAsyncMemo()之默认值
useAsyncMemo()存在第三个参数,就是这个函数的默认值,就是运算结果没返回的时候的占位值。
import React, { useState } from "react";
import { useAsyncMemo } from "use-async-memo";
import ApiService from "./ApiService";
function doRequest(p1) {
// return ApiService.fetchDataRemote(p1);
return ApiService.fetchDataFake(p1);
}
export default function App() {
const [s, setS] = useState(0); // 用作组件更新
const [n, setN] = useState(1); // 用作useAsyncMemo传参
const e3 = useAsyncMemo(async () => {
if (n > 6) return {};
return await doRequest(n);
}, [n]);
const e4 = useAsyncMemo(
async () => {
if (n > 6) return {};
return await doRequest(n);
},
[n],
{ sunan: "这是默认值" }
);
return (
<div>
<div>
e3: {JSON.stringify(e3)}[依赖n,有条件返回不同值,async+await]
<br />
e4: {JSON.stringify(e4)}[依赖n,存在一个默认值,至少三种变化]
<br />
</div>
<button onClick={() => setS(s + 1)}>
组件自身渲染, 昂贵值不变。s:{s}
</button>
<br />
<button onClick={() => setN(n + 1)}>
n变化, n同时是依赖项(n做参数) n:{n}
</button>
</div>
);
}
useAsyncMemo源码
如果查看useAsyncMemo源码的话,就会发现:它的背后就是个useEffect()。其TypeScript的代码极其简单:
import {DependencyList, useEffect, useState} from 'react'
export function useAsyncMemo<T>(factory: () => Promise<T> | undefined | null, deps: DependencyList, initial?: T) {
const [val, setVal] = useState<T | undefined>(initial)
useEffect(() => {
let cancel = false
const promise = factory()
if (promise === undefined || promise === null) return
promise.then((val) => {
if (!cancel) {
setVal(val)
}
})
return () => {
cancel = true
}
}, deps)
return val
}学习一下人家的代码思路,总归是极好的。
useAsyncMemo()更好的例子
这个例子比较具有实际意义,参考:
import { useAsyncMemo } from "use-async-memo";
function MyComponent({ uid }) {
const info = useAsyncMemo(async () => {
const response = await fetch(`http://localhost:3222/get/${uid}`);
return response.json();
}, [uid]);// 这个会引发再次渲染的
if (info === undefined) {
return <div>Loading...</div>;
}
return (
<div>
<p>{info?.email || '--'}</p>
</div>
);
}
export default function App() {
return (
<>
<MyComponent uid={123} />
<MyComponent uid={456} />
</>
);
}
这段代码的理解上,就是useAsyncMemo()会有个状态的变化过程,会再次引起组件渲染。
对应的接口服务:
const express = require("express");
const app = express();
var cors = require("cors");
app.use(cors());
app.get("/get/:uid", (req, res) => {
// req.params.id <=> req.query.id
let ret = {
email: "sunan" + req.params.uid + "@pku.edu.cn",
time: Date.now().toString().slice(-6),
};
res.send(JSON.stringify(ret));
});
const server = app.listen(process.env.PORT || 3222, () => {
const port = server.address().port;
console.log("http://localhost:%d", port);
});相关文章
- https://newsn.net/say/js-promise.html
- https://newsn.net/say/js-request-get.html
- https://newsn.net/say/js-request-fake.html