消除异步的传染性

消除异步的传染性

普通代码

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();
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);
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()执行,回到第一步。
      // 总结:并不是递归。只是不断从微任务队列取任务执行。
      let i = 0;
      const fn = () => {
        console.log("times:", i++);
        // 将fn放到微任务队列中
        Promise.resolve().then(fn);
      };
      fn();
      // 流程:
      // 1. fn()执行过程中将fn放到微任务队列中
      // 2. fn()执行完毕,事件循环从微任务队列取出任务fn执行
      // 3. fn()执行,回到第一步。
      // 总结:并不是递归。只是不断从微任务队列取任务执行。

总结

代码整体思路

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

优缺点

优点:编写代码时直接编写同步代码即可,不需要使用async、await等
缺点:函数需要多次重复执行,async、await只需要执行一次。假如函数有其他大量计算,将影响性能
共同点:仍然是异步有结果后,才能真正进行下一步操作

完整的html示例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<script>
    window.asyncFn = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => resolve(2), 2000)
      })
    }
    function m1() {
      return window.asyncFn()
    }

    function m2() {
      return m1()
    }

    function m3() {
      return m2()
    }

    function main() {
      console.log('11111') // 之前逻辑执行多次
      const r = m3()
        console.log('2222')
      console.log('r', r)
    }

    function run(func) {
      const cache = []
      let i = 0
      const beforeAsyncFn = window.asyncFn
      window.asyncFn = (...args) => {
        if (cache[i]) {
          if (cache[i].status === 'fulfilled')
            return cache[i].data
        }

        const result = {
          status: 'pending',
          data: null,
          err: null,
        }
        cache[i++] = result
        // 执行异步
        const prom = beforeAsyncFn(...args).then((res) => {
          result.status = 'fulfilled'
          result.data = res
        })
        throw prom
      }
      try {
        func()
      }
       catch (e) {
        if (e instanceof Promise) {
          const reRun = () => {
            i = 0
            func()
          }
          e.then(reRun, reRun)
        }
      }
    }

    run(main)
</script>

</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<script>
    window.asyncFn = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => resolve(2), 2000)
      })
    }
    function m1() {
      return window.asyncFn()
    }

    function m2() {
      return m1()
    }

    function m3() {
      return m2()
    }

    function main() {
      console.log('11111') // 之前逻辑执行多次
      const r = m3()
        console.log('2222')
      console.log('r', r)
    }

    function run(func) {
      const cache = []
      let i = 0
      const beforeAsyncFn = window.asyncFn
      window.asyncFn = (...args) => {
        if (cache[i]) {
          if (cache[i].status === 'fulfilled')
            return cache[i].data
        }

        const result = {
          status: 'pending',
          data: null,
          err: null,
        }
        cache[i++] = result
        // 执行异步
        const prom = beforeAsyncFn(...args).then((res) => {
          result.status = 'fulfilled'
          result.data = res
        })
        throw prom
      }
      try {
        func()
      }
       catch (e) {
        if (e instanceof Promise) {
          const reRun = () => {
            i = 0
            func()
          }
          e.then(reRun, reRun)
        }
      }
    }

    run(main)
</script>

</body>
</html>

React Suspense