·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      // 总结:并不是递归。只是不断从微任务队列取任务执行。

总结

代码整体思路

  1. 函数中有异步操作,使用throw中断函数后续的执行
  2. 异步操作执行完毕时,缓存异步结果(一个异步操作对应一个缓存)
  3. 重新执行函数,异步操作直接返回缓存内容
    1. 怎么重新执行函数?(使用try catch捕获错误,就可以在catch重新执行函数)
    2. 什么时机重新执行函数?(当异步有结果后,即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>

React Suspense