普通代码
async function getUser() {
console.log("getUser");
const user = await fetch("https://api.uomg.com/api/rand.qinghua").then(
(res) => res.json()
);
console.log("getUser已获取到用户数据user", user);
return user;
}
async function m1() {
console.log("m1");
return await getUser();
}
async function m2() {
console.log("m2");
return await m1();
}
async function m3() {
console.log("m3");
return await m2();
}
async function main() {
console.log("main");
const user = await m3();
console.log("feng", user);
}
main();
异步的传染性: 由于getUser有异步操作,导致后续函数全部都需要改造成async函数。
需求: 让函数直接写成普通同步函数,同时运行结果跟async函数一致。
async函数的特点: 异步操作执行完,才执行后续代码。
我们只需要实现async函数的特点即可完成需求。
改造后的代码
function getUser() {
// 测试普通错误
// throw new Error("普通错误");
console.log("getUser");
const user1 = fetch("https://api.uomg.com/api/rand.qinghua");
const user2 = fetch("https://api.uomg.com/api/rand.qinghua");
console.log("getUser已获取到用户数据user1", user1);
console.log("getUser已获取到用户数据user2", user2);
console.log("user1 === user2", user1 === user2);
return user1;
}
function m1() {
console.log("m1");
return getUser();
}
function m2() {
console.log("m2");
return m1();
}
function m3() {
console.log("m3");
return m2();
}
function main() {
console.log("main");
const user = m3();
console.log("feng", user);
}
// run函数为主要代码,该函数会将异步操作进行处理。
function run(fn) {
// 保留原有fetch,发起请求时使用。
const originalFetch = window.fetch;
// 缓存结果
const cache = [];
// 记录fetch调用的顺序,从而缓存多次fetch请求结果。(整条链路上可能有多个fetch调用,需要分别缓存)
let i = 0;
// 改写fetch,当有异步操作时则中断fn执行,异步有结果时,重新执行fn,并将异步结果返回。
window.fetch = (...args) => {
// 命中缓存
if (cache[i]) {
const cacheData = cache[i];
// 使用i++(第一个fetch执行完,才能执行下一个。所以i的变动放在完成后是较好的)
// 第一次fetch未命中缓存,存下标0的位置。第一次fetch有结果,重新执行fn,重置i为0,下标0有结果命中缓存,i++为1。
// 第二次fetch未命中缓存,存下标1的位置。第二次fetch有结果,重新执行fn,重置i为0,下标0有结果命中缓存,下标1有结果命中缓存,i++为2。
i++;
// 判断结果成功与否,从而决定是否抛出错误。
// 不需要判断是否等于pending,promise改变了状态才会重新执行fn。
if (cacheData.status === "fulfilled") {
return cacheData.data;
}
if (cacheData.status === "rejected") {
throw cacheData.err;
}
} else {
// 未命中缓存
const result = {
status: "pending",
data: null,
err: null,
};
cache[i] = result;
throw originalFetch(...args)
.then((res) => res.json())
.then((jsonData) => {
result.status = "fulfilled";
result.data = jsonData;
})
.catch((err) => {
result.status = "rejected";
result.err = err;
});
}
};
const execute = () => {
try {
// i需要重置。因为fn可能是重新执行的,需要准确获取缓存。(改写后的fetch,类似React的hook,用调用顺序记录不同的state)
i = 0;
fn();
} catch (err) {
// 捕获promise
if (err instanceof Promise) {
// 当异步有结果时重新执行fn。
// 重新执行的fn依旧需要try catch进行错误处理,所以不能单独执行fn。
// 注意此处并不是递归,只是不断从微任务队列取任务执行。所以不会栈溢出。但是有可能同一事件循环太多任务,导致页面卡顿。
err.then(execute, execute);
} else {
// 将普通错误抛出去
throw err;
}
}
};
execute();
}
run(main);
为什么不是递归?
let i = 0;
const fn = () => {
console.log("times:", i++);
// 将fn放到微任务队列中
Promise.resolve().then(fn);
};
fn();
// 流程:
// 1. fn()执行过程中将fn放到微任务队列中
// 2. fn()执行完毕,事件循环从微任务队列取出任务fn执行
// 3. fn()执行,回到第一步。
// 总结:并不是递归。只是不断从微任务队列取任务执行。
总结
代码整体思路
- 函数中有异步操作,使用throw中断函数后续的执行
- 异步操作执行完毕时,缓存异步结果(一个异步操作对应一个缓存)
- 重新执行函数,异步操作直接返回缓存内容
- 怎么重新执行函数?(使用try catch捕获错误,就可以在catch重新执行函数)
- 什么时机重新执行函数?(当异步有结果后,即Promise改变状态后,即可重新执行)
优缺点
优点:编写代码时直接编写同步代码即可,不需要使用async、await等
缺点:函数需要多次重复执行,async、await只需要执行一次。假如函数有其他大量计算,将影响性能
共同点:仍然是异步有结果后,才能真正进行下一步操作