·vincent
消除异步的传染性
消除异步的传染性
Javascript
消除异步的传染性
普通代码
js
1async function getUser() {
2 console.log("getUser");
3 const user = await fetch("https://api.uomg.com/api/rand.qinghua").then(
4 (res) => res.json()
5 );
6 console.log("getUser已获取到用户数据user", user);
7 return user;
8}
9
10async function m1() {
11 console.log("m1");
12 return await getUser();
13}
14
15async function m2() {
16 console.log("m2");
17 return await m1();
18}
19
20async function m3() {
21 console.log("m3");
22 return await m2();
23}
24
25async function main() {
26 console.log("main");
27 const user = await m3();
28 console.log("feng", user);
29}
30
31main();
异步的传染性: 由于getUser有异步操作,导致后续函数全部都需要改造成async函数。
需求: 让函数直接写成普通同步函数,同时运行结果跟async函数一致。
async函数的特点: 异步操作执行完,才执行后续代码。
我们只需要实现async函数的特点即可完成需求。
改造后的代码
js
1function getUser() {
2 // 测试普通错误
3 // throw new Error("普通错误");
4
5 console.log("getUser");
6 const user1 = fetch("https://api.uomg.com/api/rand.qinghua");
7 const user2 = fetch("https://api.uomg.com/api/rand.qinghua");
8 console.log("getUser已获取到用户数据user1", user1);
9 console.log("getUser已获取到用户数据user2", user2);
10 console.log("user1 === user2", user1 === user2);
11 return user1;
12}
13
14function m1() {
15 console.log("m1");
16 return getUser();
17}
18
19function m2() {
20 console.log("m2");
21 return m1();
22}
23
24function m3() {
25 console.log("m3");
26 return m2();
27}
28
29function main() {
30 console.log("main");
31 const user = m3();
32 console.log("feng", user);
33}
34
35// run函数为主要代码,该函数会将异步操作进行处理。
36function run(fn) {
37 // 保留原有fetch,发起请求时使用。
38 const originalFetch = window.fetch;
39 // 缓存结果
40 const cache = [];
41 // 记录fetch调用的顺序,从而缓存多次fetch请求结果。(整条链路上可能有多个fetch调用,需要分别缓存)
42 let i = 0;
43
44 // 改写fetch,当有异步操作时则中断fn执行,异步有结果时,重新执行fn,并将异步结果返回。
45 window.fetch = (...args) => {
46 // 命中缓存
47 if (cache[i]) {
48 const cacheData = cache[i];
49 // 使用i++(第一个fetch执行完,才能执行下一个。所以i的变动放在完成后是较好的)
50 // 第一次fetch未命中缓存,存下标0的位置。第一次fetch有结果,重新执行fn,重置i为0,下标0有结果命中缓存,i++为1。
51 // 第二次fetch未命中缓存,存下标1的位置。第二次fetch有结果,重新执行fn,重置i为0,下标0有结果命中缓存,下标1有结果命中缓存,i++为2。
52 i++;
53 // 判断结果成功与否,从而决定是否抛出错误。
54 // 不需要判断是否等于pending,promise改变了状态才会重新执行fn。
55 if (cacheData.status === "fulfilled") {
56 return cacheData.data;
57 }
58 if (cacheData.status === "rejected") {
59 throw cacheData.err;
60 }
61 } else {
62 // 未命中缓存
63 const result = {
64 status: "pending",
65 data: null,
66 err: null,
67 };
68 cache[i] = result;
69 throw originalFetch(...args)
70 .then((res) => res.json())
71 .then((jsonData) => {
72 result.status = "fulfilled";
73 result.data = jsonData;
74 })
75 .catch((err) => {
76 result.status = "rejected";
77 result.err = err;
78 });
79 }
80 };
81
82 const execute = () => {
83 try {
84 // i需要重置。因为fn可能是重新执行的,需要准确获取缓存。(改写后的fetch,类似React的hook,用调用顺序记录不同的state)
85 i = 0;
86 fn();
87 } catch (err) {
88 // 捕获promise
89 if (err instanceof Promise) {
90 // 当异步有结果时重新执行fn。
91 // 重新执行的fn依旧需要try catch进行错误处理,所以不能单独执行fn。
92 // 注意此处并不是递归,只是不断从微任务队列取任务执行。所以不会栈溢出。但是有可能同一事件循环太多任务,导致页面卡顿。
93 err.then(execute, execute);
94 } else {
95 // 将普通错误抛出去
96 throw err;
97 }
98 }
99 };
100
101 execute();
102}
103
104run(main);
为什么不是递归?
js
1 let i = 0;
2 const fn = () => {
3 console.log("times:", i++);
4 // 将fn放到微任务队列中
5 Promise.resolve().then(fn);
6 };
7 fn();
8 // 流程:
9 // 1. fn()执行过程中将fn放到微任务队列中
10 // 2. fn()执行完毕,事件循环从微任务队列取出任务fn执行
11 // 3. fn()执行,回到第一步。
12 // 总结:并不是递归。只是不断从微任务队列取任务执行。
总结
代码整体思路
- 函数中有异步操作,使用throw中断函数后续的执行
- 异步操作执行完毕时,缓存异步结果(一个异步操作对应一个缓存)
- 重新执行函数,异步操作直接返回缓存内容
- 怎么重新执行函数?(使用try catch捕获错误,就可以在catch重新执行函数)
- 什么时机重新执行函数?(当异步有结果后,即Promise改变状态后,即可重新执行)
优缺点
优点:编写代码时直接编写同步代码即可,不需要使用async、await等
缺点:函数需要多次重复执行,async、await只需要执行一次。假如函数有其他大量计算,将影响性能
共同点:仍然是异步有结果后,才能真正进行下一步操作
完整的html示例
html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <title>Title</title>
6</head>
7<body>
8
9<script>
10 window.asyncFn = () => {
11 return new Promise((resolve, reject) => {
12 setTimeout(() => resolve(2), 2000)
13 })
14 }
15 function m1() {
16 return window.asyncFn()
17 }
18
19 function m2() {
20 return m1()
21 }
22
23 function m3() {
24 return m2()
25 }
26
27 function main() {
28 console.log('11111') // 之前逻辑执行多次
29 const r = m3()
30 console.log('2222')
31 console.log('r', r)
32 }
33
34 function run(func) {
35 const cache = []
36 let i = 0
37 const beforeAsyncFn = window.asyncFn
38 window.asyncFn = (...args) => {
39 if (cache[i]) {
40 if (cache[i].status === 'fulfilled')
41 return cache[i].data
42 }
43
44 const result = {
45 status: 'pending',
46 data: null,
47 err: null,
48 }
49 cache[i++] = result
50 // 执行异步
51 const prom = beforeAsyncFn(...args).then((res) => {
52 result.status = 'fulfilled'
53 result.data = res
54 })
55 throw prom
56 }
57 try {
58 func()
59 }
60 catch (e) {
61 if (e instanceof Promise) {
62 const reRun = () => {
63 i = 0
64 func()
65 }
66 e.then(reRun, reRun)
67 }
68 }
69 }
70
71 run(main)
72</script>
73
74</body>
75</html>